Compare commits

..

125 Commits

Author SHA1 Message Date
Neeraj Gupta
97a9fd4cb7 Search test 2025-08-27 11:47:09 +05:30
Neeraj Gupta
bea840d891 Initial integration test 2025-08-27 00:33:06 +05:30
Neeraj
923f2484fb [auth] Fix missing token (#6971)
## Description
When using Auth without backup, it was giving a error `Offline key is
missing`

**Reason**: During the `init` of `BaseConfiguration` if the `tokenKey`
is not set, we clear all the keys in the secure storage, and in this
process the `offlineAuthSecretKey` was also getting cleared

**Fix** Fixed by skipping the deletion of `offlineAuthSecretKey`  

## Tests
[Test Video](https://wormhole.app/qz3mol#Dlhr0NRpVQVQsrid2X-quA)
2025-08-26 13:02:43 +05:30
AmanRajSinghMourya
37928cd2c6 Code refractoring 2025-08-26 12:40:03 +05:30
AmanRajSinghMourya
fc32ba97c1 Refactor BaseConfiguration to ensure preserved keys are not deleted 2025-08-26 12:26:01 +05:30
AmanRajSinghMourya
e49084867e Revert "Refactor BaseConfiguration to preserve offlineAuthSecretKey during logout"
This reverts commit 5b5f563d47.
2025-08-26 12:21:00 +05:30
Laurens Priem
a046748ded [mob][photos] Minor fixes and changes (#6969)
## Description

Minor fixes and changes based on testing.

## Tests

Tested in debug mode on my pixel phone.
2025-08-26 10:08:42 +05:30
laurenspriem
047d708ef1 Merge branch 'main' into fix_modal 2025-08-26 10:08:18 +05:30
AmanRajSinghMourya
5b5f563d47 Refactor BaseConfiguration to preserve offlineAuthSecretKey during logout 2025-08-26 10:02:19 +05:30
Ashil
2b60ad3748 [mob][packages] Organize imports (#6968) 2025-08-26 10:01:40 +05:30
Neeraj
1f70043c83 [mob][photos] video streaming settings & create/recreate stream (#6923) 2025-08-26 10:00:08 +05:30
laurenspriem
7ce6f6a346 Check symlink permissions 2025-08-26 09:55:17 +05:30
ashilkn
03814bff0c Organize imports 2025-08-26 09:34:00 +05:30
laurenspriem
4c63a0ff13 Copy changes 2025-08-26 09:21:59 +05:30
laurenspriem
93552fb872 vectorDB flag check ML enabled 2025-08-26 09:05:06 +05:30
laurenspriem
1b61becdcf Fix modal on group-level delete 2025-08-26 08:37:16 +05:30
Prateek Sunal
0499cad3c9 chore: ignore generated mocks file 2025-08-26 04:48:32 +05:30
Prateek Sunal
79752ef4b8 chore: update file name 2025-08-26 04:35:51 +05:30
Prateek Sunal
c1bd6d3fdb Merge branch 'taking-streaming-oob' of https://github.com/ente-io/ente into taking-streaming-oob 2025-08-26 04:35:30 +05:30
Prateek Sunal
621423d9a4 fix: refactor code 2025-08-26 04:35:27 +05:30
Prateek Sunal
edb11c89ba fix: update mobile/apps/photos/lib/ui/settings/streaming/video_streaming_settings_page.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-26 04:31:44 +05:30
Prateek Sunal
adb71fe09c fix: update mobile/apps/photos/lib/services/video_preview_service.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-26 04:31:12 +05:30
Prateek Sunal
c20cee2406 feat: state update event, tests, total logic 2025-08-26 04:27:38 +05:30
Prateek Sunal
dcfad86c47 fix: ensure preview ids are present 2025-08-25 23:32:57 +05:30
Prateek Sunal
0a2bff67bf fix: delete file from upload locks db if not found 2025-08-25 23:31:51 +05:30
Prateek Sunal
7aaa689cfb fix: simplify logic 2025-08-25 23:26:34 +05:30
Prateek Sunal
ad2a0ce897 refactor: simplify StreamingStatus handling in VideoPreviewService 2025-08-25 23:22:39 +05:30
Prateek Sunal
d99615b24f fix: remove skipped 2025-08-25 22:16:22 +05:30
Prateek Sunal
09cc48ae55 fix: add comments 2025-08-25 21:50:07 +05:30
Prateek Sunal
6ab2223a80 fix: mostly all review comments 2025-08-25 21:42:40 +05:30
Prateek Sunal
6fd86162e0 Merge remote-tracking branch 'origin/main' into taking-streaming-oob 2025-08-25 20:58:09 +05:30
Prateek Sunal
707e8dbfcf fix: show processing status, fix when to not show popup buttons, update getFiles logic 2025-08-25 20:57:18 +05:30
Prateek Sunal
5869bec781 feat: create and recreate stream buttons 2025-08-25 20:11:08 +05:30
Laurens Priem
e311a8bb32 [mob][photos] Similar images UI (#6963)
## Description

Minor fixes and UI changes.

## Tests

Tested in debug mode on my pixel phone.
2025-08-25 16:55:29 +05:30
laurenspriem
547ccfceca Use shared preferences for tracking migration 2025-08-25 16:53:33 +05:30
Keerthana
3a1917949b [mobile/photos] New translations (#6955)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-app)
2025-08-25 13:30:03 +05:30
Keerthana
3a1ce3258e [auth] New translations (#6956)
New translations from
[Crowdin](https://crowdin.com/project/ente-authenticator-app)
2025-08-25 13:10:39 +05:30
Prateek Sunal
13b2542bea fix: update logic 2025-08-25 12:02:56 +05:30
Prateek Sunal
6db3741a3b Merge remote-tracking branch 'origin/main' into taking-streaming-oob 2025-08-25 11:48:49 +05:30
laurenspriem
ce17eccd68 Fix delete issue 2025-08-25 11:11:57 +05:30
Ashil
95dc683088 Update internal change log (#6959) 2025-08-25 11:11:28 +05:30
Neeraj
cf9d5f72f7 [mob] Fix query for duplicate cleanup (#6962)
## Description
Only consider owned files
## Tests
2025-08-25 11:10:36 +05:30
Neeraj Gupta
3096e1550a Merge remote-tracking branch 'origin/main' into fixQuery 2025-08-25 11:03:45 +05:30
Neeraj Gupta
1b39435735 Fix query 2025-08-25 11:02:25 +05:30
laurenspriem
8f3d8505bb Better UI when selection is not possible 2025-08-25 11:02:03 +05:30
Manav Rathi
47e8aafe25 [desktop] Update changelog (#6960) 2025-08-25 10:50:10 +05:30
Manav Rathi
edf32d065e [desktop] Update changelog 2025-08-25 10:47:43 +05:30
ashilkn
1fa6a0c3b9 Update internal change log 2025-08-25 10:41:16 +05:30
Manav Rathi
2388989dd0 [web] Enable Czech (#6958) 2025-08-25 10:35:06 +05:30
laurenspriem
9e392277b1 Fix initState issue 2025-08-25 10:31:41 +05:30
Neeraj
4609c375db Revert "[auth] Add smaller Activision icon" (#6953)
Reverts ente-io/ente#6950

I rushed a bit, sorry. The PR wasn't meant to be merged yet (if ever)
and it won't work right now anyway. It was meant to create conversation
on the topic and then possibly merged and there may be concerns to this
as a company may not want their logo/wordmark altered but I'm not well
versed in this topic (idk maybe I'm overthinking this).

Discussion: #6951
2025-08-25 10:31:20 +05:30
Manav Rathi
839c62ea72 [web] Enable Czech 2025-08-25 10:28:23 +05:30
Neeraj
dceef49f33 [mob][photos] Pre-cache thumbnails fetched from LRU cache to Flutter's ImageCache for faster rendering (#6957)
## Description

In Gallery, even if thumbnails are stored in LRU cache, there was a
delay in rendering thumbnails when scrolling fast enough. Pre-caching
these thumbnails to flutter's `ImageCache` right before they're rendered
has made the rendering fast enough for seamless UX.

#### Before:


https://github.com/user-attachments/assets/c47958fb-fbda-4e1f-9ce7-26b51ca87938

#### After:


https://github.com/user-attachments/assets/cbaf4427-f52f-4544-a0c2-820eb2b43953
2025-08-25 10:18:05 +05:30
laurenspriem
acbdc3111a Remove use of withOpacity 2025-08-25 10:15:49 +05:30
laurenspriem
98b91a6935 Remove duplicate string 2025-08-25 10:11:07 +05:30
laurenspriem
e1640e67d4 Extract strings 2025-08-25 10:09:03 +05:30
laurenspriem
e875758419 Simplify sort options 2025-08-25 09:48:33 +05:30
Manav Rathi
214b120472 [web] New translations (#6954)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-web)
2025-08-25 09:43:37 +05:30
ashilkn
f139e0a098 Fix flickering of GalleryFileWidget on hero animation after closing it's open full view 2025-08-25 09:21:57 +05:30
laurenspriem
e3c9a61887 Align 2025-08-25 09:20:19 +05:30
ashilkn
0da3dc5084 Skip clearing flutter image cache since default (current) limit is 100MB and the threshold to clear is 250MB 2025-08-25 09:20:08 +05:30
ashilkn
a856a82249 Refactor 2025-08-25 09:18:29 +05:30
ashilkn
fbdec00a62 Improve lru cache thumbnail rendering speed when scrolling gallery by precaching it it flutter's image cache 2025-08-25 09:16:59 +05:30
Crowdin Bot
6a7f980a0d New Crowdin translations by GitHub Action 2025-08-25 01:18:00 +00:00
Crowdin Bot
10a855fe27 New Crowdin translations by GitHub Action 2025-08-25 01:05:02 +00:00
Crowdin Bot
b4f8a2b27c New Crowdin translations by GitHub Action 2025-08-25 00:39:57 +00:00
dnred
89489b4d7c Revert "[auth] Add smaller Activision icon" 2025-08-24 23:48:21 +02:00
Aman Raj Singh Mourya
50296f8dfa [auth] Add smaller Activision icon (#6950)
## Description

The current Activision icon is too wide and small to be nicely displayed
in Auth so this PR adds a smaller one, just like the favicon on
Activision's [website](https://activision.com).

I know the Activision icon is pulled from simple-icons and I don't want
to get rid of that, just add an option for a smaller one, but I see that
the smaller Allegro icon is also added but it isn't displayed in the
icon picker and the icon from simple-icons takes precedence so you'd
have to figure this out.

## Tests

I haven't tested this.
2025-08-24 23:15:44 +05:30
dnred
f69cec864b Rename activision2.svg to activision.svg 2025-08-24 16:42:49 +02:00
dnred
73d5d33fc5 Update custom-icons.json 2025-08-24 16:25:47 +02:00
dnred
4d8ea12ddd Add logo 2025-08-24 16:17:25 +02:00
Prateek Sunal
7beb267ba7 chore: remove unused import 2025-08-24 02:15:09 +05:30
Prateek Sunal
7e13ef3537 chore: remove formatting for files_db 2025-08-24 02:10:06 +05:30
Prateek Sunal
47edca5bf5 chore: fix formatting 2025-08-24 02:08:00 +05:30
Prateek Sunal
925ba10b15 chore: revert remote_sync formatting 2025-08-24 02:06:28 +05:30
Prateek Sunal
db2d0bb7e9 fix: remove formatting from ml_service 2025-08-24 02:04:09 +05:30
Prateek Sunal
f3a2b2af0c fix: include it in if loop 2025-08-24 02:02:47 +05:30
Prateek Sunal
967e88f88d Merge remote-tracking branch 'origin/main' into taking-streaming-oob 2025-08-24 02:00:33 +05:30
Prateek Sunal
b44734a493 fix: add streaming static image 2025-08-24 01:59:23 +05:30
Manav Rathi
6478b08a19 [docs] Minor improvements to self-hosted docs (#6936)
## Description
- Small correction on the self-hosted docker exec command.
- Added tip on how to install Ente CLI.

In spirit of starting with a small PR :p
2025-08-23 21:54:47 +05:30
Laurens Priem
314e81565b Fix linter issues (#6939)
## Description

Linter was failing because it didn't first run
`flutter_rust_bridge_codegen generate` to generate the dart bindings to
rust code.
2025-08-23 20:34:02 +05:30
laurenspriem
f95e20d00f Consistent tap behaviour 2025-08-23 17:35:12 +05:30
laurenspriem
35a04d6e7e Don't unnecessarily sort 2025-08-23 17:34:53 +05:30
laurenspriem
403264d2c9 Fix linter issues 2025-08-23 17:20:22 +05:30
Toby
6b06a4c388 Add instructions on how to install Ente CLI 2025-08-23 13:13:08 +02:00
Toby
678bce89b2 Add small corrections to docker commands 2025-08-23 13:06:34 +02:00
Laurens Priem
2f1d4b9f1a Update rust to solve bindings generation issue (#6935)
## Description

Update rust to potentially solve bindings generation issue
2025-08-23 10:44:27 +05:30
laurenspriem
af20eadff0 Update rust to solve bindings generation issue 2025-08-23 10:39:54 +05:30
Prateek Sunal
3264ea046c [mob][photos] add named params for translations (#6932)
## Description

Parameters were sorted based on name by intl plugin which is a breaking
change.

We now have shifted to named params for translations so position won't
change.

## Tests
2025-08-22 22:50:07 +05:30
Laurens Priem
d81a73c833 [mob][photos] Similar images various improvements (#6931)
## Description

- Put the rust generated bindings in gitignore
- Use `view` instead of `load` on VectorDB index to use less RAM
- Various UI changes

## Tests

Tested in debug mode on my pixel phone.
2025-08-22 21:12:00 +05:30
laurenspriem
ac9c63fe29 Log updates 2025-08-22 21:11:25 +05:30
laurenspriem
53cb217dbc Fix MediaQuery in initState issue (for large files view) 2025-08-22 21:06:59 +05:30
Prateek Sunal
fca9a42e0a fix: add named params for translations to fix position 2025-08-22 19:41:45 +05:30
Prateek Sunal
8b708228be fix: add different ui for enabling it 2025-08-22 19:04:16 +05:30
laurenspriem
d379262f56 Small animation 2025-08-22 18:57:37 +05:30
laurenspriem
9282632af1 Exclude dart linter checks for rust 2025-08-22 18:14:13 +05:30
laurenspriem
6a43d6a567 Generate rust bindings on internal release 2025-08-22 18:11:53 +05:30
laurenspriem
1cdbef1a01 More fancy loading 2025-08-22 15:12:12 +05:30
laurenspriem
fa84bb0845 Delete progress indicator 2025-08-22 14:33:19 +05:30
laurenspriem
cbb6f07d0d Improve empty state 2025-08-22 12:38:37 +05:30
laurenspriem
fad9cf8559 Add rust bindings generation to readme 2025-08-22 11:57:45 +05:30
laurenspriem
371ba9c552 Put generated rust bindings in gitignore 2025-08-22 11:54:59 +05:30
laurenspriem
19086e43cc Remove generated rust bindings 2025-08-22 11:54:24 +05:30
laurenspriem
964c837c40 Remove scrollbar 2025-08-22 11:36:48 +05:30
laurenspriem
d85121862d Use view of index instead of loading in memory 2025-08-22 11:34:40 +05:30
Prateek Sunal
42d31a73a3 fix: update localization & fix lint 2025-08-21 23:32:45 +05:30
Prateek Sunal
946d2ae522 fix: remove unnecessary blank line in analysis_options.yaml + circular button 2025-08-21 22:51:01 +05:30
Prateek Sunal
8e9eb50783 fix: downgrade Dart SDK version to 3.7.2 2025-08-21 22:50:24 +05:30
Prateek Sunal
af3bc7757f fix: downgrade dart sdk again 2025-08-21 22:50:04 +05:30
Prateek Sunal
eda1d05216 fix: revert dart sdk 2025-08-21 22:49:49 +05:30
Prateek Sunal
b58e0f8331 fix: remove analysis options for now 2025-08-21 22:49:37 +05:30
Prateek Sunal
6dcf53650d fix: re-add captioned text 2025-08-21 22:48:42 +05:30
Prateek Sunal
bff53d9081 chore: add back space 2025-08-21 22:47:42 +05:30
Prateek Sunal
f3306e14c7 fix: revert 2025-08-21 22:47:30 +05:30
Prateek Sunal
b5c075bac4 fix: add space at end 2025-08-21 22:45:38 +05:30
Prateek Sunal
241d21c2aa fix: revert things 2025-08-21 22:44:34 +05:30
Prateek Sunal
789d77747c fix: update other contact page 2025-08-21 22:35:24 +05:30
Prateek Sunal
35050aa32f fix: delete generated intl_utils locale 2025-08-21 22:33:23 +05:30
Prateek Sunal
40e6bd9fae Merge remote-tracking branch 'origin/main' into taking-streaming-oob 2025-08-21 22:32:41 +05:30
Prateek Sunal
b317df2000 Merge branch 'main' into taking-streaming-oob 2025-08-21 18:11:42 +05:30
Prateek Sunal
2e706228ee feat: separate settings page improvements 2025-08-21 17:22:57 +05:30
Prateek Sunal
fe40185889 fix: redirect video streaming to setting page 2025-08-20 14:43:54 +05:30
Prateek Sunal
6bb4428a8a chore: update generated translations 2025-08-20 14:41:57 +05:30
Prateek Sunal
0881685915 feat: add video streaming settings page 2025-08-20 14:34:16 +05:30
Prateek Sunal
3e13932d03 chore: format files and update analysis file 2025-08-20 14:33:35 +05:30
366 changed files with 6909 additions and 5797 deletions

View File

@@ -8,7 +8,7 @@ on:
env:
FLUTTER_VERSION: "3.32.8"
RUST_VERSION: "1.85.1"
RUST_VERSION: "1.86.0"
permissions:
contents: read
@@ -47,6 +47,9 @@ jobs:
- name: Install Flutter Rust Bridge
run: cargo install flutter_rust_bridge_codegen
- name: Generate Rust bindings
run: flutter_rust_bridge_codegen generate
- name: Increment version code for build
run: |
CURRENT_VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //')

View File

@@ -9,6 +9,7 @@ on:
env:
FLUTTER_VERSION: "3.32.8"
RUST_VERSION: "1.86.0"
permissions:
contents: read
@@ -31,7 +32,18 @@ jobs:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- run: flutter pub get
- name: Install Rust ${{ env.RUST_VERSION }}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.RUST_VERSION }}
- name: Install Flutter Rust Bridge
run: cargo install flutter_rust_bridge_codegen
- name: Generate Rust bindings
run: flutter_rust_bridge_codegen generate
- run: flutter analyze --no-fatal-infos

View File

@@ -2,6 +2,8 @@
## v1.7.15 (Unreleased)
- Custom domains.
- Support Czech translations.
- .
## v1.7.14

View File

@@ -8,6 +8,12 @@ description: Guide to configuring Ente CLI for Self Hosted Instance
If you are self-hosting, you can configure Ente CLI to export data & perform
basic administrative actions.
::: tip Installing Ente CLI
For instructions on installing the Ente CLI, see the [README available on Github](https://github.com/ente-io/ente/tree/main/cli/README.md).
:::
## Step 1: Configure endpoint
To do this, first configure the CLI to use your server's endpoint.

View File

@@ -22,8 +22,7 @@ can achieve this the following steps:
# Change the DB name and DB user name if you use different
# values.
# If using Docker
docker exec -it <postgres-ente-container-name>
docker exec -it <postgres-ente-container-name> sh
psql -U pguser -d ente_db
# Or when using psql directly

View File

@@ -46,7 +46,7 @@ If running Museum without Docker, the code should be visible in the terminal
# Change the DB name and DB user name if you use different
# values.
# If using Docker docker exec -it <postgres-ente-container-name>
# If using Docker docker exec -it <postgres-ente-container-name> sh
psql -U pguser -d ente_db
# Or when using psql directly

View File

@@ -33,6 +33,8 @@
.pub/
/build/
macos/build/
.gradle/
settings.local.json
# Web related
lib/generated_plugin_registrant.dart

View File

@@ -0,0 +1,576 @@
import 'package:ente_auth/app/view/app.dart';
import 'package:ente_auth/bootstrap.dart';
import 'package:ente_auth/main.dart';
import 'package:ente_auth/onboarding/view/common/add_chip.dart';
import 'package:ente_auth/services/update_service.dart';
import 'package:ente_lock_screen/local_authentication_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Authentication Flow Integration Test with Persistence', () {
testWidgets(
'Complete auth flow: Use without backup -> Enter setup key -> Verify entry persists after restart',
(WidgetTester tester) async {
// Bootstrap the app
await bootstrap(App.new);
await init(false, via: 'integrationTest');
await UpdateService.instance.init();
await tester.pumpAndSettle();
// Step 1: Click on "Use without backup" option
final useOfflineText = find.text('Use without backups');
expect(
useOfflineText,
findsOneWidget,
reason:
'ERROR: "Use without backups" button not found on initial screen. Check if app loaded correctly or text changed.',
);
await tester.tap(useOfflineText);
await tester.pumpAndSettle();
// Step 2: Click OK button on the warning dialog
final okButton = find.text('Ok');
expect(
okButton,
findsOneWidget,
reason:
'ERROR: "Ok" button not found in warning dialog. Check if dialog appeared or button text changed.',
);
await tester.tap(okButton);
await tester.pumpAndSettle();
// Wait for navigation to complete
await tester.pumpAndSettle(const Duration(seconds: 2));
// Step 3: Navigate to manual entry screen
bool foundManualEntry = false;
// Try FloatingActionButton approach first
final fabFinder = find.byType(FloatingActionButton);
if (fabFinder.evaluate().isNotEmpty) {
await tester.tap(fabFinder);
await tester.pumpAndSettle();
final manualEntryFinder = find.text('Enter a setup key');
if (manualEntryFinder.evaluate().isNotEmpty) {
await tester.tap(manualEntryFinder);
await tester.pumpAndSettle();
foundManualEntry = true;
}
}
// Alternative approaches if FAB didn't work
if (!foundManualEntry) {
final alternatives = [
'Enter details manually',
'Enter a setup key',
];
for (final text in alternatives) {
final finder = find.text(text);
if (finder.evaluate().isNotEmpty) {
await tester.tap(finder.first);
await tester.pumpAndSettle();
foundManualEntry = true;
break;
}
}
}
expect(
foundManualEntry,
isTrue,
reason:
'ERROR: Could not find manual entry option. Tried FAB + "Enter details manually", "Enter a setup key", etc. Check UI navigation.',
);
// Step 4: Fill in the form with test data
final textFields = find.byType(TextFormField);
expect(
textFields.evaluate().length,
greaterThanOrEqualTo(3),
reason:
'ERROR: Expected at least 3 text fields (issuer, secret, account) but found ${textFields.evaluate().length}. Check manual entry form.',
);
// Fill issuer field
await tester.tap(textFields.first);
await tester.enterText(textFields.first, 'testIssuer');
await tester.pumpAndSettle();
// Fill secret field
await tester.tap(textFields.at(1));
await tester.enterText(
textFields.at(1),
'JBSWY3DPEHPK3PXP',
); // Valid base32 secret
await tester.pumpAndSettle();
// Fill account field
await tester.tap(textFields.at(2));
await tester.enterText(textFields.at(2), 'testAccount');
await tester.pumpAndSettle();
// Step 5: Save the entry
final saveButton = find.text('Save');
expect(
saveButton,
findsOneWidget,
reason:
'ERROR: "Save" button not found on manual entry form. Check if button text changed or form layout changed.',
);
await tester.tap(saveButton);
await tester.pumpAndSettle();
// Step 6: Verify entry was created successfully
await tester.pumpAndSettle(const Duration(seconds: 1));
// Check if coach mark overlay is present and dismiss it
final coachMarkOverlay = find.text('Ok');
if (coachMarkOverlay.evaluate().isNotEmpty) {
print('🎯 Dismissing coach mark overlay...');
await tester.tap(coachMarkOverlay);
await tester.pumpAndSettle();
}
// Look for the created entry
final issuerEntryFinder = find.textContaining('testIssuer');
expect(
issuerEntryFinder,
findsAtLeastNWidgets(1),
reason:
'ERROR: testIssuer entry not found after saving. Entry creation may have failed or navigation issue occurred.',
);
final accountEntryFinder = find.textContaining('testAccount');
expect(
accountEntryFinder,
findsAtLeastNWidgets(1),
reason:
'ERROR: testAccount not found after saving. Account field may not have been saved properly.',
);
print('✅ Step 1 completed: Entry created successfully');
print('- testIssuer entry is visible');
print('- testAccount is visible');
// warning about clearing
// Step 8: Add second code entry
print('🔄 Adding second code entry...');
// Wait a moment before adding second entry
await tester.pumpAndSettle(const Duration(seconds: 1));
// Click FAB to add second entry
final fabFinder2 = find.byType(FloatingActionButton);
expect(
fabFinder2,
findsOneWidget,
reason: 'FAB not found for second entry',
);
await tester.tap(fabFinder2);
await tester.pumpAndSettle();
// Click "Enter details manually" (coach mark won't show second time)
final manualEntryFinder = find.text('Enter details manually');
expect(
manualEntryFinder,
findsOneWidget,
reason: 'Manual entry option not found',
);
await tester.tap(manualEntryFinder);
await tester.pumpAndSettle();
// Fill second entry form
final textFields2 = find.byType(TextFormField);
expect(textFields2.evaluate().length, greaterThanOrEqualTo(3));
// Fill second issuer field
await tester.tap(textFields2.first);
await tester.enterText(textFields2.first, 'testIssuer2');
await tester.pumpAndSettle();
// Verify issuer field was filled
final issuerField = tester.widget<TextFormField>(textFields2.first);
print(
'✓ Issuer field controller text: "${issuerField.controller?.text ?? "null"}"');
// Fill second secret field
await tester.tap(textFields2.at(1));
await tester.enterText(textFields2.at(1), 'JBSWY3DPEHPK3PXP');
await tester.pumpAndSettle();
// Verify secret field was filled
final secretField = tester.widget<TextFormField>(textFields2.at(1));
print(
'✓ Secret field controller text: "${secretField.controller?.text ?? "null"}"');
// Fill second account field
await tester.tap(textFields2.at(2));
await tester.enterText(textFields2.at(2), 'testAccount2');
await tester.pumpAndSettle();
// Save second entry
final saveButton2 = find.text('Save');
await tester.tap(saveButton2);
await tester.pumpAndSettle();
await tester.pumpAndSettle(const Duration(seconds: 2));
// Verify both entries exist
final issuer1Finder = find.textContaining('testIssuer');
final issuer2Finder = find.textContaining('testIssuer2');
final account1Finder = find.textContaining('testAccount');
final account2Finder = find.textContaining('testAccount2');
expect(issuer1Finder, findsAtLeastNWidgets(1),
reason: 'First issuer not found');
expect(issuer2Finder, findsAtLeastNWidgets(1),
reason: 'Second issuer not found');
expect(
account1Finder,
findsAtLeastNWidgets(1),
reason: 'First account not found',
);
expect(
account2Finder,
findsAtLeastNWidgets(1),
reason: 'Second account not found',
);
print('✅ Step 2 completed: Both entries created successfully');
print('- testIssuer and testIssuer2 entries are visible');
print('- testAccount and testAccount2 are visible');
// Step 9: Test search functionality
print('🔍 Testing search functionality...');
// Click on search icon to activate search
final searchIcon = find.byIcon(Icons.search);
expect(searchIcon, findsOneWidget, reason: 'Search icon not found');
await tester.tap(searchIcon);
await tester.pumpAndSettle();
// Find the search text field
final searchField = find.byType(TextField);
expect(searchField, findsOneWidget,
reason: 'Search text field not found');
// Enter search term "issuer2"
await tester.tap(searchField);
await tester.enterText(searchField, 'issuer2');
await tester.pumpAndSettle();
// Verify only one result is shown (testIssuer2)
final searchResults = find.textContaining('testIssuer');
final issuer2Results = find.textContaining('testIssuer2');
// Should find testIssuer2 but not testIssuer when searching for "issuer2"
expect(issuer2Results, findsAtLeastNWidgets(1),
reason: 'testIssuer2 not found in search results');
// Verify total results - should only show the matching entry
final allVisibleIssuers = find.textContaining('testIssuer');
expect(allVisibleIssuers.evaluate().length, equals(1),
reason: 'Search should show only one result for "issuer2"');
print('✅ Search results verified: only testIssuer2 is visible');
// Clear search bar
final clearIcon = find.byIcon(Icons.clear);
expect(clearIcon, findsOneWidget,
reason: 'Clear search icon not found');
await tester.tap(clearIcon);
await tester.pumpAndSettle();
// Verify both entries are visible again after clearing search
final allIssuer1 = find.textContaining('testIssuer');
final allIssuer2 = find.textContaining('testIssuer2');
expect(allIssuer1, findsAtLeastNWidgets(1),
reason: 'testIssuer not visible after clearing search');
expect(allIssuer2, findsAtLeastNWidgets(1),
reason: 'testIssuer2 not visible after clearing search');
print('✅ Search cleared: both entries visible again');
print('✅ Step 3 completed: Search functionality working correctly');
// Step 10: Long press on issuer2 to edit and add tags
print('🏷️ Testing tag functionality...');
// Long press on testIssuer2 entry to bring up edit menu
final issuer2Entry = find.textContaining('testIssuer2');
expect(issuer2Entry, findsOneWidget,
reason: 'testIssuer2 entry not found for long press');
await tester.longPress(issuer2Entry);
await tester.pumpAndSettle();
LocalAuthenticationService.instance.lastAuthTime = DateTime.now()
.add(const Duration(minutes: 10))
.millisecondsSinceEpoch;
// Look for edit option and tap it
final editOption = find.text('Edit');
expect(editOption, findsOneWidget, reason: 'Edit option not found');
await tester.tap(editOption);
await tester.pumpAndSettle();
// Wait for edit page to load
await tester.pumpAndSettle(const Duration(seconds: 2));
// Look for AddChip widget to add first tag
final addChip = find.byType(AddChip);
expect(addChip, findsOneWidget, reason: 'AddChip widget not found');
await tester.tap(addChip);
await tester.pumpAndSettle();
// Enter first tag name "tag1"
final tagInputField = find.byType(TextField).last;
await tester.tap(tagInputField);
await tester.enterText(tagInputField, 'tag1');
await tester.pumpAndSettle();
// Tap create/save button for first tag
final createButton = find.text('Create');
expect(createButton, findsOneWidget,
reason: 'Create button not found for first tag');
await tester.tap(createButton);
await tester.pumpAndSettle();
// Add second tag
final addChip2 = find.byType(AddChip);
await tester.tap(addChip2);
await tester.pumpAndSettle();
// Enter second tag name "tag2"
final tagInputField2 = find.byType(TextField).last;
await tester.tap(tagInputField2);
await tester.enterText(tagInputField2, 'tag2');
await tester.pumpAndSettle();
// Tap create button for second tag
final createButton2 = find.text('Create');
await tester.tap(createButton2);
await tester.pumpAndSettle();
// Verify tags are selected/visible
final tag1Chip = find.text('tag1');
final tag2Chip = find.text('tag2');
expect(tag1Chip, findsOneWidget,
reason: 'tag1 not found after creation');
expect(tag2Chip, findsOneWidget,
reason: 'tag2 not found after creation');
print('✅ Tags created: tag1 and tag2 are visible');
// Save the edited entry
final saveEditButton = find.text('Save');
expect(saveEditButton, findsOneWidget,
reason: 'Save button not found on edit page');
await tester.tap(saveEditButton);
await tester.pumpAndSettle();
// Wait for navigation back to home
await tester.pumpAndSettle(const Duration(seconds: 2));
print('✅ Entry saved with tags');
// Step 11: Test tag filtering functionality
print('🏷️ Testing tag filtering...');
// Click on tag1 to filter entries
final tag1Filter = find.textContaining('tag1');
if (tag1Filter.evaluate().isNotEmpty) {
await tester.tap(tag1Filter.first);
await tester.pumpAndSettle();
// Verify only testIssuer2 is visible (the one with tag1)
final filteredIssuer2 = find.textContaining('testIssuer2');
final filteredIssuer1 = find.textContaining('testIssuer').evaluate().where(
(element) => !element.widget.toString().contains('testIssuer2')
).length;
expect(filteredIssuer2, findsAtLeastNWidgets(1),
reason: 'testIssuer2 not visible when filtering by tag1');
expect(filteredIssuer1, equals(0),
reason: 'testIssuer should not be visible when filtering by tag1');
print('✅ Tag1 filtering verified: only testIssuer2 is visible');
// Click "All" to clear tag filter
final allFilter = find.text('All');
if (allFilter.evaluate().isNotEmpty) {
await tester.tap(allFilter);
await tester.pumpAndSettle();
// Verify both entries are visible again
final allEntriesIssuer1 = find.textContaining('testIssuer');
final allEntriesIssuer2 = find.textContaining('testIssuer2');
expect(allEntriesIssuer1, findsAtLeastNWidgets(1));
expect(allEntriesIssuer2, findsAtLeastNWidgets(1));
print('✅ Tag filter cleared: both entries visible again');
}
}
print('✅ Step 4 completed: Tag functionality working correctly');
// Step 12: Test trash functionality
print('🗑️ Testing trash functionality...');
// Long press on testIssuer2 entry to bring up context menu
final issuer2EntryForTrash = find.textContaining('testIssuer2').first;
await tester.longPress(issuer2EntryForTrash);
await tester.pumpAndSettle();
// Look for trash/delete option and tap it
final trashOption = find.text('Trash');
if (trashOption.evaluate().isEmpty) {
// Try alternative delete options
final deleteOption = find.text('Delete');
expect(deleteOption, findsOneWidget, reason: 'Delete/Trash option not found');
await tester.tap(deleteOption);
} else {
await tester.tap(trashOption);
}
await tester.pumpAndSettle();
// Confirm deletion if dialog appears
final confirmButtons = [
find.text('Yes'),
find.text('OK'),
find.text('Confirm'),
find.text('Delete'),
find.text('Trash'),
];
for (final button in confirmButtons) {
if (button.evaluate().isNotEmpty) {
await tester.tap(button);
await tester.pumpAndSettle();
break;
}
}
print('✅ Issuer2 entry trashed');
// Step 13: Verify tags are no longer visible and trash tag appears
print('🏷️ Verifying tag visibility after trash...');
// Wait for UI to update
await tester.pumpAndSettle(const Duration(seconds: 1));
// Verify tag1 and tag2 are no longer visible (since issuer2 was the only entry with these tags)
final tag1Visible = find.textContaining('tag1');
final tag2Visible = find.textContaining('tag2');
// Tags should not be visible anymore since the only entry with these tags was trashed
expect(tag1Visible.evaluate().isEmpty, isTrue, reason: 'tag1 should not be visible after trashing issuer2');
expect(tag2Visible.evaluate().isEmpty, isTrue, reason: 'tag2 should not be visible after trashing issuer2');
// Verify trash tag is now visible
final trashTag = find.text('Trash');
expect(trashTag, findsOneWidget, reason: 'Trash tag not visible after trashing entry');
print('✅ Tags hidden and Trash tag visible');
// Step 14: Test trash filtering
print('🗑️ Testing trash tag filtering...');
// Click on Trash tag to show trashed items
await tester.tap(trashTag);
await tester.pumpAndSettle();
// Verify issuer2 is visible in trash
final trashedIssuer2 = find.textContaining('testIssuer2');
expect(trashedIssuer2, findsOneWidget, reason: 'testIssuer2 not visible in trash');
// Verify issuer1 is not visible (should be filtered out)
final issuer1InTrash = find.textContaining('testIssuer').evaluate().where(
(element) => !element.widget.toString().contains('testIssuer2')
).length;
expect(issuer1InTrash, equals(0), reason: 'testIssuer should not be visible in trash filter');
print('✅ Trash filtering working: only trashed items visible');
// Step 15: Test All filter (should not show trashed items)
print('📋 Testing All filter excludes trash...');
// Click on "All" to show all non-trashed items
final allTag = find.text('All');
await tester.tap(allTag);
await tester.pumpAndSettle();
// Verify issuer1 is visible
final allFilterIssuer1 = find.textContaining('testIssuer').evaluate().where(
(element) => !element.widget.toString().contains('testIssuer2')
).length;
expect(allFilterIssuer1, greaterThan(0), reason: 'testIssuer should be visible in All filter');
// Verify issuer2 is NOT visible in All
final allFilterIssuer2 = find.textContaining('testIssuer2');
expect(allFilterIssuer2.evaluate().isEmpty, isTrue, reason: 'testIssuer2 should not be visible in All filter');
print('✅ All filter working: trashed items excluded');
// Step 16: Test restore functionality
print('♻️ Testing restore functionality...');
// Go back to trash view
await tester.tap(trashTag);
await tester.pumpAndSettle();
// Long press on trashed issuer2 entry
final trashedEntryForRestore = find.textContaining('testIssuer2');
await tester.longPress(trashedEntryForRestore);
await tester.pumpAndSettle();
// Look for restore option
final restoreOption = find.text('Restore');
expect(restoreOption, findsOneWidget, reason: 'Restore option not found');
await tester.tap(restoreOption);
await tester.pumpAndSettle();
print('✅ Restore option tapped');
// Step 17: Verify restoration worked
print('✅ Verifying restoration...');
// Wait for restoration to complete
await tester.pumpAndSettle(const Duration(seconds: 1));
// Go to All view to check if issuer2 is restored
await tester.tap(allTag);
await tester.pumpAndSettle();
// Verify both entries are now visible in All
final restoredIssuer1 = find.textContaining('testIssuer');
final restoredIssuer2 = find.textContaining('testIssuer2');
expect(restoredIssuer1, findsAtLeastNWidgets(1), reason: 'testIssuer not visible after restore');
expect(restoredIssuer2, findsAtLeastNWidgets(1), reason: 'testIssuer2 not visible after restore');
// Verify tags are visible again
final restoredTag1 = find.textContaining('tag1');
final restoredTag2 = find.textContaining('tag2');
if (restoredTag1.evaluate().isNotEmpty && restoredTag2.evaluate().isNotEmpty) {
print('✅ Tags restored and visible again');
}
print('✅ Step 5 completed: Trash and restore functionality working correctly');
print('✅ Integration test completed successfully!');
print('- Both entries created and verified');
print('- Search functionality tested and working');
print('- Tag functionality tested and working');
print('- Trash functionality tested and working');
print('- Restore functionality tested and working');
print('- Multiple TOTP codes are being generated');
print('- Data persistence is working correctly');
},
timeout: const Timeout(Duration(minutes: 3)),
);
});
}

View File

@@ -68,6 +68,8 @@ PODS:
- Flutter
- fluttertoast (0.0.2):
- Flutter
- integration_test (0.0.1):
- Flutter
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
@@ -150,6 +152,7 @@ DEPENDENCIES:
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- move_to_background (from `.symlinks/plugins/move_to_background/ios`)
- objective_c (from `.symlinks/plugins/objective_c/ios`)
@@ -210,6 +213,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
move_to_background:
@@ -260,6 +265,7 @@ SPEC CHECKSUMS:
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
local_auth_darwin: fa4b06454df7df8e97c18d7ee55151c57e7af0de
move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb

View File

@@ -111,6 +111,7 @@
"importAegisGuide": "Použijte možnost \"Export the vault\" v nastavení aplikace Aegis.",
"import2FasGuide": "Použijte možnost \"Settings->Backup -Export\" v 2FA.\n\nPokud je Vaše záloha šifrovaná, budete muset zadat heslo pro její odemčení",
"importLastpassGuide": "V nastavení aplikace Lastpass Authenticator vyberte možnost \"Transfer accounts\" a poté \"Export accounts to file\". Vygenerovaný soubor JSON následně nahrajte sem.",
"importProtonAuthGuide": "K exportu kódů použijte možnost „Exportovat“ v nastavení aplikace Proton Authenticator.",
"exportCodes": "Exportovat kódy",
"importLabel": "Importovat",
"importInstruction": "Vyberte, prosím, soubor obsahující seznam Vašich kódů v následujícím formátu",
@@ -124,6 +125,7 @@
"authToChangeYourEmail": "Pro změnu svého e-mailu se, prosím, ověřte",
"authToChangeYourPassword": "Pro změnu svého hesla se, prosím, ověřte",
"authToViewSecrets": "Pro zobrazení svých tajných údajů se musíte ověřit",
"authToInitiateSignIn": "Proveďte ověření a přihlaste se k zálohování.",
"ok": "Ok",
"cancel": "Zrušit",
"yes": "Ano",
@@ -171,6 +173,7 @@
"invalidQRCode": "Neplatný QR kód",
"noRecoveryKeyTitle": "Nemáte obnovovací klíč?",
"enterEmailHint": "Zadejte svou e-mailovou adresu",
"enterNewEmailHint": "Zadejte svou novou e-mailovou adresu",
"invalidEmailTitle": "Neplatná e-mailová adresa",
"invalidEmailMessage": "Prosím, zadejte platnou e-mailovou adresu.",
"deleteAccount": "Odstranit účet",
@@ -509,6 +512,19 @@
"supportEnte": "Podpořte <bold-green>ente</bold-green>",
"giveUsAStarOnGithub": "Dejte nám hvězdu na Githubu",
"free5GB": "5GB zdarma na <bold-green>ente</bold-green> Fotky",
"loginWithAuthAccount": "Přihlaste se pomocí svého účtu Auth",
"freeStorageOffer": "10% sleva na <bold-green>ente</bold-green> fotky",
"freeStorageOfferDescription": "Použijte kód \"AUTH\" pro získání 10% slevy na první rok"
"freeStorageOfferDescription": "Použijte kód \"AUTH\" pro získání 10% slevy na první rok",
"advanced": "Pokročilé",
"algorithm": "Algoritmus",
"type": "Typ",
"period": "Období",
"digits": "Digitální",
"importFromGallery": "Importovat z galerie",
"errorCouldNotReadImage": "Nelze přečíst vybraný obrazový soubor.",
"errorInvalidQRCode": "Neplatný QR kód",
"errorInvalidQRCodeBody": "Naskenovaný QR kód není platným účtem 2FA.",
"errorNoQRCode": "Nenalezen žádný QR kód",
"errorGenericTitle": "Došlo k chybě",
"errorGenericBody": "Při importu došlo k neočekávané chybě."
}

View File

@@ -111,6 +111,7 @@
"importAegisGuide": "Verwenden Sie die Option \"Tresor exportieren\" in den Einstellungen von Aegis.\n\nFalls Ihr Tresor verschlüsselt ist, müssen Sie das Passwort für den Tresor eingeben, um ihn zu entschlüsseln.",
"import2FasGuide": "Verwenden Sie unter \"Einstellungen → Backup\" die Option \"Exportieren\" in 2FAS.\n\nFalls Ihr Backup verschlüsselt ist, müssen Sie das Passwort eingeben, um das Backup zu entschlüsseln.",
"importLastpassGuide": "Verwenden Sie die Option \"Konten übertragen → Konten in Datei exportieren\" in den Lastpass Authenticator Einstellungen. \nImportieren Sie anschließend die heruntergeladene JSON-Datei.",
"importProtonAuthGuide": "Verwenden Sie die Option \"Exportieren\" in den Proton Authenticator Settings um Ihre Codes zu exportieren.",
"exportCodes": "Codes exportieren",
"importLabel": "Importieren",
"importInstruction": "Bitte wählen Sie eine Datei die Codes in folgendem Format beinhaltet",
@@ -519,5 +520,12 @@
"algorithm": "Algorithmus",
"type": "Typ",
"period": "Periode",
"digits": "Ziffern"
"digits": "Ziffern",
"importFromGallery": "Aus Galerie importieren",
"errorCouldNotReadImage": "Die ausgewählte Bild-Datei konnte nicht verarbeitet werden.",
"errorInvalidQRCode": "Ungültiger QR-Code",
"errorInvalidQRCodeBody": "Der gescannte QR-Code ist kein gültiges 2FA-Konto.",
"errorNoQRCode": "Kein QR-Code gefunden",
"errorGenericTitle": "Ein Fehler ist aufgetreten",
"errorGenericBody": "Beim Importieren ist ein unerwarteter Fehler aufgetreten."
}

View File

@@ -111,6 +111,7 @@
"importAegisGuide": "Utilisez l'option \"Exporter le coffre-fort\" dans les paramètres d'Aegis.\n\nSi votre coffre-fort est crypté, vous devrez saisir le mot de passe du coffre-fort pour déchiffrer le coffre-fort.",
"import2FasGuide": "Utilisez l'option \"Paramètres->Sauvegarde -Export\" dans 2FAS.\n\nSi votre sauvegarde est chiffrée, vous devrez entrer le mot de passe pour déchiffrer la sauvegarde",
"importLastpassGuide": "Utilisez l'option \"Transférer des comptes\" dans les paramètres de l'authentificateur Lastpass et appuyez sur \"Exporter des comptes vers un fichier\". Importez le JSON téléchargé.",
"importProtonAuthGuide": "Utilisez l'option \"Export\" dans les paramètres de Proton Authenticator pour exporter vos codes.",
"exportCodes": "Exporter les codes",
"importLabel": "Importer",
"importInstruction": "Veuillez sélectionner un fichier qui contient une liste de vos codes dans le format suivant",
@@ -519,5 +520,12 @@
"algorithm": "Algorithme",
"type": "Type",
"period": "Période",
"digits": "Chiffres"
"digits": "Chiffres",
"importFromGallery": "Importer depuis la galerie",
"errorCouldNotReadImage": "Impossible de lire le fichier sélectionné.",
"errorInvalidQRCode": "QR Code invalide",
"errorInvalidQRCodeBody": "Le code QR scanné n'est pas un compte 2FA valide.",
"errorNoQRCode": "Aucun code QR trouvé",
"errorGenericTitle": "Une erreur s'est produite",
"errorGenericBody": "Une erreur inattendue est survenue lors de l'importation."
}

View File

@@ -111,6 +111,7 @@
"importAegisGuide": "Naudokite „Aegis“ nustatymuose esančią parinktį Eksportuoti slėptuvę.\n\nJei jūsų saugykla užšifruota, turėsite įvesti saugyklos slaptažodį, kad iššifruotumėte saugyklą.",
"import2FasGuide": "Naudokite programoje 2FAS esančią parinktį „Settings->2FAS Backup->Export to file“.\n\nJei atsarginė kopija užšifruota, turėsite įvesti slaptažodį, kad iššifruotumėte atsarginę kopiją.",
"importLastpassGuide": "Naudokite „Lastpass Authenticator“ nustatymuose esančią parinktį „Transfer accounts“ (perkelti paskyras) ir paspauskite „Export accounts to file“ (eksportuoti paskyras į failą). Importuokite atsisiųstą JSON failą.",
"importProtonAuthGuide": "Naudokite „Proton Authenticator“ nustatymuose esančią parinktį „Export“ (eksportuoti), kad eksportuotumėte savo kodus.",
"exportCodes": "Eksportuoti kodus",
"importLabel": "Importuoti",
"importInstruction": "Pasirinkite failą, kuriame yra tokio formato jūsų kodų sąrašas",
@@ -519,5 +520,12 @@
"algorithm": "Algoritmas",
"type": "Tipas",
"period": "Laikotarpis",
"digits": "Skaitmenys"
"digits": "Skaitmenys",
"importFromGallery": "Importuoti iš galerijos",
"errorCouldNotReadImage": "Nepavyko perskaityti pasirinkto vaizdo failo.",
"errorInvalidQRCode": "Netinkamas QR kodas",
"errorInvalidQRCodeBody": "Nuskenuotas QR kodas nėra tinkama 2FA paskyra.",
"errorNoQRCode": "QR kodas nerastas.",
"errorGenericTitle": "Įvyko klaida",
"errorGenericBody": "Importuojant įvyko netikėta klaida."
}

View File

@@ -111,6 +111,7 @@
"importAegisGuide": "Użyj opcji \"Eksportuj sejf\" w ustawieniach Aegis.\n\nJeśli twój sejf jest zaszyfrowany, musisz wprowadzić hasło sejfu, aby odszyfrować sejf.",
"import2FasGuide": "Użyj opcji \"Ustawienia->Kopia Zapasowa-Eksport\" w 2FAS.\n\nJeśli twoja kopia zapasowa jest zaszyfrowana, musisz wprowadzić hasło, aby odszyfrować kopię zapasową",
"importLastpassGuide": "Użyj opcji \"Przenieś konta\" w Ustawieniach Lastpass Authenticator i naciśnij \"Eksportuj konta do pliku\". Zaimportuj pobrany plik JSON.",
"importProtonAuthGuide": "Użyj opcji „Eksportuj” w ustawieniach Proton Authenticator, aby wyeksportować kody.",
"exportCodes": "Eksportuj kody",
"importLabel": "Importuj",
"importInstruction": "Wybierz plik, który zawiera listę twoich kodów w następującym formacie",
@@ -519,5 +520,12 @@
"algorithm": "Algorytm",
"type": "Rodzaj",
"period": "Okres",
"digits": "Cyfry"
"digits": "Cyfry",
"importFromGallery": "Importuj z galerii",
"errorCouldNotReadImage": "Nie można odczytać wybranego pliku obrazu.",
"errorInvalidQRCode": "Nieprawidłowy kod QR",
"errorInvalidQRCodeBody": "Zeskanowany kod QR nie wskazuje na prawidłowe konto 2FA.",
"errorNoQRCode": "Nie znaleziono kodu QR",
"errorGenericTitle": "Wystąpił błąd",
"errorGenericBody": "Podczas importowania wystąpił nieoczekiwany błąd."
}

View File

@@ -18,7 +18,7 @@
"incorrectDetails": "Felaktiga uppgifter",
"pleaseVerifyDetails": "Kontrollera dina detaljer och försök igen",
"codeIssuerHint": "Utfärdare",
"codeSecretKeyHint": "Secret Key",
"codeSecretKeyHint": "Hemlig nyckel",
"secret": "Säkerhetsnyckel",
"all": "Alla",
"notes": "Anteckningar",
@@ -33,7 +33,7 @@
}
}
},
"codeAccountHint": "Konto (du@domän.com)",
"codeAccountHint": "Konto (du@domain.com)",
"codeTagHint": "Tagg",
"accountKeyType": "Typ av nyckel",
"sessionExpired": "Sessionen har gått ut",
@@ -68,7 +68,7 @@
"reportABug": "Rapportera en bugg",
"crashAndErrorReporting": "Krasch och felrapportering",
"reportBug": "Rapportera bugg",
"emailUsMessage": "Skicka e-mail till {email}",
"emailUsMessage": "Skicka e-post till {email}",
"@emailUsMessage": {
"placeholders": {
"email": {
@@ -79,7 +79,7 @@
"contactSupport": "Kontakta support",
"rateUsOnStore": "Betygsätt på {storeName}",
"blog": "Blogg",
"merchandise": "Merchandise",
"merchandise": "Produkter",
"verifyPassword": "Bekräfta lösenord",
"pleaseWait": "Vänligen vänta...",
"generatingEncryptionKeysTitle": "Skapar krypteringsnycklar...",
@@ -104,13 +104,14 @@
"importFromApp": "Importera koder från {appName}",
"importGoogleAuthGuide": "Exportera dina konton från Google Authenticator till en QR-kod med alternativet \"Överföra konton\". Använd sedan en annan enhet och skanna QR-koden.\n\nTips: Du kan använda din bärbara dators webbkamera för att ta en bild av QR-koden.",
"importSelectJsonFile": "Välj JSON-fil",
"importSelectAppExport": "Välj {appName} exportfil",
"importSelectAppExport": "Välj {appName} exporteringsfil",
"importEnteEncGuide": "Välj den krypterade JSON-filen som exporteras från Ente",
"importRaivoGuide": "Använd alternativet \"Exportera OTPs till zip-arkiv\" i Raivos inställningar.\n\nExtrahera zip-filen och importera JSON-filen.",
"importBitwardenGuide": "Använd alternativet \"Exportera valv\" inom Bitwarden Tools och importera den okrypterade JSON-filen.",
"importAegisGuide": "Använd alternativet \"Exportera valvet\" i Aegis inställningar.\n\nOm ditt valv är krypterat måste du ange valvlösenordet för att dekryptera valvet.",
"import2FasGuide": "Använd alternativet \"Inställningar->Säkerhetskopiera -Exportera\" i 2FAS.\n\nOm din säkerhetskopia är krypterad måste du ange lösenordet för att dekryptera säkerhetskopian.",
"importLastpassGuide": "Använd alternativet \"Överför konton\" i LastPass Authenticators inställningar och tryck på \"Exportera konton till fil\". Importera JSON-filen som laddas ner.",
"importProtonAuthGuide": "Använd alternativet \"Exportera\" i Proton Authenticator-inställningarna för att exportera koder.",
"exportCodes": "Exportera koder",
"importLabel": "Importera",
"importInstruction": "Vänligen välj en fil som innehåller en lista över dina koder i följande format",
@@ -119,11 +120,11 @@
"emailVerificationToggle": "E-postverifiering",
"emailVerificationEnableWarning": "För att undvika att bli låst från ditt konto, se till att spara en kopia av din e-post 2FA utanför Ente Auth innan du aktiverar e-postverifiering.",
"authToChangeEmailVerificationSetting": "Autentisera för att ändra din e-postadress",
"authenticateGeneric": "Var god autentisera",
"authenticateGeneric": "Vänligen autentisera",
"authToViewYourRecoveryKey": "Autentisera för att visa din återställningsnyckel",
"authToChangeYourEmail": "Autentisera för att ändra din e-postadress",
"authToChangeYourPassword": "Autentisera för att ändra ditt lösenord",
"authToViewSecrets": "Autentisera för att visa din återställningsnyckel",
"authToViewSecrets": "Vänligen autentisera för att visa din återställningsnyckel",
"authToInitiateSignIn": "Vänligen autentisera för att initiera inloggning för säkerhetskopiering.",
"ok": "OK",
"cancel": "Avbryt",
@@ -147,7 +148,7 @@
"leaveFamily": "Lämna familjen",
"leaveFamilyMessage": "Är du säker på att du vill lämna familjeplanen?",
"inFamilyPlanMessage": "Du är på en familjeplan!",
"hintForMobile": "Håll i på en kod för att redigera eller ta bort.",
"hintForMobile": "Tryck länge på en kod för att redigera eller ta bort.",
"hintForDesktop": "Högerklicka på en kod för att redigera eller ta bort.",
"scan": "Skanna",
"scanACode": "Skanna kod",
@@ -191,7 +192,7 @@
"oopsSomethingWentWrong": "Hoppsan! Något gick fel.",
"selectLanguage": "Välj språk",
"language": "Språk",
"social": "Social",
"social": "Socialt",
"security": "Säkerhet",
"lockscreen": "Låsskärm",
"authToChangeLockscreenSetting": "Vänligen autentisera för att ändra låsskärms inställningar",
@@ -200,7 +201,7 @@
"authToViewYourActiveSessions": "Autentisera för att visa dina aktiva sessioner",
"searchHint": "Sök...",
"search": "Sök",
"sorryUnableToGenCode": "Tyvärr, det gick inte att generera en kod för {issuerName}",
"sorryUnableToGenCode": "Tyvärr, kunde inte generera en kod för {issuerName}",
"noResult": "Inga resultat",
"addCode": "Lägg till kod",
"scanAQrCode": "Skanna en QR-kod",
@@ -215,7 +216,7 @@
"error": "Fel",
"recoveryKeyCopiedToClipboard": "Återställningsnyckel kopierad till urklipp",
"recoveryKeyOnForgotPassword": "Om du glömmer ditt lösenord är det enda sättet du kan återställa dina data med denna nyckel.",
"recoveryKeySaveDescription": "Vi lagrar inte och har därför inte åtkomst till denna nyckel, vänligen spara denna 24 ords nyckel på en säker plats.",
"recoveryKeySaveDescription": "Vi lagrar inte och har därför inte åtkomst till denna nyckel, vänligen spara denna 24 ordsnyckeln på en säker plats.",
"doThisLater": "Gör detta senare",
"saveKey": "Spara nyckel",
"save": "Spara",
@@ -254,7 +255,7 @@
"insecureDevice": "Osäker enhet",
"sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Tyvärr, kunde vi inte generera säkra nycklar på den här enheten.\n\nvänligen registrera dig från en annan enhet.",
"howItWorks": "Så här fungerar det",
"ackPasswordLostWarning": "Jag förstår att om jag förlorar mitt lösenord kan jag förlora mina data eftersom min data är <underline>end-to-end-krypterad</underline>.",
"ackPasswordLostWarning": "Jag förstår att om jag förlorar mitt lösenord kan jag förlora mina data eftersom min data är <underline>totalsträckskrypterad</underline>.",
"loginTerms": "Jag samtycker till <u-terms>användarvillkoren</u-terms> och <u-policy>integritetspolicyn</u-policy>",
"logInLabel": "Logga in",
"logout": "Logga ut",
@@ -278,7 +279,7 @@
"recoveryKeyVerifyReason": "Din återställningsnyckel är det enda sättet att återställa dina foton om du glömmer ditt lösenord. Du hittar din återställningsnyckel i Inställningar > Säkerhet.\n\nAnge din återställningsnyckel här för att verifiera att du har sparat den ordentligt.",
"confirmYourRecoveryKey": "Bekräfta din återställningsnyckel",
"confirm": "Bekräfta",
"emailYourLogs": "Maila dina loggar",
"emailYourLogs": "E-posta dina loggar",
"pleaseSendTheLogsTo": "Vänligen skicka loggarna till \n{toEmail}",
"copyEmailAddress": "Kopiera e-postadress",
"exportLogs": "Exportera loggar",
@@ -297,7 +298,7 @@
"criticalUpdateAvailable": "Kritisk uppdatering tillgänglig",
"updateAvailable": "Uppdatering tillgänglig",
"update": "Uppdatera",
"checking": "Kontrollerar ...",
"checking": "Kontrollerar...",
"youAreOnTheLatestVersion": "Du är på den senaste versionen",
"warning": "Varning",
"exportWarningDesc": "Den exporterade filen innehåller känslig information. Förvara den på ett säkert sätt.",
@@ -306,7 +307,7 @@
"description": "Text for the button to confirm the user understands the warning"
},
"authToExportCodes": "Autentisera för att exportera dina koder",
"importSuccessTitle": "Jippi!",
"importSuccessTitle": "Hurra!",
"importSuccessDesc": "Du har importerat {count} koder!",
"@importSuccessDesc": {
"placeholders": {
@@ -324,7 +325,7 @@
"checkInboxAndSpamFolder": "Vänligen kontrollera din inkorg (och skräppost) för att slutföra verifieringen",
"tapToEnterCode": "Tryck för att ange kod",
"resendEmail": "Skicka e-post igen",
"weHaveSendEmailTo": "Vi har skickat ett mail till <green>{email}</green>",
"weHaveSendEmailTo": "Vi har skickat ett e-postmeddelande till <green>{email}</green>",
"@weHaveSendEmailTo": {
"description": "Text to indicate that we have sent a mail to the user",
"placeholders": {
@@ -362,7 +363,7 @@
"selectExportFormat": "Välj exportformat",
"exportDialogDesc": "Krypterad export skyddas av ett lösenord som du väljer.",
"encrypted": "Krypterad",
"plainText": "Enkel text",
"plainText": "Oformaterad text",
"passwordToEncryptExport": "Lösenord för att kryptera export",
"export": "Exportera",
"useOffline": "Använd utan säkerhetskopior",
@@ -374,14 +375,14 @@
"compactMode": "Kompakt läge",
"shouldHideCode": "Dölj koder",
"doubleTapToViewHiddenCode": "Du kan dubbeltrycka på en post för att visa koden",
"focusOnSearchBar": "Fokusera på sök vid appstart",
"focusOnSearchBar": "Fokusera på sök vid uppstart av app",
"confirmUpdatingkey": "Är du säker på att du vill uppdatera den hemliga nyckeln?",
"minimizeAppOnCopy": "Minimera appen vid kopiering",
"editCodeAuthMessage": "Autentisera för att redigera kod",
"deleteCodeAuthMessage": "Autentisera för att radera kod",
"showQRAuthMessage": "Autentisera för att visa QR-kod",
"confirmAccountDeleteTitle": "Bekräfta radering av kontot",
"confirmAccountDeleteMessage": "Detta konto är kopplat till andra Ente apps, om du använder någon.\n\nDina uppladdade data, över alla Ente appar, kommer att schemaläggas för radering och ditt konto kommer att raderas permanent.",
"confirmAccountDeleteMessage": "Detta konto är kopplat till andra Ente applikationer, om du använder någon.\n\nDina uppladdade data, över alla Ente applikationer, kommer att schemaläggas för radering och ditt konto kommer att raderas permanent.",
"androidBiometricHint": "Verifiera identitet",
"@androidBiometricHint": {
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
@@ -390,7 +391,7 @@
"@androidBiometricNotRecognized": {
"description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
},
"androidBiometricSuccess": "Slutförd",
"androidBiometricSuccess": "Lyckades",
"@androidBiometricSuccess": {
"description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters."
},
@@ -441,7 +442,7 @@
"signOutOtherDevices": "Logga ut andra enheter",
"doNotSignOut": "Logga inte ut",
"hearUsWhereTitle": "Hur hörde du talas om Ente? (valfritt)",
"hearUsExplanation": "Vi spårar inte appinstallationer, Det skulle hjälpa oss om du berättade var du hittade oss!",
"hearUsExplanation": "Vi spårar inte installationer. Det skulle hjälpa oss om du berättade hur du hittade oss!",
"recoveryKeySaved": "Återställningsnyckel sparad i nedladdningsmappen!",
"waitingForBrowserRequest": "Väntar på webbläsarbegäran...",
"waitingForVerification": "Väntar på verifiering...",
@@ -488,6 +489,8 @@
"hideContent": "Dölj innehåll",
"hideContentDescriptionAndroid": "Döljer appinnehåll i app-växlaren och inaktiverar skärmdumpar",
"hideContentDescriptioniOS": "Döljer appinnehåll i app-växlaren",
"autoLockFeatureDescription": "Tid efter vilken appen låses efter att ha satts i bakgrunden",
"appLockDescription": "Välj mellan enhetens förvalda låsskärm och en anpassad låsskärm med en PIN-kod eller lösenord.",
"pinLock": "Pinkodslås",
"enterPin": "Ange PIN-kod",
"setNewPin": "Ställ in ny PIN-kod",
@@ -498,9 +501,31 @@
"appLockOfflineModeWarning": "Du har valt att fortsätta utan säkerhetskopior. Om du glömmer ditt applås, kommer du att bli utelåst från att komma åt dina data.",
"duplicateCodes": "Dubblettkoder",
"noDuplicates": "✨ Inga dubbletter",
"youveNoDuplicateCodesThatCanBeCleared": "Du har inga dubbla koder som kan rensas",
"deduplicateCodes": "Deduplicera koder",
"deselectAll": "Avmarkera alla",
"selectAll": "Markera alla",
"deleteDuplicates": "Radera dubbletter",
"plainHTML": "Ren HTML"
"plainHTML": "Ren HTML",
"tellUsWhatYouThink": "Berätta vad du tycker",
"dropReviewiOS": "Skriv en recension på App Store",
"dropReviewAndroid": "Skriv en recension på Play Store",
"supportEnte": "Stöd <bold-green>ente</bold-green>",
"giveUsAStarOnGithub": "Ge oss en stjärna på Github",
"free5GB": "5 GB gratis på <bold-green>ente</bold-green> Foton",
"loginWithAuthAccount": "Logga in med ditt Auth-konto",
"freeStorageOffer": "10% rabatt på <bold-green>ente</bold-green> foton",
"freeStorageOfferDescription": "Använd koden \"AUTH\" för att få 10% rabatt första året",
"advanced": "Avancerad",
"algorithm": "Algoritm",
"type": "Typ",
"period": "Tidsperiod",
"digits": "Siffror",
"importFromGallery": "Importera från galleri",
"errorCouldNotReadImage": "Kunde inte läsa den valda bildfilen.",
"errorInvalidQRCode": "Ogiltig QR-kod",
"errorInvalidQRCodeBody": "Den skannade QR-koden är inte ett giltigt 2FA konto.",
"errorNoQRCode": "Ingen QR-kod hittades",
"errorGenericTitle": "Ett fel inträffade",
"errorGenericBody": "Ett oväntat fel inträffade vid import."
}

View File

@@ -6,7 +6,7 @@
"@counterAppBarTitle": {
"description": "Text shown in the AppBar of the Counter Page"
},
"onBoardingBody": "妥善保管您的两步验证码",
"onBoardingBody": "妥善保管您的双重认证代码",
"onBoardingGetStarted": "开始",
"setupFirstAccount": "设置您的第一个账户",
"importScanQrCode": "扫描二维码",
@@ -111,13 +111,14 @@
"importAegisGuide": "使用 Aegis 设置中的“导出密码库”选项。\n\n如果您的密码库已加密则需要输入密码库密码才能解密密码库。",
"import2FasGuide": "使用 2FAS 中的“设置 -> 备份 -> 导出”选项。\n\n如果您的备份已加密则需要输入密码来解密备份",
"importLastpassGuide": "使用 Lastpass Authenticator 设置中的“转移账户”选项,然后按“将账户导出到文件”。导入下载的 JSON。",
"importProtonAuthGuide": "使用 Proton Authenticator 设置中的“导出”选项导出您的代码。",
"exportCodes": "导出代码",
"importLabel": "导入",
"importInstruction": "请选择一个包含以下格式的代码列表的文件",
"importCodeDelimiterInfo": "代码可以用逗号或换行符分隔",
"selectFile": "选择文件",
"emailVerificationToggle": "电子邮件验证",
"emailVerificationEnableWarning": "为避免被锁在您的账户之外,请在启用电子邮件验证之前确保在 Ente Auth 之外存储电子邮件两步验证的副本。",
"emailVerificationEnableWarning": "为避免被锁在您的账户之外,请在启用电子邮件验证之前确保在 Ente Auth 之外存储电子邮件双重认证的副本。",
"authToChangeEmailVerificationSetting": "请进行身份验证以更改电子邮件验证",
"authenticateGeneric": "请验证",
"authToViewYourRecoveryKey": "请验证以查看您的恢复密钥",
@@ -155,7 +156,7 @@
"verifyEmail": "验证电子邮件",
"enterCodeHint": "从你的身份验证器应用中\n输入6位数字代码",
"lostDeviceTitle": "丢失了设备吗?",
"twoFactorAuthTitle": "两步验证",
"twoFactorAuthTitle": "双重认证",
"passkeyAuthTitle": "通行密钥验证",
"verifyPasskey": "验证通行密钥",
"loginWithTOTP": "使用 TOTP 登录",
@@ -519,5 +520,12 @@
"algorithm": "算法",
"type": "类型",
"period": "周期",
"digits": "数字"
"digits": "数字",
"importFromGallery": "从图库导入",
"errorCouldNotReadImage": "无法读取所选图片文件。",
"errorInvalidQRCode": "二维码无效",
"errorInvalidQRCodeBody": "扫描的二维码不是有效的双重认证账户。",
"errorNoQRCode": "未找到二维码",
"errorGenericTitle": "出错了",
"errorGenericBody": "导入时发生意外错误。"
}

View File

@@ -99,7 +99,7 @@ Future<void> _runInForeground() async {
return await _runWithLogs(() async {
_logger.info("Starting app in foreground");
try {
await _init(false, via: 'mainMethod');
await init(false, via: 'mainMethod');
} catch (e, s) {
_logger.severe("Failed to init", e, s);
rethrow;
@@ -160,7 +160,7 @@ void _registerWindowsProtocol() {
}
}
Future<void> _init(bool bool, {String? via}) async {
Future<void> init(bool bool, {String? via}) async {
_registerWindowsProtocol();
await CryptoUtil.init();

View File

@@ -605,6 +605,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
flutter_driver:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_email_sender:
dependency: "direct main"
description:
@@ -853,6 +858,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
@@ -965,6 +975,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.4"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
intl:
dependency: "direct main"
description:
@@ -1358,6 +1373,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.0.8"
process:
dependency: transitive
description:
name: process
sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
protobuf:
dependency: "direct main"
description:
@@ -1740,6 +1763,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.1.0"
sync_http:
dependency: transitive
description:
name: sync_http
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
synchronized:
dependency: transitive
description:
@@ -1972,6 +2003,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webdriver:
dependency: transitive
description:
name: webdriver
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
win32:
dependency: "direct main"
description:

View File

@@ -138,6 +138,8 @@ dev_dependencies:
build_runner: ^2.1.11
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
json_serializable: ^6.2.0
lints: ^5.1.1
mocktail: ^1.0.3

View File

@@ -47,4 +47,9 @@ android/app/build/
# FVM Version Cache
.fvm/
lib/generated/intl/app_localizations*.dart
lib/generated/intl/app_localizations*.dart
# Generated rust bindings
lib/src/rust/*
rust/src/frb_generated*
test/**/*.mocks.dart

View File

@@ -48,7 +48,7 @@ You can alternatively install the build from PlayStore or F-Droid.
1. Install [Flutter v3.32.8](https://flutter.dev/docs/get-started/install) and [Rust](https://www.rust-lang.org/tools/install).
2. Install [Flutter Rust Bridge](https://cjycode.com/flutter_rust_bridge/) with `cargo install flutter_rust_bridge_codegen`
2. Install [Flutter Rust Bridge](https://cjycode.com/flutter_rust_bridge/) with `cargo install flutter_rust_bridge_codegen` and run `flutter_rust_bridge_codegen generate` to generate the Rust bindings.
3. Pull in all submodules with `git submodule update --init --recursive`

View File

@@ -73,3 +73,6 @@ analyzer:
- thirdparty/**
- lib/generated/**
- rust_builder/**
- lib/src/rust/**
- rust/src/**
- test/**

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -1,36 +1,36 @@
ente is a simple app to backup and share your photos and videos.
ente je jednoduchá aplikace pro automatické zálohování a organizaci vašich fotek a videí.
If you've been looking for a privacy-friendly alternative to Google Photos, you've come to the right place. With ente, they are stored end-to-end encrypted (e2ee). This means that only you can view them.
Pokud hledáte alternativu ke službě Google Photos, která respektuje vaše soukromí, jste na správném místě. S Ente jsou uloženy s koncovým "end-to-end" šifrováním (e2ee). To znamená, že je můžete vidět pouze vy.
We have open-source apps across Android, iOS, web and desktop, and your photos will seamlessly sync between all of them in an end-to-end encrypted (e2ee) manner.
Máme aplikace napříč všemi platformami a vaše fotky se budou bezproblémově synchronizovat mezi všemi vašimi zařízeními tak, aby byly koncově šifrovány mezi jednotlivými zařízeními (e2ee).
ente also makes it simple to share your albums with your loved ones, even if they aren't on ente. You can share publicly viewable links, where they can view your album and collaborate by adding photos to it, even without an account or app.
ente také usnadňuje sdílení vašich alb s vašimi blízkými, i když nejsou na ente. Můžete sdílet veřejně přístupné odkazy, kde si mohou prohlížet vaše album a spolupracovat přidáváním fotografií, a to i bez účtu nebo aplikace.
Your encrypted data is replicated to 3 different locations, including a fall-out shelter in Paris. We take posterity seriously and make it easy to ensure that your memories outlive you.
Vaše šifrovaná data jsou uložena na 3 místech, včetně protiatomového krytu v Paříži. Bereme budoucnost vážně a usnadňujeme vám zajistit, aby vaše vzpomínky přežily vás samotné.
We are here to make the safest photos app ever, come join our journey!
Jsme tu, abychom vytvořili nejbezpečnější aplikaci pro fotografie, jaká kdy byla. Přidejte se k nám!
FEATURES
- Original quality backups, because every pixel is important
- Family plans, so you can share storage with your family
- Collaborative albums, so you can pool together photos after a trip
- Shared folders, in case you want your partner to enjoy your "Camera" clicks
- Album links, that can be protected with a password
- Ability to free up space, by removing files that have been safely backed up
- Human support, because you're worth it
- Descriptions, so you can caption your memories and find them easily
- Image editor, to add finishing touches
- Favorite, hide and relive your memories, for they are precious
- One-click import from Google, Apple, your hard drive and more
- Dark theme, because your photos look good in it
- 2FA, 3FA, biometric auth
- and a LOT more!
VLASTNOSTI
- Zálohování v původní kvalitě, protože každý pixel je důležitý
- Rodinné plány, takže můžete sdílet úložiště s rodinou
- Společná alba, do kterých můžete po cestě shromažďovat fotografie
- Sdílené složky, aby i váš partner mohl obdivovat vaše fotografie
- Odkazy na alba, které lze chránit heslem a nastavit jejich platnost
- Možnost uvolnit místo odstraněním souborů, které byly bezpečně zálohovány
- Lidská podpora, protože si to zasloužíte
- Popisy, abyste mohli své vzpomínky opatřit popisky a snadno je najít
- Editor obrázků pro doladění detailů
- Označte si své oblíbené fotografie, skryjte je a nebo je prožijte znovu pomocí vzpomínek
- Import jedním kliknutím z Google, Apple, pevného disku a dalších zdrojů
- Tmavý motiv, protože vaše fotografie v něm vypadají dobře
- 2FA, 3FA, biometrické ověření
- a ještě MNOHEM víc!
OPRÁVNĚNÍ
ente requests for certain permissions to serve the purpose of a photo storage provider, which can be reviewed here: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md
ente žádá o určitá oprávnění, aby mohla plnit funkci poskytovatele úložiště fotografií. Tyto oprávnění si můžete prohlédnout zde: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md
PRICING
We don't offer forever free plans, because it is important to us that we remain sustainable and withstand the test of time. Instead we offer affordable plans that you can freely share with your family. You can find more information at ente.io.
CENY
Nenabízíme doživotní tarify, protože je pro nás důležité zůstat udržitelnými a obstát ve zkoušce času. Místo toho nabízíme cenově dostupné plány, které můžete svobodně sdílet se svou rodinou. Více informací najdete na ente.io.
PODPORA
We take pride in offering human support. If you are our paid customer, you can reach out to team@ente.io and expect a response from our team within 24 hours.
Jsme hrdí na to, že poskytujeme lidskou podporu. Pokud máte placený tarif, můžete se obrátit na team@ente.io a do 24 hodin od našeho týmu očekávat odpověď.

View File

@@ -1 +1 @@
ente is an end-to-end encrypted photo storage app
ente je aplikace pro ukládání fotografií a videí s koncovým šifrováním

View File

@@ -1 +1 @@
ente - encrypted photo storage
ente šifrované úložiště fotografií

View File

@@ -1,33 +1,33 @@
Ente is a simple app to automatically backup and organize your photos and videos.
Ente je jednoduchá aplikace pro automatické zálohování a organizaci vašich fotek a videí.
If you've been looking for a privacy-friendly alternative to preserve your memories, you've come to the right place. With Ente, they are stored end-to-end encrypted (e2ee). This means that only you can view them.
Pokud hledáte alternativu, která respektuje vaše soukromí a umožní vám uchovat vaše vzpomínky, jste na správném místě. S Ente jsou uloženy s koncovým "end-to-end" šifrováním (e2ee). To znamená, že je můžete vidět pouze vy.
We have apps across all platforms, and your photos will seamlessly sync between all your devices in an end-to-end encrypted (e2ee) manner.
Máme aplikace napříč všemi platformami a vaše fotky se budou bezproblémově synchronizovat mezi všemi vašimi zařízeními tak, aby byly koncově šifrovány mezi jednotlivými zařízeními (e2ee).
Ente also makes it simple to share your albums with your loved ones. You can either share them directly with other Ente users, end-to-end encrypted; or with publicly viewable links.
Ente také zjednodušuje sdílení alb s vašimi blízkými. Můžete je buď sdílet přímo s ostatními uživateli Ente, koncově šifrované; nebo s pomocí veřejně přístupných odkazů.
Your encrypted data is stored across multiple locations, including a fall-out shelter in Paris. We take posterity seriously and make it easy to ensure that your memories outlive you.
Vaše šifrovaná data jsou uložena na několika místech, včetně protiatomového krytu v Paříži. Bereme budoucnost vážně a usnadňujeme vám zajistit, aby vaše vzpomínky přežily vás samotné.
We are here to make the safest photos app ever, come join our journey!
Jsme tu, abychom vytvořili nejbezpečnější aplikaci pro fotografie, jaká kdy byla. Přidejte se k nám!
FEATURES
- Original quality backups, because every pixel is important
- Family plans, so you can share storage with your family
- Shared folders, in case you want your partner to enjoy your "Camera" clicks
- Album links, that can be protected with a password and set to expire
- Ability to free up space, by removing files that have been safely backed up
- Image editor, to add finishing touches
- Favorite, hide and relive your memories, for they are precious
- One-click import from all major storage providers
- Dark theme, because your photos look good in it
- 2FA, 3FA, biometric auth
- and a LOT more!
VLASTNOSTI
- Zálohování v původní kvalitě, protože každý pixel je důležitý
- Rodinné plány, takže můžete sdílet úložiště s rodinou
- Sdílené složky, aby i váš partner mohl obdivovat vaše fotografie
- Odkazy na alba, které lze chránit heslem a nastavit jejich platnost
- Možnost uvolnit místo odstraněním souborů, které byly bezpečně zálohovány
- Editor obrázků, pro finální doladění detailů
- Označte si své oblíbené fotografie, skryjte je a nebo je prožijte znovu pomocí vzpomínek
- Import jedním kliknutím ze všech velkých poskytovatelů úložišť
- Tmavý motiv, protože vaše fotografie v něm vypadají dobře
- 2FA, 3FA, biometrické ověření
- a ještě MNOHEM víc!
PRICING
We don't offer forever free plans, because it is important to us that we remain sustainable and withstand the test of time. Instead we offer affordable plans that you can freely share with your family. You can find more information at ente.io.
CENY
Nenabízíme doživotní tarify, protože je pro nás důležité zůstat udržitelnými a obstát ve zkoušce času. Místo toho nabízíme cenově dostupné plány, které můžete svobodně sdílet se svou rodinou. Více informací najdete na ente.io.
SUPPORT
We take pride in offering human support. If you are our paid customer, you can reach out to team@ente.io and expect a response from our team within 24 hours.
PODPORA
Jsme hrdí na to, že poskytujeme lidskou podporu. Pokud máte placený tarif, můžete se obrátit na team@ente.io a do 24 hodin od našeho týmu očekávat odpověď.
TERMS
PODMÍNKY
https://ente.io/terms

View File

@@ -1 +1 @@
photos,photography,family,privacy,cloud,backup,videos,photo,encryption,storage,album,alternative
fotky,fotografie,rodina,soukromí,cloud,zálohování,videa,fotka,šifrování,úložiště,album,alternativa

View File

@@ -1 +1 @@
Ente Photos
Ente Fotky

View File

@@ -1 +1 @@
Encrypted photo storage
Šifrované úložiště fotografií

View File

@@ -1,30 +1,30 @@
Ente is a simple app to automatically backup and organize your photos and videos.
Ente je jednoduchá aplikace pro automatické zálohování a organizaci vašich fotek a videí.
If you've been looking for a privacy-friendly alternative to preserve your memories, you've come to the right place. With Ente, they are stored end-to-end encrypted (e2ee). This means that only you can view them.
Pokud hledáte alternativu, která respektuje vaše soukromí a umožní vám uchovat vaše vzpomínky, jste na správném místě. S Ente jsou uloženy s koncovým "end-to-end" šifrováním (e2ee). To znamená, že je můžete vidět pouze vy.
We have apps across Android, iOS, web and Desktop, and your photos will seamlessly sync between all your devices in an end-to-end encrypted (e2ee) manner.
Máme aplikace napříč všemi platformami a vaše fotky se budou bezproblémově synchronizovat mezi všemi vašimi zařízeními tak, aby byly koncově šifrovány mezi jednotlivými zařízeními (e2ee).
Ente also makes it simple to share your albums with your loved ones. You can either share them directly with other Ente users, end-to-end encrypted; or with publicly viewable links.
Ente také zjednodušuje sdílení alb s vašimi blízkými. Můžete je buď sdílet přímo s ostatními uživateli Ente, koncově šifrované; nebo s pomocí veřejně přístupných odkazů.
Your encrypted data is stored across multiple locations, including a fall-out shelter in Paris. We take posterity seriously and make it easy to ensure that your memories outlive you.
Vaše šifrovaná data jsou uložena na několika místech, včetně protiatomového krytu v Paříži. Bereme budoucnost vážně a usnadňujeme vám zajistit, aby vaše vzpomínky přežily vás samotné.
We are here to make the safest photos app ever, come join our journey!
Jsme tu, abychom vytvořili nejbezpečnější aplikaci pro fotografie, jaká kdy byla. Přidejte se k nám!
FEATURES
- Original quality backups, because every pixel is important
- Family plans, so you can share storage with your family
- Shared folders, in case you want your partner to enjoy your "Camera" clicks
- Album links, that can be protected with a password and set to expire
- Ability to free up space, by removing files that have been safely backed up
- Image editor, to add finishing touches
- Favorite, hide and relive your memories, for they are precious
- One-click import from Google, Apple, your hard drive and more
- Dark theme, because your photos look good in it
- 2FA, 3FA, biometric auth
- and a LOT more!
VLASTNOSTI
- Zálohování v původní kvalitě, protože každý pixel je důležitý
- Rodinné plány, takže můžete sdílet úložiště s rodinou
- Sdílené složky, aby i váš partner mohl obdivovat vaše fotografie
- Odkazy na alba, které lze chránit heslem a nastavit jejich platnost
- Možnost uvolnit místo odstraněním souborů, které byly bezpečně zálohovány
- Editor obrázků, pro finální doladění detailů
- Označte si své oblíbené fotografie, skryjte je a nebo je prožijte znovu pomocí vzpomínek
- Import jedním kliknutím ze všech velkých poskytovatelů úložišť
- Tmavý motiv, protože vaše fotografie v něm vypadají dobře
- 2FA, 3FA, biometrické ověření
- a ještě MNOHEM víc!
💲 PRICING
We don't offer forever free plans, because it is important to us that we remain sustainable and withstand the test of time. Instead we offer affordable plans that you can freely share with your family. You can find more information at ente.io.
💲 CENY
Nenabízíme doživotní tarify, protože je pro nás důležité zůstat udržitelnými a obstát ve zkoušce času. Místo toho nabízíme cenově dostupné plány, které můžete svobodně sdílet se svou rodinou. Více informací najdete na ente.io.
🙋 SUPPORT
We take pride in offering human support. If you are our paid customer, you can reach out to team@ente.io and expect a response from our team within 24 hours.
🙋 PODPORA
Jsme hrdí na to, že poskytujeme lidskou podporu. Pokud máte placený tarif, můžete se obrátit na team@ente.io a do 24 hodin od našeho týmu očekávat odpověď.

View File

@@ -1 +1 @@
Encrypted photo storage - backup, organize and share your photos and videos
Šifrované úložiště fotografií zálohujte, organizujte a sdílejte své fotografie a videa

View File

@@ -1 +1 @@
Ente Photos
Ente Fotky

View File

@@ -3,3 +3,4 @@ output-dir: lib/generated/intl
template-arb-file: intl_en.arb
output-localization-file: app_localizations.dart
nullable-getter: false
use-named-parameters: true

View File

@@ -981,7 +981,8 @@ class FilesDB with SqlDbBase {
// remove references for local files which are either already uploaded
// or queued for upload but not yet uploaded
Future<int> removeQueuedLocalFiles(Set<String> localIDs) async {
// Remove queued local files that have duplicate uploaded entries with same localID
Future<int> removeQueuedLocalFiles(Set<String> localIDs, int ownerID) async {
if (localIDs.isEmpty) {
_logger.finest("No local IDs provided for removal");
return 0;
@@ -990,54 +991,63 @@ class FilesDB with SqlDbBase {
final db = await instance.sqliteAsyncDB;
const batchSize = 10000;
int totalRemoved = 0;
final localIDsList = localIDs.toList();
for (int i = 0; i < localIDsList.length; i += batchSize) {
final endIndex = (i + batchSize > localIDsList.length)
? localIDsList.length
: i + batchSize;
final batch = localIDsList.sublist(i, endIndex);
final placeholders = List.filled(batch.length, '?').join(',');
final List<String> alreadyUploaded = [];
// find localIDs that are already uploaded
final result = await db.execute('''
// Find localIDs that already have uploaded entries
final result = await db.execute(
'''
SELECT DISTINCT $columnLocalID
FROM $filesTable
WHERE $columnLocalID IN ($placeholders)
WHERE
$columnOwnerID = $ownerID
AND $columnLocalID IN ($placeholders)
AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID != -1)
''');
for (final row in result) {
alreadyUploaded.add(row[columnLocalID] as String);
}
final uploadedPlaceholders =
alreadyUploaded.map((id) => "'$id'").join(',');
final r = await db.execute(
'''
DELETE FROM $filesTable
WHERE $columnLocalID IN ($uploadedPlaceholders)
AND ($columnUploadedFileID IS NULL OR $columnUploadedFileID = -1)
''',
''',
batch,
);
if (r.isNotEmpty) {
_logger.warning(
"Batch ${(i ~/ batchSize) + 1}: Removed duplicate ${r.length} files",
if (result.isNotEmpty) {
final alreadyUploadedLocalIDs =
result.map((row) => row[columnLocalID] as String).toList();
final localIdPlaceholder =
List.filled(alreadyUploadedLocalIDs.length, '?').join(',');
// Delete queued entries for localIDs that already have uploaded versions
final deleteResult = await db.execute(
'''
DELETE FROM $filesTable
WHERE $columnLocalID IN ($localIdPlaceholder)
AND ($columnUploadedFileID IS NULL OR $columnUploadedFileID = -1)
''',
alreadyUploadedLocalIDs,
);
totalRemoved += r.length;
final removedCount =
deleteResult.length; // or however your DB returns affected rows
if (removedCount > 0) {
_logger.warning(
"Batch ${(i ~/ batchSize) + 1}: Removed $removedCount queued duplicates",
);
totalRemoved += removedCount;
}
}
}
if (totalRemoved > 0) {
_logger.warning(
"Removed $totalRemoved potential dups for already queued local files",
"Removed $totalRemoved queued files that had uploaded duplicates",
);
} else {
_logger.finest("No duplicate id found for queued/uploaded files");
_logger.finest("No queued duplicates found for uploaded files");
}
return totalRemoved;
}
@@ -1677,26 +1687,36 @@ class FilesDB with SqlDbBase {
);
}
Future<List<EnteFile>> getAllFilesAfterDate({
required FileType fileType,
required DateTime beginDate,
Future<List<EnteFile>> getStreamingEligibleVideoFiles({
DateTime? beginDate,
required int userID,
bool onlyFilesWithLocalId = false,
}) async {
final db = await instance.sqliteAsyncDB;
final results = await db.getAll(
'''
String query = '''
SELECT * FROM $filesTable
WHERE $columnFileType = ?
AND $columnCreationTime > ?
AND $columnUploadedFileID != -1
AND $columnOwnerID = $userID
AND $columnLocalID IS NOT NULL
AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID != -1)
AND $columnOwnerID = ?
AND ($columnFileSize IS NOT NULL AND $columnFileSize <= 524288000)
AND ($columnDuration IS NOT NULL AND ($columnDuration <= 60 AND $columnDuration > 0))
ORDER BY $columnCreationTime DESC
''',
[getInt(fileType), beginDate.microsecondsSinceEpoch],
);
''';
final List<Object> queryArgs = [getInt(FileType.video), userID];
if (beginDate != null) {
query += ' AND $columnCreationTime > ?';
queryArgs.add(beginDate.microsecondsSinceEpoch);
}
if (onlyFilesWithLocalId) {
query += ' AND $columnLocalID IS NOT NULL';
}
query += ' ORDER BY $columnCreationTime DESC';
final results = await db.getAll(query, queryArgs);
return convertToFiles(results);
}

View File

@@ -1,4 +1,3 @@
import "dart:io" show File;
import "dart:typed_data" show Float32List;
import "package:flutter_rust_bridge/flutter_rust_bridge.dart" show Uint64List;
@@ -8,11 +7,13 @@ import "package:path_provider/path_provider.dart";
import "package:photos/models/ml/vector.dart";
import "package:photos/services/machine_learning/semantic_search/query_result.dart";
import "package:photos/src/rust/api/usearch_api.dart";
import "package:shared_preferences/shared_preferences.dart";
class ClipVectorDB {
static final Logger _logger = Logger("ClipVectorDB");
static const _databaseName = "ente.ml.vectordb.clip";
static const _kMigrationKey = "clip_vector_migration";
static final BigInt _embeddingDimension = BigInt.from(512);
@@ -51,10 +52,9 @@ class ClipVectorDB {
Future<bool> checkIfMigrationDone() async {
if (_migrationDone != null) return _migrationDone!;
_logger.info("Checking if ClipVectorDB migration has run");
final documentsDirectory = await getApplicationDocumentsDirectory();
final migrationFlagFile =
File(join(documentsDirectory.path, 'clip_vector_migration_done'));
if (await migrationFlagFile.exists()) {
final prefs = await SharedPreferences.getInstance();
final migrationDone = prefs.getBool(_kMigrationKey) ?? false;
if (migrationDone) {
_logger.info("ClipVectorDB migration already done");
_migrationDone = true;
return _migrationDone!;
@@ -67,10 +67,8 @@ class ClipVectorDB {
Future<void> setMigrationDone() async {
_logger.info("Setting ClipVectorDB migration done");
final documentsDirectory = await getApplicationDocumentsDirectory();
final migrationFlagFile =
File(join(documentsDirectory.path, 'clip_vector_migration_done'));
await migrationFlagFile.create(recursive: true);
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_kMigrationKey, true);
_migrationDone = true;
}

View File

@@ -53,12 +53,20 @@ class UploadLocksDB {
columnCreatedAt: "created_at",
);
static const _streamQueueTable = (
table: "stream_queue",
columnUploadedFileID: "uploaded_file_id",
columnQueueType: "queue_type", // 'create' or 'recreate'
columnCreatedAt: "created_at",
);
static final initializationScript = [
..._createUploadLocksTable(),
];
static final migrationScripts = [
..._createTrackUploadsTable(),
..._createStreamQueueTable(),
];
final dbConfig = MigrationConfig(
@@ -137,11 +145,24 @@ class UploadLocksDB {
];
}
static List<String> _createStreamQueueTable() {
return [
'''
CREATE TABLE IF NOT EXISTS ${_streamQueueTable.table} (
${_streamQueueTable.columnUploadedFileID} INTEGER PRIMARY KEY,
${_streamQueueTable.columnQueueType} TEXT NOT NULL,
${_streamQueueTable.columnCreatedAt} INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL
)
''',
];
}
Future<void> clearTable() async {
final db = await instance.database;
await db.delete(_uploadLocksTable.table);
await db.delete(_trackUploadTable.table);
await db.delete(_partsTable.table);
await db.delete(_streamQueueTable.table);
}
Future<void> acquireLock(String id, String owner, int time) async {
@@ -519,4 +540,56 @@ class UploadLocksDB {
return row[_trackUploadTable.columnEncryptedFileName] as String;
});
}
// Stream Queue Management Methods
Future<void> addToStreamQueue(
int uploadedFileID,
String queueType, // 'create' or 'recreate'
) async {
final db = await instance.database;
await db.insert(
_streamQueueTable.table,
{
_streamQueueTable.columnUploadedFileID: uploadedFileID,
_streamQueueTable.columnQueueType: queueType,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<void> removeFromStreamQueue(int uploadedFileID) async {
final db = await instance.database;
await db.delete(
_streamQueueTable.table,
where: '${_streamQueueTable.columnUploadedFileID} = ?',
whereArgs: [uploadedFileID],
);
}
Future<Map<int, String>> getStreamQueue() async {
final db = await instance.database;
final rows = await db.query(
_streamQueueTable.table,
columns: [
_streamQueueTable.columnUploadedFileID,
_streamQueueTable.columnQueueType,
],
);
final map = <int, String>{};
for (final row in rows) {
map[row[_streamQueueTable.columnUploadedFileID] as int] =
row[_streamQueueTable.columnQueueType] as String;
}
return map;
}
Future<bool> isInStreamQueue(int uploadedFileID) async {
final db = await instance.database;
final rows = await db.query(
_streamQueueTable.table,
where: '${_streamQueueTable.columnUploadedFileID} = ?',
whereArgs: [uploadedFileID],
);
return rows.isNotEmpty;
}
}

View File

@@ -76,8 +76,7 @@ class _EmergencyPageState extends State<EmergencyPage> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = EnteTheme.getColorScheme(theme);
final colorScheme = getEnteColorScheme(context);
final currentUserID = Configuration.instance.getUserID()!;
final List<EmergencyContact> othersTrustedContacts =
info?.othersEmergencyContact ?? [];
@@ -181,7 +180,7 @@ class _EmergencyPageState extends State<EmergencyPage> {
currentUserID: currentUserID,
),
menuItemColor:
EnteTheme.getColorScheme(theme).fillFaint,
getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right,
trailingIconIsMuted: true,
onTap: () async {
@@ -193,7 +192,7 @@ class _EmergencyPageState extends State<EmergencyPage> {
),
DividerWidget(
dividerType: DividerType.menu,
bgColor: EnteTheme.getColorScheme(theme).fillFaint,
bgColor: getEnteColorScheme(context).fillFaint,
),
],
);
@@ -204,13 +203,13 @@ class _EmergencyPageState extends State<EmergencyPage> {
const SizedBox(height: 20),
Text(
context.l10n.legacyPageDesc,
style: EnteTheme.getTextTheme(theme).body,
style: getEnteTextTheme(context).body,
),
SizedBox(
height: 200,
width: 200,
child: SvgPicture.asset(
EnteTheme.getColorScheme(theme).backdropBase ==
getEnteColorScheme(context).backdropBase ==
backgroundBaseDark
? "assets/icons/legacy-light.svg"
: "assets/icons/legacy-dark.svg",
@@ -220,7 +219,7 @@ class _EmergencyPageState extends State<EmergencyPage> {
),
Text(
context.l10n.legacyPageDesc2,
style: EnteTheme.getTextTheme(theme).smallMuted,
style: getEnteTextTheme(context).smallMuted,
),
const SizedBox(height: 16),
ButtonWidget(
@@ -249,8 +248,7 @@ class _EmergencyPageState extends State<EmergencyPage> {
),
leadingIcon: Icons.add_outlined,
surfaceExecutionStates: false,
menuItemColor:
EnteTheme.getColorScheme(theme).fillFaint,
menuItemColor: getEnteColorScheme(context).fillFaint,
onTap: () async {
await routeToPage(
context,
@@ -310,7 +308,7 @@ class _EmergencyPageState extends State<EmergencyPage> {
currentUserID: currentUserID,
),
menuItemColor:
EnteTheme.getColorScheme(theme).fillFaint,
getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right,
trailingIconIsMuted: true,
onTap: () async {
@@ -353,7 +351,7 @@ class _EmergencyPageState extends State<EmergencyPage> {
: DividerWidget(
dividerType: DividerType.menu,
bgColor:
EnteTheme.getColorScheme(theme).fillFaint,
getEnteColorScheme(context).fillFaint,
),
],
);
@@ -499,7 +497,8 @@ class _EmergencyPageState extends State<EmergencyPage> {
isInAlert: true,
),
],
body: AppLocalizations.of(context).legacyInvite(contact.user.email),
body:
AppLocalizations.of(context).legacyInvite(email: contact.user.email),
actionSheetType: ActionSheetType.defaultActionSheet,
);
return;
@@ -552,7 +551,7 @@ class _EmergencyPageState extends State<EmergencyPage> {
isInAlert: true,
),
],
body: context.l10n.recoveryWarningBody(emergencyContactEmail),
body: context.l10n.recoveryWarningBody(email: emergencyContactEmail),
actionSheetType: ActionSheetType.defaultActionSheet,
);
return;

View File

@@ -79,9 +79,8 @@ class _OtherContactPageState extends State<OtherContactPage> {
);
waitTill = getFormattedTime(context, dateTime);
}
final theme = Theme.of(context);
final colorScheme = EnteTheme.getColorScheme(theme);
final textTheme = EnteTheme.getTextTheme(theme);
final colorScheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Scaffold(
appBar: AppBar(),
body: Padding(
@@ -118,7 +117,7 @@ class _OtherContactPageState extends State<OtherContactPage> {
)
: (recoverySession!.status == "READY"
? Text(
context.l10n.recoveryReady(accountEmail),
context.l10n.recoveryReady(email: accountEmail),
style: textTheme.body,
)
: Text(
@@ -156,8 +155,8 @@ class _OtherContactPageState extends State<OtherContactPage> {
context,
context.l10n.recoveryInitiated,
context.l10n.recoveryInitiatedDesc(
widget.contact.recoveryNoticeInDays,
Configuration.instance.getEmail()!,
days: widget.contact.recoveryNoticeInDays,
email: Configuration.instance.getEmail()!,
),
);
}
@@ -230,7 +229,7 @@ class _OtherContactPageState extends State<OtherContactPage> {
),
leadingIcon: Icons.not_interested_outlined,
leadingIconColor: warning500,
menuItemColor: EnteTheme.getColorScheme(theme).fillFaint,
menuItemColor: getEnteColorScheme(context).fillFaint,
surfaceExecutionStates: false,
onTap: () async {
await showRemoveSheet();

View File

@@ -80,13 +80,12 @@ class _RecoverOthersAccountState extends State<RecoverOthersAccount> {
String title = AppLocalizations.of(context).setPasswordTitle;
title = AppLocalizations.of(context).resetPasswordTitle;
final theme = Theme.of(context);
return Scaffold(
resizeToAvoidBottomInset: isKeypadOpen,
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
@@ -119,7 +118,6 @@ class _RecoverOthersAccountState extends State<RecoverOthersAccount> {
passwordStrengthText = AppLocalizations.of(context).moderateStrength;
passwordStrengthColor = Colors.orangeAccent;
}
final theme = Theme.of(context);
return Column(
children: [
Expanded(
@@ -131,7 +129,7 @@ class _RecoverOthersAccountState extends State<RecoverOthersAccount> {
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Text(
buttonTextAndHeading,
style: theme.textTheme.headlineMedium,
style: Theme.of(context).textTheme.headlineMedium,
),
),
Padding(
@@ -140,7 +138,10 @@ class _RecoverOthersAccountState extends State<RecoverOthersAccount> {
"Enter new password for $email account. You will be able "
"to use this password to login into $email account.",
textAlign: TextAlign.start,
style: theme.textTheme.titleMedium!.copyWith(fontSize: 14),
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 14),
),
),
const Padding(padding: EdgeInsets.all(12)),
@@ -178,7 +179,7 @@ class _RecoverOthersAccountState extends State<RecoverOthersAccount> {
_password1Visible
? Icons.visibility
: Icons.visibility_off,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
size: 20,
),
onPressed: () {
@@ -190,8 +191,11 @@ class _RecoverOthersAccountState extends State<RecoverOthersAccount> {
: _isPasswordValid
? Icon(
Icons.check,
color: theme.inputDecorationTheme
.focusedBorder!.borderSide.color,
color: Theme.of(context)
.inputDecorationTheme
.focusedBorder!
.borderSide
.color,
)
: null,
),
@@ -237,7 +241,7 @@ class _RecoverOthersAccountState extends State<RecoverOthersAccount> {
_password2Visible
? Icons.visibility
: Icons.visibility_off,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
size: 20,
),
onPressed: () {
@@ -249,8 +253,11 @@ class _RecoverOthersAccountState extends State<RecoverOthersAccount> {
: _passwordsMatch
? Icon(
Icons.check,
color: theme.inputDecorationTheme
.focusedBorder!.borderSide.color,
color: Theme.of(context)
.inputDecorationTheme
.focusedBorder!
.borderSide
.color,
)
: null,
border: UnderlineInputBorder(
@@ -277,8 +284,9 @@ class _RecoverOthersAccountState extends State<RecoverOthersAccount> {
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Text(
AppLocalizations.of(context)
.passwordStrength(passwordStrengthText),
AppLocalizations.of(context).passwordStrength(
passwordStrengthValue: passwordStrengthText,
),
style: TextStyle(
color: passwordStrengthColor,
),

View File

@@ -58,9 +58,8 @@ class _AddContactPage extends State<AddContactPage> {
@override
Widget build(BuildContext context) {
isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
final theme = Theme.of(context);
final enteTextTheme = EnteTheme.getTextTheme(theme);
final enteColorScheme = EnteTheme.getColorScheme(theme);
final enteTextTheme = getEnteTextTheme(context);
final enteColorScheme = getEnteColorScheme(context);
final List<User> suggestedUsers = _getSuggestedUser();
isEmailListEmpty = suggestedUsers.isEmpty;
return Scaffold(
@@ -86,7 +85,7 @@ class _AddContactPage extends State<AddContactPage> {
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: _getEmailField(theme),
child: _getEmailField(),
),
if (isEmailListEmpty)
const Expanded(child: SizedBox.shrink())
@@ -129,9 +128,9 @@ class _AddContactPage extends State<AddContactPage> {
type: AvatarType.mini,
),
menuItemColor:
EnteTheme.getColorScheme(theme).fillFaint,
getEnteColorScheme(context).fillFaint,
pressedColor:
EnteTheme.getColorScheme(theme).fillFaint,
getEnteColorScheme(context).fillFaint,
trailingIcon:
(selectedEmail == currentUser.email)
? Icons.check
@@ -153,8 +152,8 @@ class _AddContactPage extends State<AddContactPage> {
? const SizedBox.shrink()
: DividerWidget(
dividerType: DividerType.menu,
bgColor: EnteTheme.getColorScheme(theme)
.fillFaint,
bgColor:
getEnteColorScheme(context).fillFaint,
),
],
);
@@ -194,8 +193,8 @@ class _AddContactPage extends State<AddContactPage> {
title: AppLocalizations.of(context).warning,
body: AppLocalizations.of(context)
.confirmAddingTrustedContact(
emailToAdd,
30,
email: emailToAdd,
numOfDays: 30,
),
firstButtonLabel:
AppLocalizations.of(context).proceed,
@@ -270,19 +269,19 @@ class _AddContactPage extends State<AddContactPage> {
setState(() => {});
}
Widget _getEmailField(ThemeData theme) {
Widget _getEmailField() {
return TextFormField(
controller: _textController,
focusNode: textFieldFocusNode,
style: EnteTheme.getTextTheme(theme).body,
style: getEnteTextTheme(context).body,
autofillHints: const [AutofillHints.email],
decoration: InputDecoration(
focusedBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
borderSide:
BorderSide(color: EnteTheme.getColorScheme(theme).strokeMuted),
BorderSide(color: getEnteColorScheme(context).strokeMuted),
),
fillColor: EnteTheme.getColorScheme(theme).fillFaint,
fillColor: getEnteColorScheme(context).fillFaint,
filled: true,
hintText: AppLocalizations.of(context).enterEmail,
contentPadding: const EdgeInsets.symmetric(
@@ -295,7 +294,7 @@ class _AddContactPage extends State<AddContactPage> {
),
prefixIcon: Icon(
Icons.email_outlined,
color: EnteTheme.getColorScheme(theme).strokeMuted,
color: getEnteColorScheme(context).strokeMuted,
),
suffixIcon: _email == ''
? null
@@ -303,7 +302,7 @@ class _AddContactPage extends State<AddContactPage> {
onPressed: clearFocus,
icon: Icon(
Icons.cancel,
color: EnteTheme.getColorScheme(theme).strokeMuted,
color: getEnteColorScheme(context).strokeMuted,
),
),
),

View File

@@ -0,0 +1,12 @@
import "package:photos/events/event.dart";
import "package:photos/models/preview/preview_item_status.dart";
class VideoPreviewStateChangedEvent extends Event {
final int fileId;
final PreviewItemStatus status;
VideoPreviewStateChangedEvent(this.fileId, this.status);
@override
String get reason => '$runtimeType: fileId=$fileId, status=$status';
}

File diff suppressed because it is too large Load Diff

View File

@@ -1827,5 +1827,119 @@
"type": "int"
}
}
}
}
},
"videosProcessed": "Videos processed",
"totalVideos": "Total videos",
"skippedVideos": "Skipped videos",
"videoStreamingDescription": "Play videos instantly on any device. Enable to process video streams on this device.",
"videoStreamingNote": "Only videos from last 60 days and under 1 minute are processed on this device. For older/longer videos, enable streaming in the desktop app.",
"createStream": "Create stream",
"recreateStream": "Recreate stream",
"addedToStreamCreationQueue": "Added to stream creation queue",
"addedToStreamRecreationQueue": "Added to stream recreation queue",
"videoPreviewAlreadyExists": "Video preview already exists",
"videoAlreadyInQueue": "Video file already present in the queue",
"addedToQueue": "Added to queue",
"creatingStream": "Creating stream",
"similarImages": "Similar images",
"deletingProgress": "Deleting... {progress}",
"@deletingProgress": {
"placeholders": {
"progress": {
"type": "String"
}
}
},
"findSimilarImages": "Find similar images",
"noSimilarImagesFound": "No similar images found",
"yourPhotosLookUnique": "Your photos look unique",
"similarGroupsFound": "{count, plural, =1{{count} group found} other{{count} groups found}}",
"@similarGroupsFound": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"reviewAndRemoveSimilarImages": "Review and remove similar images",
"deletePhotosWithSize": "Delete {count} photos ({size})",
"@deletePhotosWithSize": {
"placeholders": {
"count": {
"type": "int"
},
"size": {
"type": "String"
}
}
},
"selectionOptions": "Selection options",
"selectExactWithCount": "Exactly similar ({count})",
"@selectExactWithCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"selectExact": "Select exact",
"selectSimilarWithCount": "Mostly similar ({count})",
"@selectSimilarWithCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"selectSimilar": "Select similar",
"selectAllWithCount": "All similarities ({count})",
"@selectAllWithCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"selectSimilarImagesTitle": "Select similar images",
"chooseSimilarImagesToSelect": "Select images based on their visual similarity",
"clearSelection": "Clear selection",
"similarImagesCount": "{count} similar images",
"@similarImagesCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"deleteWithCount": "Delete ({count})",
"@deleteWithCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"deleteFiles": "Delete files",
"areYouSureDeleteFiles": "Are you sure you want to delete these files?",
"greatJob": "Great job!",
"cleanedUpSimilarImages": "You cleaned up {count, plural, =1{{count} similar image} other{{count} similar images}} and freed up {size}",
"@cleanedUpSimilarImages": {
"placeholders": {
"count": {
"type": "int"
},
"size": {
"type": "String"
}
}
},
"size": "Size",
"similarity": "Similarity",
"analyzingPhotosLocally": "Analyzing your photos locally",
"lookingForVisualSimilarities": "Looking for visual similarities",
"comparingImageDetails": "Comparing image details",
"findingSimilarImages": "Finding similar images",
"almostDone": "Almost done",
"processingLocally": "Processing locally",
"useMLToFindSimilarImages": "Use ML to find images that look similar to each other."
}

View File

@@ -1776,5 +1776,56 @@
"same": "Igual",
"different": "Diferente",
"sameperson": "la misma persona?",
"indexingPausedStatusDescription": "La indexación está pausada. Se reanudará automáticamente cuando el dispositivo esté listo. El dispositivo se considera listo cuando su nivel de batería, la salud de la batería y temperatura están en un rango saludable."
"cLTitle1": "Editor avanzado de imágenes",
"cLDesc1": "Estamos lanzando un nuevo y avanzado editor de imágenes que añade más marcos de recorte, preajustes de filtros para edición rápida, opciones de ajuste finas incluyendo saturación, contraste, brillo, temperatura y mucho más. El nuevo editor también incluye la capacidad de dibujar en tus fotos y añadir emojis como pegatinas.",
"cLTitle2": "Álbumes Inteligentes",
"cLDesc2": "Ahora puedes añadir automáticamente fotos de personas seleccionadas a cualquier álbum. Solo tienes que ir al álbum, y seleccionar \"Agregar personas automáticamente\" del menú desbordante. Si se utiliza junto con el álbum compartido, puedes compartir fotos con cero clics.",
"cLTitle3": "Galería mejorada",
"cLDesc3": "Hemos añadido la capacidad de agrupar tu galería por semanas, meses y años. Ahora puedes personalizar tu galería exactamente como quieras con estas nuevas opciones de agrupación, junto con rejillas personalizadas",
"cLTitle4": "Desplazamiento más rápido",
"cLDesc4": "Junto con un montón de mejoras bajo el capó para mejorar la experiencia del desplazamiento de la galería también hemos rediseñado la barra de desplazamiento para mostrar los marcadores, permitiéndote saltar rápidamente a través de la línea de tiempo.",
"indexingPausedStatusDescription": "La indexación está pausada. Se reanudará automáticamente cuando el dispositivo esté listo. El dispositivo se considera listo cuando su nivel de batería, la salud de la batería y temperatura están en un rango saludable.",
"thisWeek": "Esta semana",
"lastWeek": "Semana pasada",
"thisMonth": "Este mes",
"thisYear": "Este año",
"groupBy": "Agrupar por",
"faceThumbnailGenerationFailed": "No se pueden generar las miniaturas de cara",
"fileAnalysisFailed": "No se puede analizar el archivo",
"editAutoAddPeople": "Editar agregar personas automáticamente",
"autoAddPeople": "Agregar personas automáticamente",
"autoAddToAlbum": "Añadir al álbum automáticamente",
"shouldRemoveFilesSmartAlbumsDesc": "¿Deben eliminarse los archivos relacionados con la persona previamente seleccionada en los álbumes inteligentes?",
"addingPhotos": "Añadiendo fotos",
"gettingReady": "Preparándose",
"addSomePhotosDesc1": "Añadir algunas fotos o elegir ",
"addSomePhotosDesc2": "caras familiares",
"addSomePhotosDesc3": "\npara comenzar con",
"ignorePerson": "Ignorar persona",
"mixedGrouping": "¿Grupo mixto?",
"analysis": "Análisis",
"doesGroupContainMultiplePeople": "¿Esta agrupación contiene varias personas?",
"automaticallyAnalyzeAndSplitGrouping": "Analizaremos automáticamente la agrupación para determinar si hay varias personas presentes, y separarlas de nuevo. Esto puede tardar unos segundos.",
"layout": "Disposición",
"day": "Día",
"peopleAutoAddDesc": "Selecciona las personas que quieres añadir automáticamente al álbum",
"undo": "Deshacer",
"redo": "Rehacer",
"filter": "Filtro",
"adjust": "Ajustar",
"draw": "Dibujar",
"sticker": "Pegatina",
"brushColor": "Color del pincel",
"font": "Fuente",
"background": "Fondo",
"align": "Alinear",
"addedToAlbums": "{count, plural, one {}=1{Añadido con éxito a 1 álbum} other{Añadido con éxito a {count} álbumes}}",
"@addedToAlbums": {
"description": "Message shown when items are added to albums",
"placeholders": {
"count": {
"type": "int"
}
}
}
}

View File

@@ -347,9 +347,11 @@
"deletePhotos": "Radera foton",
"inviteToEnte": "Bjud in till Ente",
"removePublicLink": "Ta bort publik länk",
"disableLinkMessage": "Detta kommer att ta bort den publika länken för att komma åt \"{albumName}\".",
"sharing": "Delar...",
"youCannotShareWithYourself": "Du kan inte dela med dig själv",
"archive": "Arkiv",
"createAlbumActionHint": "Långtryck för att välja foton och klicka på + för att skapa ett album",
"importing": "Importerar....",
"failedToLoadAlbums": "Det gick inte att läsa in album",
"hidden": "Dold",
@@ -360,16 +362,96 @@
"videoSmallCase": "video",
"photoSmallCase": "foto",
"singleFileDeleteHighlight": "Det kommer att tas bort från alla album.",
"singleFileInBothLocalAndRemote": "Denna {fileType} finns i både Ente och din enhet.",
"singleFileInRemoteOnly": "Denna {fileType} kommer att raderas från Ente.",
"singleFileDeleteFromDevice": "Denna {fileType} kommer att raderas från din enhet.",
"deleteFromEnte": "Radera från ente",
"yesDelete": "Ja, radera",
"movedToTrash": "Flyttad till papperskorgen",
"deleteFromDevice": "Radera från enhet",
"deleteFromBoth": "",
"newAlbum": "Nytt album",
"albums": "Album",
"memoryCount": "{count, plural, =0{inga minnen} one{{formattedCount} minne} other{{formattedCount} minnen}}",
"@memoryCount": {
"description": "The text to display the number of memories",
"type": "text",
"placeholders": {
"count": {
"example": "1",
"type": "int"
},
"formattedCount": {
"type": "String",
"example": "11.513, 11,511"
}
}
},
"selectedPhotos": "{count} markerade",
"@selectedPhotos": {
"description": "Display the number of selected photos",
"type": "text",
"placeholders": {
"count": {
"example": "5",
"type": "int"
}
}
},
"selectedPhotosWithYours": "{count} markerade ({yourCount} din)",
"@selectedPhotosWithYours": {
"description": "Display the number of selected photos, including the number of selected photos owned by the user",
"type": "text",
"placeholders": {
"count": {
"example": "12",
"type": "int"
},
"yourCount": {
"example": "2",
"type": "int"
}
}
},
"advancedSettings": "Avancerad",
"@advancedSettings": {
"description": "The text to display in the advanced settings section"
},
"photoGridSize": "Storlek på bildrutnät",
"manageDeviceStorage": "Hantera enhetscache",
"manageDeviceStorageDesc": "Granska och rensa lokal cachelagring.",
"machineLearning": "Maskininlärning",
"mlConsent": "Aktivera maskininlärning",
"mlConsentTitle": "Aktivera maskininlärning?",
"mlConsentDescription": "Om du aktiverar maskininlärning, kommer Ente extrahera information som ansiktsgeometri från filer, inklusive de som delas med dig.\n\nDetta kommer att hända på din enhet, och all genererad biometrisk information kommer att helsträckskrypteras.",
"mlConsentPrivacy": "Klicka här för mer information om denna funktion i vår integritetspolicy",
"mlConsentConfirmation": "Jag förstår och vill aktivera maskininlärning",
"magicSearch": "Magisk sökning",
"discover": "Upptäck",
"@discover": {
"description": "The text to display for the discover section under which we show receipts, screenshots, sunsets, greenery, etc."
},
"discover_identity": "Identitet",
"discover_screenshots": "Skärmdumpar",
"discover_receipts": "Kvitton",
"discover_notes": "Anteckningar",
"discover_memes": "Memes",
"discover_visiting_cards": "Besökskort",
"discover_babies": "Barn",
"discover_pets": "Husdjur",
"discover_selfies": "Selfies",
"discover_wallpapers": "Bakgrundsbilder",
"discover_food": "Mat",
"discover_celebrations": "Firanden",
"discover_sunset": "Solnedgång",
"discover_hills": "Berg",
"discover_greenery": "Grönt landskap",
"mlIndexingDescription": "Observera att maskininlärning kommer att resultera i en högre bandbredd och batterianvändning tills alla objekt är indexerade. Överväg att använda skrivbordsappen för snabbare indexering, alla resultat kommer att synkroniseras automatiskt.",
"loadingModel": "Laddar ner modeller...",
"waitingForWifi": "Väntar på WiFi...",
"status": "Status",
"indexedItems": "Indexerade objekt",
"pendingItems": "Väntande objekt",
"clearIndexes": "Rensa index",
"selectFoldersForBackup": "Välj mappar för säkerhetskopiering",
"selectedFoldersWillBeEncryptedAndBackedUp": "Valda mappar kommer att krypteras och säkerhetskopieras",
@@ -398,19 +480,47 @@
"yearsAgo": "{count, plural, one{{count} år sedan} other{{count} år sedan}}",
"backupSettings": "Säkerhetskopieringsinställningar",
"backupStatus": "Säkerhetskopieringsstatus",
"backupStatusDescription": "Objekt som har säkerhetskopierats kommer att visas här",
"backupOverMobileData": "Säkerhetskopiera via mobildata",
"backupVideos": "Säkerhetskopiera videor",
"disableAutoLock": "Inaktivera automatisk låsning",
"deviceLockExplanation": "Inaktivera enhetens skärmlås när Ente är i förgrunden och säkerhetskopiering pågår. Detta är normalt inte nödvändigt, men kan hjälpa stora uppladdningar och initial import av stora bibliotek slutföra snabbare.",
"about": "Om",
"weAreOpenSource": "Vi har öppen källkod!",
"privacy": "Sekretess",
"terms": "Villkor",
"checkForUpdates": "Sök efter uppdateringar",
"checkStatus": "Kontrollera status",
"checking": "Kontrollerar...",
"youAreOnTheLatestVersion": "Du är på den senaste versionen",
"account": "Konto",
"manageSubscription": "Hantera prenumeration",
"authToChangeYourEmail": "Autentisera för att ändra din e-postadress",
"changePassword": "Ändra lösenord",
"authToChangeYourPassword": "Autentisera för att ändra ditt lösenord",
"emailVerificationToggle": "E-postverifiering",
"exportYourData": "Exportera din data",
"logout": "Logga ut",
"authToInitiateAccountDeletion": "Vänligen autentisera för att initiera borttagning av konto",
"areYouSureYouWantToLogout": "Är du säker på att du vill logga ut?",
"yesLogout": "Ja, logga ut",
"aNewVersionOfEnteIsAvailable": "En ny version av Ente är tillgänglig.",
"update": "Uppdatera",
"installManually": "Installera manuellt",
"criticalUpdateAvailable": "Kritisk uppdatering tillgänglig",
"updateAvailable": "Uppdatering tillgänglig",
"ignoreUpdate": "Ignorera",
"downloading": "Laddar ner...",
"cannotDeleteSharedFiles": "Kan inte ta bort delade filer",
"theDownloadCouldNotBeCompleted": "Nedladdningen kunde inte slutföras",
"retry": "Försök igen",
"backedUpFolders": "Säkerhetskopiera mappar",
"backup": "Säkerhetskopiera",
"freeUpDeviceSpace": "Frigör enhetens lagringsutrymme",
"freeUpDeviceSpaceDesc": "Spara utrymme på din enhet genom att rensa filer som redan har säkerhetskopierats.",
"allClear": "✨ Allt klart",
"noDeviceThatCanBeDeleted": "Du har inga filer på denna enhet som kan tas bort",
"removeDuplicates": "Ta bort dubbletter",
"viewActiveSessions": "Visa aktiva sessioner",
"no": "Nej",
"yes": "Ja",

View File

@@ -280,7 +280,6 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
}).ignore();
}
_logger.info("PushService/HomeWidget done $tlog");
VideoPreviewService.instance.init(preferences);
unawaited(SemanticSearchService.instance.init());
unawaited(MLService.instance.init());
await PersonService.init(

View File

@@ -22,6 +22,6 @@ class FillerMemory extends SmartMemory {
@override
String createTitle(AppLocalizations locals, String languageCode) {
return locals.yearsAgo(yearsAgo);
return locals.yearsAgo(count: yearsAgo);
}
}

View File

@@ -75,25 +75,25 @@ String activityTitle(
) {
switch (activity) {
case PeopleActivity.admiring:
return locals.admiringThem(personName);
return locals.admiringThem(name: personName);
case PeopleActivity.embracing:
return locals.embracingThem(personName);
return locals.embracingThem(name: personName);
case PeopleActivity.party:
return locals.partyWithThem(personName);
return locals.partyWithThem(name: personName);
case PeopleActivity.hiking:
return locals.hikingWithThem(personName);
return locals.hikingWithThem(name: personName);
case PeopleActivity.feast:
return locals.feastingWithThem(personName);
return locals.feastingWithThem(name: personName);
case PeopleActivity.selfies:
return locals.selfiesWithThem(personName);
return locals.selfiesWithThem(name: personName);
case PeopleActivity.posing:
return locals.posingWithThem(personName);
return locals.posingWithThem(name: personName);
case PeopleActivity.background:
return locals.backgroundWithThem(personName);
return locals.backgroundWithThem(name: personName);
case PeopleActivity.sports:
return locals.sportsWithThem(personName);
return locals.sportsWithThem(name: personName);
case PeopleActivity.roadtrip:
return locals.roadtripWithThem(personName);
return locals.roadtripWithThem(name: personName);
}
}
@@ -154,7 +154,7 @@ class PeopleMemory extends SmartMemory {
switch (peopleMemoryType) {
case PeopleMemoryType.youAndThem:
assert(personName != null);
return locals.youAndThem(personName!);
return locals.youAndThem(name: personName!);
case PeopleMemoryType.doingSomethingTogether:
assert(activity != null);
assert(personName != null);
@@ -163,16 +163,16 @@ class PeopleMemory extends SmartMemory {
if (personName == null) {
return locals.spotlightOnYourself;
} else if (newAge == null) {
return locals.spotlightOnThem(personName!);
return locals.spotlightOnThem(name: personName!);
} else {
if (isBirthday!) {
return locals.personIsAge(personName!, newAge!);
return locals.personIsAge(name: personName!, age: newAge!);
} else {
return locals.personTurningAge(personName!, newAge!);
return locals.personTurningAge(name: personName!, age: newAge!);
}
}
case PeopleMemoryType.lastTimeYouSawThem:
return locals.lastTimeWithThem(personName!);
return locals.lastTimeWithThem(name: personName!);
}
}
}

View File

@@ -31,21 +31,21 @@ class TimeMemory extends SmartMemory {
if (day != null) {
final dayFormat = DateFormat.MMMd(languageCode).format(day!);
if (yearsAgo != null) {
return "$dayFormat, " + locals.yearsAgo(yearsAgo!);
return "$dayFormat, " + locals.yearsAgo(count: yearsAgo!);
} else {
return locals.throughTheYears(dayFormat);
return locals.throughTheYears(dateFormat: dayFormat);
}
}
if (month != null) {
final monthFormat = DateFormat.MMMM(languageCode).format(month!);
if (yearsAgo != null) {
return "$monthFormat, " + locals.yearsAgo(yearsAgo!);
return "$monthFormat, " + locals.yearsAgo(count: yearsAgo!);
} else {
return locals.throughTheYears(monthFormat);
return locals.throughTheYears(dateFormat: monthFormat);
}
}
if (yearsAgo != null) {
return locals.thisWeekXYearsAgo(yearsAgo!);
return locals.thisWeekXYearsAgo(count: yearsAgo!);
} else {
return locals.thisWeekThroughTheYears;
}

View File

@@ -51,13 +51,13 @@ class TripMemory extends SmartMemory {
assert(locationName != null || tripYear != null);
if (locationName != null) {
if (locationName!.toLowerCase().contains("base")) return locationName!;
return locals.tripToLocation(locationName!);
return locals.tripToLocation(location: locationName!);
}
if (tripYear != null) {
if (tripYear == DateTime.now().year - 1) {
return locals.lastYearsTrip;
} else {
return locals.tripInYear(tripYear!);
return locals.tripInYear(year: tripYear!);
}
}
throw ArgumentError("TripMemory must have a location name or trip year");

View File

@@ -15,7 +15,6 @@ class SimilarFiles {
int get totalSize => files.fold(0, (sum, file) => sum + (file.fileSize ?? 0));
// TODO: lau: check if we're not using this wrong
bool get isEmpty => files.isEmpty;
int get length => files.length;

View File

@@ -6,6 +6,7 @@ import "package:package_info_plus/package_info_plus.dart";
import "package:photos/gateways/entity_gw.dart";
import "package:photos/module/download/manager.dart";
import "package:photos/services/account/billing_service.dart";
import "package:photos/services/collections_service.dart";
import "package:photos/services/entity_service.dart";
import "package:photos/services/filedata/filedata_service.dart";
import "package:photos/services/location_service.dart";
@@ -183,3 +184,9 @@ SmartAlbumsService get smartAlbumsService {
_smartAlbumsService ??= SmartAlbumsService();
return _smartAlbumsService!;
}
CollectionsService? _collectionsService;
CollectionsService get collectionsService {
_collectionsService ??= CollectionsService.instance;
return _collectionsService!;
}

View File

@@ -527,7 +527,7 @@ class UserService {
if (response.statusCode == 200) {
showShortToast(
context,
AppLocalizations.of(context).emailChangedTo(email),
AppLocalizations.of(context).emailChangedTo(newEmail: email),
);
await setEmail(email);
Navigator.of(context).popUntil((route) => route.isFirst);

View File

@@ -7,12 +7,17 @@ import "package:flutter/services.dart";
import "package:photos/utils/ffprobe_util.dart";
class IsolatedFfmpegService {
static Future<Map> runFfmpeg(String command) async {
IsolatedFfmpegService._privateConstructor();
static final IsolatedFfmpegService instance =
IsolatedFfmpegService._privateConstructor();
Future<Map> runFfmpeg(String command) async {
final rootIsolateToken = RootIsolateToken.instance!;
return await Isolate.run<Map>(() => _ffmpegRun(command, rootIsolateToken));
}
static Future<Map> getVideoInfo(String file) async {
Future<Map> getVideoInfo(String file) async {
final rootIsolateToken = RootIsolateToken.instance!;
return await Isolate.run<Map>(() => _getVideoProps(file, rootIsolateToken));
}

View File

@@ -76,13 +76,15 @@ class ComputeController {
_logger.info("Device not healthy or user interacting, denying request.");
return false;
}
bool result = false;
if (ml) {
return _requestML();
result = _requestML();
} else if (stream) {
return _requestStream();
result = _requestStream();
} else {
_logger.severe("No compute request specified, denying request.");
}
_logger.severe("No compute request specified, denying request.");
return false;
return result;
}
bool _requestML() {

View File

@@ -71,6 +71,9 @@ class MLService {
// Listen on ComputeController
Bus.instance.on<ComputeControlEvent>().listen((event) {
if (!flagService.hasGrantedMLConsent) {
if (event.shouldRun) {
VideoPreviewService.instance.queueFiles(duration: Duration.zero);
}
return;
}

View File

@@ -764,7 +764,7 @@ class MemoriesCacheService {
final s = await LanguageService.locals;
await NotificationService.instance.scheduleNotification(
memory.personName != null
? s.wishThemAHappyBirthday(memory.personName!)
? s.wishThemAHappyBirthday(name: memory.personName!)
: s.happyBirthday,
id: memory.id.hashCode,
channelID: "birthday",

View File

@@ -17,6 +17,7 @@ import 'package:photos/events/files_updated_event.dart';
import 'package:photos/events/force_reload_home_gallery_event.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/events/sync_status_update_event.dart';
import "package:photos/main.dart" show isProcessBg;
import 'package:photos/models/device_collection.dart';
import "package:photos/models/file/extensions/file_props.dart";
import 'package:photos/models/file/file.dart';
@@ -31,7 +32,6 @@ import 'package:photos/services/local_file_update_service.dart';
import "package:photos/services/notification_service.dart";
import 'package:photos/services/sync/diff_fetcher.dart';
import 'package:photos/services/sync/sync_service.dart';
import "package:photos/services/video_preview_service.dart";
import 'package:photos/utils/file_uploader.dart';
import 'package:photos/utils/file_util.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -132,13 +132,9 @@ class RemoteSyncService {
}
if (
// Only Uploading Previews in fg to prevent heating issues
AppLifecycleService.instance.isForeground &&
// if ML is enabled the MLService will queue when ML is done
!flagService.hasGrantedMLConsent) {
fileDataService.syncFDStatus().then((_) {
VideoPreviewService.instance.queueFiles();
}).ignore();
// We don't need syncFDStatus here if in background
!isProcessBg) {
fileDataService.syncFDStatus().ignore();
}
final filesToBeUploaded = await _getFilesToBeUploaded();
@@ -377,7 +373,7 @@ class RemoteSyncService {
localIDsToSync.removeAll(alreadyClaimedLocalIDs);
if (alreadyClaimedLocalIDs.isNotEmpty && !_hasCleanupStaleEntry) {
try {
await _db.removeQueuedLocalFiles(alreadyClaimedLocalIDs);
await _db.removeQueuedLocalFiles(alreadyClaimedLocalIDs, ownerID);
} catch (e, s) {
_logger.severe("removeQueuedLocalFiles failed", e, s);
}

View File

@@ -15,19 +15,21 @@ import "package:path_provider/path_provider.dart";
import "package:photos/core/cache/video_cache_manager.dart";
import "package:photos/core/configuration.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/core/network/network.dart";
import 'package:photos/db/files_db.dart';
import "package:photos/db/upload_locks_db.dart";
import "package:photos/events/video_preview_state_changed_event.dart";
import "package:photos/events/video_streaming_changed.dart";
import 'package:photos/generated/intl/app_localizations.dart';
import "package:photos/models/base/id.dart";
import "package:photos/models/ffmpeg/ffprobe_props.dart";
import "package:photos/models/file/file.dart";
import "package:photos/models/file/file_type.dart";
import "package:photos/models/metadata/file_magic.dart";
import "package:photos/models/preview/playlist_data.dart";
import "package:photos/models/preview/preview_item.dart";
import "package:photos/models/preview/preview_item_status.dart";
import "package:photos/service_locator.dart";
import "package:photos/services/collections_service.dart";
import "package:photos/services/file_magic_service.dart";
import "package:photos/services/filedata/model/file_data.dart";
import "package:photos/services/isolated_ffmpeg_service.dart";
import "package:photos/ui/notification/toast.dart";
@@ -36,7 +38,6 @@ import "package:photos/utils/file_key.dart";
import "package:photos/utils/file_util.dart";
import "package:photos/utils/gzip.dart";
import "package:photos/utils/network_util.dart";
import "package:shared_preferences/shared_preferences.dart";
const _maxRetryCount = 3;
@@ -47,39 +48,54 @@ class VideoPreviewService {
final int _maxPreviewSizeLimitForCache = 50 * 1024 * 1024; // 50 MB
Set<int>? _failureFiles;
bool _hasQueuedFile = false;
bool get _hasQueuedFile => fileQueue.isNotEmpty;
VideoPreviewService._privateConstructor();
VideoPreviewService._privateConstructor()
: serviceLocator = ServiceLocator.instance,
filesDB = FilesDB.instance,
uploadLocksDB = UploadLocksDB.instance,
ffmpegService = IsolatedFfmpegService.instance,
fileMagicService = FileMagicService.instance,
cacheManager = DefaultCacheManager(),
videoCacheManager = VideoCacheManager.instance,
config = Configuration.instance;
VideoPreviewService(
this.config,
this.serviceLocator,
this.filesDB,
this.uploadLocksDB,
this.fileMagicService,
this.ffmpegService,
this.cacheManager,
this.videoCacheManager,
);
static final VideoPreviewService instance =
VideoPreviewService._privateConstructor();
final cacheManager = DefaultCacheManager();
final videoCacheManager = VideoCacheManager.instance;
int uploadingFileId = -1;
final _enteDio = NetworkClient.instance.enteDio;
final _nonEnteDio = NetworkClient.instance.getDio();
final CollectionsService collectionsService = CollectionsService.instance;
final Configuration config;
final ServiceLocator serviceLocator;
final FilesDB filesDB;
final UploadLocksDB uploadLocksDB;
final FileMagicService fileMagicService;
final IsolatedFfmpegService ffmpegService;
final DefaultCacheManager cacheManager;
final CacheManager videoCacheManager;
void init(SharedPreferences prefs) {
_prefs = prefs;
}
late final SharedPreferences _prefs;
static const String _videoStreamingEnabled = "videoStreamingEnabled";
bool get isVideoStreamingEnabled {
return _prefs.getBool(_videoStreamingEnabled) ?? false;
return serviceLocator.prefs.getBool(_videoStreamingEnabled) ?? false;
}
Future<void> setIsVideoStreamingEnabled(bool value) async {
_prefs.setBool(_videoStreamingEnabled, value).ignore();
serviceLocator.prefs.setBool(_videoStreamingEnabled, value).ignore();
Bus.instance.fire(VideoStreamingChanged());
if (isVideoStreamingEnabled) {
await fileDataService.syncFDStatus();
queueFiles(duration: Duration.zero);
} else {
clearQueue();
@@ -87,13 +103,81 @@ class VideoPreviewService {
}
void clearQueue() {
// Fire events for all items being cleared
for (final entry in _items.entries) {
_fireVideoPreviewStateChange(entry.key, PreviewItemStatus.uploaded);
}
fileQueue.clear();
_items.clear();
_hasQueuedFile = false;
}
void _fireVideoPreviewStateChange(int fileId, PreviewItemStatus status) {
Bus.instance.fire(VideoPreviewStateChangedEvent(fileId, status));
}
// Return value indicates file was successfully added to queue or not
Future<bool> addToManualQueue(EnteFile file, String queueType) async {
if (file.uploadedFileID == null) return false;
// Check if already in queue
final bool alreadyInQueue =
await uploadLocksDB.isInStreamQueue(file.uploadedFileID!);
if (alreadyInQueue) {
return false; // Indicates file was already in queue
}
// Add to persistent database queue
await uploadLocksDB.addToStreamQueue(file.uploadedFileID!, queueType);
// Start processing if not already processing
if (uploadingFileId < 0) {
queueFiles(duration: Duration.zero);
} else {
_items[file.uploadedFileID!] = PreviewItem(
status: PreviewItemStatus.inQueue,
file: file,
retryCount: 0,
collectionID: file.collectionID ?? 0,
);
_fireVideoPreviewStateChange(
file.uploadedFileID!,
PreviewItemStatus.inQueue,
);
fileQueue[file.uploadedFileID!] = file;
}
return true;
}
bool isCurrentlyProcessing(int? uploadedFileID) {
if (uploadedFileID == null) return false;
return uploadingFileId == uploadedFileID;
}
Future<bool> _isRecreateOperation(EnteFile file) async {
if (file.uploadedFileID == null) return false;
try {
// Check database directly instead of relying on in-memory _manualQueueFiles
// which might not be populated yet
final manualQueueFiles = await uploadLocksDB.getStreamQueue();
final queueType = manualQueueFiles[file.uploadedFileID!];
return queueType == 'recreate';
} catch (_) {
return false;
}
}
Future<void> _ensurePreviewIdsInitialized() async {
// Ensure fileDataService previewIds is initialized before using it
if (fileDataService.previewIds.isEmpty) {
await fileDataService.syncFDStatus();
}
}
Future<bool> isSharedFileStreamble(EnteFile file) async {
try {
await _ensurePreviewIdsInitialized();
if (fileDataService.previewIds.containsKey(file.uploadedFileID)) {
return true;
}
@@ -104,6 +188,66 @@ class VideoPreviewService {
}
}
Future<List<EnteFile>> _getFiles({
DateTime? beginDate,
bool onlyFilesWithLocalId = true,
}) async {
return await filesDB.getStreamingEligibleVideoFiles(
beginDate: beginDate,
userID: config.getUserID()!,
onlyFilesWithLocalId: onlyFilesWithLocalId,
);
}
Future<double> calcStatus(
List<EnteFile> files,
Map<int, PreviewInfo> previewIds,
) async {
// This is the total video files that have streams
final Set<int> processed = previewIds.keys.toSet();
// Total: Total Remote video files owned - skipped video files
// + processed videos (any platform)
final Set<int> total = {...processed};
for (final file in files) {
// skipped -> don't add
if (file.pubMagicMetadata?.sv == 1) {
continue;
}
// Include the file to total set
total.add(file.uploadedFileID!);
}
// If total is empty then mark all as processed else compute the ratio
// of processed files and total remote video files
// netProcessedItems = processed / total
final double netProcessedItems =
total.isEmpty ? 1 : (processed.length / total.length).clamp(0, 1);
// Store the data and return it
final status = netProcessedItems;
return status;
}
Future<double> getStatus() async {
try {
await _ensurePreviewIdsInitialized();
// This will get us all the video files that are present on remote
// and also that could be / have been skipped due to device
// limitations
final files = await _getFiles(
beginDate: null,
onlyFilesWithLocalId: false,
);
return calcStatus(files, fileDataService.previewIds);
} catch (e, s) {
_logger.severe('Error getting Streaming status', e, s);
rethrow;
}
}
Future<void> chunkAndUploadVideo(
BuildContext? ctx,
EnteFile enteFile, [
@@ -126,10 +270,14 @@ class VideoPreviewService {
return;
}
try {
// check if playlist already exist
if (await getPlaylist(enteFile) != null) {
// check if playlist already exist, but skip this check for 'recreate' operations
final isRecreateOperation = await _isRecreateOperation(enteFile);
if (!isRecreateOperation && await getPlaylist(enteFile) != null) {
if (ctx != null && ctx.mounted) {
showShortToast(ctx, 'Video preview already exists');
showShortToast(
ctx,
AppLocalizations.of(ctx).videoPreviewAlreadyExists,
);
}
removeFile = true;
return;
@@ -143,6 +291,9 @@ class VideoPreviewService {
return;
}
}
_logger.info(
"Starting video preview generation for ${enteFile.displayName}",
);
// elimination case for <=10 MB with H.264
var (props, result, file) = await _checkFileForPreviewCreation(enteFile);
if (result) {
@@ -162,6 +313,10 @@ class VideoPreviewService {
: _items[enteFile.uploadedFileID!]?.retryCount ?? 0,
collectionID: enteFile.collectionID ?? 0,
);
_fireVideoPreviewStateChange(
enteFile.uploadedFileID!,
PreviewItemStatus.inQueue,
);
fileQueue[enteFile.uploadedFileID!] = enteFile;
return;
}
@@ -175,6 +330,10 @@ class VideoPreviewService {
forceUpload ? 0 : _items[enteFile.uploadedFileID!]?.retryCount ?? 0,
collectionID: enteFile.collectionID ?? 0,
);
_fireVideoPreviewStateChange(
enteFile.uploadedFileID!,
PreviewItemStatus.compressing,
);
// get file
file ??= await getFile(enteFile, isOrigin: true);
@@ -187,8 +346,9 @@ class VideoPreviewService {
props ??= await getVideoPropsAsync(file);
final fileSize = enteFile.fileSize ?? file.lengthSync();
final videoData = List.from(props?.propData?["streams"] ?? [])
.firstWhereOrNull((e) => e["type"] == "video");
final videoData = List.from(
props?.propData?["streams"] ?? [],
).firstWhereOrNull((e) => e["type"] == "video");
final codec = videoData["codec_name"]?.toString().toLowerCase();
final isH264 = codec?.contains("h264") ?? false;
@@ -203,7 +363,7 @@ class VideoPreviewService {
(colorTransfer == "smpte2084" || colorTransfer == "arib-std-b67");
// create temp file & directory for preview generation
final String tempDir = Configuration.instance.getTempDirectory();
final String tempDir = config.getTempDirectory();
final String prefix =
"${tempDir}_${enteFile.uploadedFileID}_${newID("pv")}";
Directory(prefix).createSync();
@@ -214,8 +374,10 @@ class VideoPreviewService {
keyfile.writeAsBytesSync(key.bytes);
final keyinfo = File('$prefix/mykey.keyinfo');
keyinfo.writeAsStringSync("data:text/plain;base64,${key.base64}\n"
"${keyfile.path}\n");
keyinfo.writeAsStringSync(
"data:text/plain;base64,${key.base64}\n"
"${keyfile.path}\n",
);
_logger.info(
'Generating HLS Playlist ${enteFile.displayName} at $prefix/output.m3u8',
@@ -267,19 +429,19 @@ class VideoPreviewService {
_logger.info(command);
final playlistGenResult = await IsolatedFfmpegService.runFfmpeg(
final playlistGenResult = await ffmpegService
.runFfmpeg(
// input file path
'-i "${file.path}" ' +
// main params for streaming
command +
// output file path
'$prefix/output.m3u8',
).onError(
(error, stackTrace) {
_logger.warning("FFmpeg command failed", error, stackTrace);
return {};
},
);
)
.onError((error, stackTrace) {
_logger.warning("FFmpeg command failed", error, stackTrace);
return {};
});
final playlistGenReturnCode = playlistGenResult["returnCode"] as int?;
@@ -294,6 +456,10 @@ class VideoPreviewService {
collectionID: enteFile.collectionID ?? 0,
retryCount: _items[enteFile.uploadedFileID!]?.retryCount ?? 0,
);
_fireVideoPreviewStateChange(
enteFile.uploadedFileID!,
PreviewItemStatus.uploading,
);
_logger.info('Playlist Generated ${enteFile.displayName}');
@@ -305,18 +471,18 @@ class VideoPreviewService {
objectSize = result.$2;
// Fetch resolution of generated stream by decrypting a single frame
final playlistFrameResult = await IsolatedFfmpegService.runFfmpeg(
final playlistFrameResult = await ffmpegService
.runFfmpeg(
'-allowed_extensions ALL -i "$prefix/output.m3u8" -frames:v 1 -c copy "$prefix/frame.ts"',
).onError(
(error, stackTrace) {
_logger.warning(
"FFmpeg command failed for frame",
error,
stackTrace,
);
return {};
},
);
)
.onError((error, stackTrace) {
_logger.warning(
"FFmpeg command failed for frame",
error,
stackTrace,
);
return {};
});
final playlistFrameReturnCode =
playlistFrameResult["returnCode"] as int?;
int? width, height;
@@ -373,11 +539,14 @@ class VideoPreviewService {
retryCount: _items[enteFile.uploadedFileID!]!.retryCount,
collectionID: enteFile.collectionID ?? 0,
);
_fireVideoPreviewStateChange(
enteFile.uploadedFileID!,
PreviewItemStatus.uploaded,
);
_removeFromLocks(enteFile).ignore();
Directory(prefix).delete(recursive: true).ignore();
}
} finally {
computeController.releaseCompute(stream: true);
if (error != null) {
_retryFile(enteFile, error);
} else if (removeFile) {
@@ -397,6 +566,8 @@ class VideoPreviewService {
final file = entry.value;
fileQueue.remove(entry.key);
await chunkAndUploadVideo(ctx, file);
} else {
computeController.releaseCompute(stream: true);
}
}
}
@@ -404,16 +575,27 @@ class VideoPreviewService {
Future<void> _removeFromLocks(EnteFile enteFile) async {
final bool isFailurePresent =
_failureFiles?.contains(enteFile.uploadedFileID!) ?? false;
final bool isInManualQueue =
await uploadLocksDB.isInStreamQueue(enteFile.uploadedFileID!);
if (isFailurePresent) {
await UploadLocksDB.instance
.deleteStreamUploadErrorEntry(enteFile.uploadedFileID!);
await uploadLocksDB.deleteStreamUploadErrorEntry(
enteFile.uploadedFileID!,
);
_failureFiles?.remove(enteFile.uploadedFileID!);
}
if (isInManualQueue) {
await uploadLocksDB.removeFromStreamQueue(enteFile.uploadedFileID!);
}
}
void _removeFile(EnteFile enteFile) {
_items.remove(enteFile.uploadedFileID!);
final fileId = enteFile.uploadedFileID!;
_items.remove(fileId);
// Note: Using 'uploaded' status as there's no 'removed' status in PreviewItemStatus
// This indicates the item has been successfully processed and removed from queue
_fireVideoPreviewStateChange(fileId, PreviewItemStatus.uploaded);
}
void _retryFile(EnteFile enteFile, Object error) {
@@ -424,6 +606,10 @@ class VideoPreviewService {
retryCount: _items[enteFile.uploadedFileID!]!.retryCount + 1,
collectionID: enteFile.collectionID ?? 0,
);
_fireVideoPreviewStateChange(
enteFile.uploadedFileID!,
PreviewItemStatus.retry,
);
fileQueue[enteFile.uploadedFileID!] = enteFile;
} else {
_items[enteFile.uploadedFileID!] = PreviewItem(
@@ -433,17 +619,21 @@ class VideoPreviewService {
collectionID: enteFile.collectionID ?? 0,
error: error,
);
_fireVideoPreviewStateChange(
enteFile.uploadedFileID!,
PreviewItemStatus.failed,
);
final bool isFailurePresent =
_failureFiles?.contains(enteFile.uploadedFileID!) ?? false;
if (isFailurePresent) {
UploadLocksDB.instance.appendStreamEntry(
uploadLocksDB.appendStreamEntry(
enteFile.uploadedFileID!,
error.toString(),
);
} else {
UploadLocksDB.instance.appendStreamEntry(
uploadLocksDB.appendStreamEntry(
enteFile.uploadedFileID!,
error.toString(),
);
@@ -474,7 +664,7 @@ class VideoPreviewService {
},
encryptionKey,
);
final _ = await _enteDio.put(
final _ = await serviceLocator.enteDio.put(
"/files/video-data",
data: {
"fileID": file.uploadedFileID!,
@@ -493,7 +683,7 @@ class VideoPreviewService {
Future<(String, int)> _uploadPreviewVideo(EnteFile file, File preview) async {
_logger.info("Pushing preview for $file");
try {
final response = await _enteDio.get(
final response = await serviceLocator.enteDio.get(
"/files/data/preview-upload-url",
queryParameters: {
"fileID": file.uploadedFileID!,
@@ -503,14 +693,10 @@ class VideoPreviewService {
final uploadURL = response.data["url"];
final String objectID = response.data["objectID"];
final objectSize = preview.lengthSync();
final _ = await _enteDio.put(
final _ = await serviceLocator.enteDio.put(
uploadURL,
data: preview.openRead(),
options: Options(
headers: {
Headers.contentLengthHeader: objectSize,
},
),
options: Options(headers: {Headers.contentLengthHeader: objectSize}),
);
return (objectID, objectSize);
} catch (e) {
@@ -538,6 +724,7 @@ class VideoPreviewService {
Future<PlaylistData?> _getPlaylist(EnteFile file) async {
_logger.info("Getting playlist for $file");
int? width, height, size;
try {
late final String objectID;
final PreviewInfo? previewInfo =
@@ -553,8 +740,9 @@ class VideoPreviewService {
objectID = previewInfo.objectId;
}
final FileInfo? playlistCache =
await cacheManager.getFileFromCache(_getCacheKey(objectID));
final FileInfo? playlistCache = await cacheManager.getFileFromCache(
_getCacheKey(objectID),
);
final detailsCache = await cacheManager.getFileFromCache(
_getDetailsCacheKey(objectID),
);
@@ -576,9 +764,7 @@ class VideoPreviewService {
unawaited(
cacheManager.putFile(
_getCacheKey(objectID),
Uint8List.fromList(
(playlistData["playlist"] as String).codeUnits,
),
Uint8List.fromList((playlistData["playlist"] as String).codeUnits),
),
);
unawaited(
@@ -594,8 +780,9 @@ class VideoPreviewService {
),
);
}
final videoFile = (await videoCacheManager
.getFileFromCache(_getVideoPreviewKey(objectID)))
final videoFile = (await videoCacheManager.getFileFromCache(
_getVideoPreviewKey(objectID),
))
?.file;
if (videoFile == null) {
previewURLResult = previewURLResult ?? await _getPreviewUrl(file);
@@ -607,21 +794,26 @@ class VideoPreviewService {
),
);
}
finalPlaylist =
finalPlaylist.replaceAll('\noutput.ts', '\n${previewURLResult.$1}');
finalPlaylist = finalPlaylist.replaceAll(
'\noutput.ts',
'\n${previewURLResult.$1}',
);
} else {
finalPlaylist =
finalPlaylist.replaceAll('\noutput.ts', '\n${videoFile.path}');
finalPlaylist = finalPlaylist.replaceAll(
'\noutput.ts',
'\n${videoFile.path}',
);
}
final tempDir = await getTemporaryDirectory();
final playlistFile = File("${tempDir.path}/${file.uploadedFileID}.m3u8");
await playlistFile.writeAsString(finalPlaylist);
final String log = (StringBuffer()
..write("[CACHE-STATUS] ")
..write("Video: ${videoFile != null ? '' : ''} | ")
..write("Details: ${detailsCache != null ? '' : ''} | ")
..write("Playlist: ${playlistCache != null ? '' : ''}"))
.toString();
final String log = (
StringBuffer()
..write("[CACHE-STATUS] ")
..write("Video: ${videoFile != null ? '' : ''} | ")
..write("Details: ${detailsCache != null ? '' : ''} | ")
..write("Playlist: ${playlistCache != null ? '' : ''}"),
).toString();
_logger.info("Mapped playlist to ${playlistFile.path}, $log");
final data = PlaylistData(
preview: playlistFile,
@@ -631,11 +823,7 @@ class VideoPreviewService {
durationInSeconds: parseDurationFromHLS(finalPlaylist),
);
if (shouldAppendPreview) {
fileDataService.appendPreview(
file.uploadedFileID!,
objectID,
size!,
);
fileDataService.appendPreview(file.uploadedFileID!, objectID, size!);
}
return data;
} catch (_) {
@@ -646,24 +834,19 @@ class VideoPreviewService {
Future<Map<String, dynamic>> _getPlaylistData(EnteFile file) async {
late Response<dynamic> response;
if (collectionsService.isSharedPublicLink(file.collectionID!)) {
response = await _nonEnteDio.get(
"${Configuration.instance.getHttpEndpoint()}/public-collection/files/data/fetch/",
queryParameters: {
"fileID": file.uploadedFileID,
"type": "vid_preview",
},
response = await serviceLocator.nonEnteDio.get(
"${config.getHttpEndpoint()}/public-collection/files/data/fetch/",
queryParameters: {"fileID": file.uploadedFileID, "type": "vid_preview"},
options: Options(
headers:
collectionsService.publicCollectionHeaders(file.collectionID!),
headers: collectionsService.publicCollectionHeaders(
file.collectionID!,
),
),
);
} else {
response = await _enteDio.get(
response = await serviceLocator.enteDio.get(
"/files/data/fetch/",
queryParameters: {
"fileID": file.uploadedFileID,
"type": "vid_preview",
},
queryParameters: {"fileID": file.uploadedFileID, "type": "vid_preview"},
);
}
final encryptedData = response.data["data"]["encryptedData"];
@@ -683,10 +866,7 @@ class VideoPreviewService {
for (final line in lines) {
if (line.startsWith("#EXTINF:")) {
// Extract duration value (e.g., "#EXTINF:2.400000," → "2.400000")
final durationStr = line.substring(
8,
line.length - 1,
);
final durationStr = line.substring(8, line.length - 1);
final duration = double.tryParse(durationStr);
if (duration != null) {
totalDuration += duration;
@@ -700,21 +880,22 @@ class VideoPreviewService {
try {
late String url;
if (collectionsService.isSharedPublicLink(file.collectionID!)) {
final response = await _nonEnteDio.get(
"${Configuration.instance.getHttpEndpoint()}/public-collection/files/data/preview",
final response = await serviceLocator.nonEnteDio.get(
"${config.getHttpEndpoint()}/public-collection/files/data/preview",
queryParameters: {
"fileID": file.uploadedFileID,
"type":
file.fileType == FileType.video ? "vid_preview" : "img_preview",
},
options: Options(
headers:
collectionsService.publicCollectionHeaders(file.collectionID!),
headers: collectionsService.publicCollectionHeaders(
file.collectionID!,
),
),
);
url = (response.data["url"] as String);
} else {
final response = await _enteDio.get(
final response = await serviceLocator.enteDio.get(
"/files/data/preview",
queryParameters: {
"fileID": file.uploadedFileID,
@@ -739,9 +920,7 @@ class VideoPreviewService {
EnteFile enteFile,
) async {
if ((enteFile.pubMagicMetadata?.sv ?? 0) == 1) {
_logger.info(
"Skip Preview due to sv=1 for ${enteFile.displayName}",
);
_logger.info("Skip Preview due to sv=1 for ${enteFile.displayName}");
return (null, true, null);
}
if (enteFile.fileSize == null || enteFile.duration == null) {
@@ -753,9 +932,7 @@ class VideoPreviewService {
final int size = enteFile.fileSize!;
final int duration = enteFile.duration!;
if (size >= 500 * 1024 * 1024 || duration > 60) {
_logger.info(
"Skip Preview due to size: $size or duration: $duration",
);
_logger.info("Skip Preview due to size: $size or duration: $duration");
return (null, true, null);
}
FFProbeProps? props;
@@ -767,8 +944,9 @@ class VideoPreviewService {
file = await getFile(enteFile, isOrigin: true);
if (file != null) {
props = await getVideoPropsAsync(file);
final videoData = List.from(props?.propData?["streams"] ?? [])
.firstWhereOrNull((e) => e["type"] == "video");
final videoData = List.from(
props?.propData?["streams"] ?? [],
).firstWhereOrNull((e) => e["type"] == "video");
final codec = videoData["codec_name"]?.toString().toLowerCase();
skipFile = codec?.contains("h264") ?? false;
@@ -776,6 +954,10 @@ class VideoPreviewService {
_logger.info(
"[init] Ignoring file ${enteFile.displayName} for preview due to codec",
);
await fileMagicService.updatePublicMagicMetadata(
[enteFile],
{streamVersionKey: 1},
);
return (props, skipFile, file);
}
}
@@ -787,35 +969,96 @@ class VideoPreviewService {
}
// generate stream for all files after cutoff date
Future<void> _putFilesForPreviewCreation([bool updateInit = false]) async {
Future<void> _putFilesForPreviewCreation() async {
if (!isVideoStreamingEnabled || !await canUseHighBandwidth()) return;
if (updateInit) _hasQueuedFile = true;
Map<int, String> failureFiles = {};
Map<int, String> manualQueueFiles = {};
try {
failureFiles = await UploadLocksDB.instance.getStreamUploadError();
failureFiles = await uploadLocksDB.getStreamUploadError();
_failureFiles = {...failureFiles.keys};
manualQueueFiles = await uploadLocksDB.getStreamQueue();
// handle case when failures are already previewed
for (final failure in _failureFiles!) {
if (_items.containsKey(failure)) {
UploadLocksDB.instance.deleteStreamUploadErrorEntry(failure).ignore();
uploadLocksDB.deleteStreamUploadErrorEntry(failure).ignore();
}
}
// handle case when manual queue items are already previewed (for 'create' type only)
for (final queueItem in manualQueueFiles.keys) {
final queueType = manualQueueFiles[queueItem];
final hasPreview = fileDataService.previewIds[queueItem] != null;
if (hasPreview && queueType == 'create') {
// Remove from queue only if it's a 'create' type and preview exists
await uploadLocksDB.removeFromStreamQueue(queueItem);
}
}
// Refresh manual queue after cleanup
manualQueueFiles = await uploadLocksDB.getStreamQueue();
} catch (_) {}
final files = await FilesDB.instance.getAllFilesAfterDate(
fileType: FileType.video,
beginDate: DateTime.now().subtract(
const Duration(days: 30),
),
userID: Configuration.instance.getUserID()!,
final files = await _getFiles(
beginDate: DateTime.now().subtract(const Duration(days: 60)),
onlyFilesWithLocalId: true,
);
final previewIds = fileDataService.previewIds;
_logger.info(
"[init] Found ${files.length} files in last 60 days, ${manualQueueFiles.length} manual queue files: ${manualQueueFiles.keys.toList()}",
);
final previewIds = fileDataService.previewIds;
final allFiles =
files.where((file) => previewIds[file.uploadedFileID] == null).toList();
// Add manual queue files first (they have priority)
for (final queueFileId in manualQueueFiles.keys) {
final queueType = manualQueueFiles[queueFileId] ?? 'create';
final hasPreview = previewIds[queueFileId] != null;
// For create, only add if no preview exists
if (queueType == 'create' && hasPreview) {
_logger.info(
"[manual-queue] Skipping file $queueFileId (type=$queueType, hasPreview=$hasPreview)",
);
continue;
}
// First try to find the file in the 60-day list
var queueFile =
files.firstWhereOrNull((f) => f.uploadedFileID == queueFileId);
// If not found in 60-day list, fetch it individually
queueFile ??=
await filesDB.getAnyUploadedFile(queueFileId).catchError((e) => null);
if (queueFile == null) {
await uploadLocksDB
.removeFromStreamQueue(queueFileId)
.catchError((e) {});
continue;
}
_items[queueFile.uploadedFileID!] = PreviewItem(
status: PreviewItemStatus.inQueue,
file: queueFile,
collectionID: queueFile.collectionID ?? 0,
);
_fireVideoPreviewStateChange(
queueFile.uploadedFileID!,
PreviewItemStatus.inQueue,
);
fileQueue[queueFile.uploadedFileID!] = queueFile;
}
// Then add regular files that need processing
final allFiles = files
.where(
(file) =>
previewIds[file.uploadedFileID] == null &&
!manualQueueFiles.containsKey(file.uploadedFileID),
)
.toList();
// set all video status to in queue
var n = allFiles.length, i = 0;
@@ -831,6 +1074,10 @@ class VideoPreviewService {
retryCount: _maxRetryCount,
error: failureFiles[enteFile.uploadedFileID!],
);
_fireVideoPreviewStateChange(
enteFile.uploadedFileID!,
PreviewItemStatus.failed,
);
}
if (isFailure) {
_logger.info(
@@ -846,25 +1093,29 @@ class VideoPreviewService {
file: enteFile,
collectionID: enteFile.collectionID ?? 0,
);
_fireVideoPreviewStateChange(
enteFile.uploadedFileID!,
PreviewItemStatus.inQueue,
);
fileQueue[enteFile.uploadedFileID!] = enteFile;
i++;
}
if (allFiles.isEmpty) {
final totalFiles = fileQueue.length;
if (totalFiles == 0) {
_logger.info("[init] No preview to cache");
return;
}
_logger.info("[init] Processing ${allFiles.length} items for streaming");
_logger.info(
"[init] Processing $totalFiles items for streaming (${manualQueueFiles.length} manual requested, ${fileQueue.length} queued, ${allFiles.length} regular)",
);
// take first file and put it for stream generation
final file = allFiles.removeAt(0);
for (final enteFile in allFiles) {
if (_items.containsKey(enteFile.uploadedFileID!)) {
continue;
}
fileQueue[enteFile.uploadedFileID!] = enteFile;
}
final entry = fileQueue.entries.first;
final file = entry.value;
fileQueue.remove(entry.key);
chunkAndUploadVideo(null, file).ignore();
}
@@ -874,12 +1125,14 @@ class VideoPreviewService {
}
void queueFiles({Duration duration = const Duration(seconds: 5)}) {
Future.delayed(duration, () {
if (!_hasQueuedFile && _allowStream()) {
_putFilesForPreviewCreation(true).catchError((_) {
_hasQueuedFile = false;
});
}
Future.delayed(duration, () async {
if (_hasQueuedFile) return;
final isStreamAllowed = _allowStream();
if (!isStreamAllowed) return;
await _ensurePreviewIdsInitialized();
await _putFilesForPreviewCreation();
});
}
}

View File

@@ -1,12 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: require_trailing_commas
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:photos/src/rust/frb_generated.dart';
String greet({required String name}) =>
RustLib.instance.api.crateApiSimpleGreet(name: name);

View File

@@ -1,57 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: require_trailing_commas
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:photos/src/rust/frb_generated.dart';
// These functions are ignored because they are not marked as `pub`: `ensure_capacity`, `save_index`
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<VectorDB>>
abstract class VectorDb implements RustOpaqueInterface {
Future<void> addVector({required BigInt key, required List<double> vector});
Future<void> bulkAddVectors(
{required Uint64List keys, required List<Float32List> vectors});
Future<List<Float32List>> bulkGetVectors({required Uint64List keys});
Future<BigInt> bulkRemoveVectors({required Uint64List keys});
Future<(Uint64List, List<Uint64List>, List<Float32List>)> bulkSearchKeys(
{required Uint64List potentialKeys,
required BigInt count,
required bool exact});
Future<(List<Uint64List>, List<Float32List>)> bulkSearchVectors(
{required List<Float32List> queries,
required BigInt count,
required bool exact});
/// Check if a vector with the given key exists in the index.
/// `true` if the index contains the vector with the given key, `false` otherwise.
Future<bool> containsVector({required BigInt key});
Future<void> deleteIndex();
Future<(BigInt, BigInt, BigInt, BigInt, BigInt, BigInt, BigInt)>
getIndexStats();
Future<Float32List> getVector({required BigInt key});
factory VectorDb({required String filePath, required BigInt dimensions}) =>
RustLib.instance.api.crateApiUsearchApiVectorDbNew(
filePath: filePath, dimensions: dimensions);
Future<BigInt> removeVector({required BigInt key});
Future<void> resetIndex();
Future<(Uint64List, Float32List)> searchVectors(
{required List<double> query,
required BigInt count,
required bool exact});
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,315 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: require_trailing_commas
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
import 'dart:async';
import 'dart:convert';
import 'dart:ffi' as ffi;
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
import 'package:photos/src/rust/api/simple.dart';
import 'package:photos/src/rust/api/usearch_api.dart';
import 'package:photos/src/rust/frb_generated.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({
required super.handler,
required super.wire,
required super.generalizedFrbRustBinding,
required super.portManager,
});
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_VectorDbPtr => wire
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDBPtr;
@protected
VectorDb
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB(
dynamic raw);
@protected
VectorDb
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB(
dynamic raw);
@protected
VectorDb
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB(
dynamic raw);
@protected
String dco_decode_String(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
double dco_decode_f_32(dynamic raw);
@protected
List<Float32List> dco_decode_list_list_prim_f_32_strict(dynamic raw);
@protected
List<Uint64List> dco_decode_list_list_prim_u_64_strict(dynamic raw);
@protected
List<double> dco_decode_list_prim_f_32_loose(dynamic raw);
@protected
Float32List dco_decode_list_prim_f_32_strict(dynamic raw);
@protected
Uint64List dco_decode_list_prim_u_64_strict(dynamic raw);
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
(List<Uint64List>, List<Float32List>)
dco_decode_record_list_list_prim_u_64_strict_list_list_prim_f_32_strict(
dynamic raw);
@protected
(
Uint64List,
List<Uint64List>,
List<Float32List>
) dco_decode_record_list_prim_u_64_strict_list_list_prim_u_64_strict_list_list_prim_f_32_strict(
dynamic raw);
@protected
(
Uint64List,
Float32List
) dco_decode_record_list_prim_u_64_strict_list_prim_f_32_strict(dynamic raw);
@protected
(BigInt, BigInt, BigInt, BigInt, BigInt, BigInt, BigInt)
dco_decode_record_usize_usize_usize_usize_usize_usize_usize(dynamic raw);
@protected
BigInt dco_decode_u_64(dynamic raw);
@protected
int dco_decode_u_8(dynamic raw);
@protected
void dco_decode_unit(dynamic raw);
@protected
BigInt dco_decode_usize(dynamic raw);
@protected
VectorDb
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB(
SseDeserializer deserializer);
@protected
VectorDb
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB(
SseDeserializer deserializer);
@protected
VectorDb
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB(
SseDeserializer deserializer);
@protected
String sse_decode_String(SseDeserializer deserializer);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
double sse_decode_f_32(SseDeserializer deserializer);
@protected
List<Float32List> sse_decode_list_list_prim_f_32_strict(
SseDeserializer deserializer);
@protected
List<Uint64List> sse_decode_list_list_prim_u_64_strict(
SseDeserializer deserializer);
@protected
List<double> sse_decode_list_prim_f_32_loose(SseDeserializer deserializer);
@protected
Float32List sse_decode_list_prim_f_32_strict(SseDeserializer deserializer);
@protected
Uint64List sse_decode_list_prim_u_64_strict(SseDeserializer deserializer);
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
(List<Uint64List>, List<Float32List>)
sse_decode_record_list_list_prim_u_64_strict_list_list_prim_f_32_strict(
SseDeserializer deserializer);
@protected
(
Uint64List,
List<Uint64List>,
List<Float32List>
) sse_decode_record_list_prim_u_64_strict_list_list_prim_u_64_strict_list_list_prim_f_32_strict(
SseDeserializer deserializer);
@protected
(Uint64List, Float32List)
sse_decode_record_list_prim_u_64_strict_list_prim_f_32_strict(
SseDeserializer deserializer);
@protected
(BigInt, BigInt, BigInt, BigInt, BigInt, BigInt, BigInt)
sse_decode_record_usize_usize_usize_usize_usize_usize_usize(
SseDeserializer deserializer);
@protected
BigInt sse_decode_u_64(SseDeserializer deserializer);
@protected
int sse_decode_u_8(SseDeserializer deserializer);
@protected
void sse_decode_unit(SseDeserializer deserializer);
@protected
BigInt sse_decode_usize(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB(
VectorDb self, SseSerializer serializer);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB(
VectorDb self, SseSerializer serializer);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB(
VectorDb self, SseSerializer serializer);
@protected
void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_f_32(double self, SseSerializer serializer);
@protected
void sse_encode_list_list_prim_f_32_strict(
List<Float32List> self, SseSerializer serializer);
@protected
void sse_encode_list_list_prim_u_64_strict(
List<Uint64List> self, SseSerializer serializer);
@protected
void sse_encode_list_prim_f_32_loose(
List<double> self, SseSerializer serializer);
@protected
void sse_encode_list_prim_f_32_strict(
Float32List self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_64_strict(
Uint64List self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self, SseSerializer serializer);
@protected
void sse_encode_record_list_list_prim_u_64_strict_list_list_prim_f_32_strict(
(List<Uint64List>, List<Float32List>) self, SseSerializer serializer);
@protected
void
sse_encode_record_list_prim_u_64_strict_list_list_prim_u_64_strict_list_list_prim_f_32_strict(
(Uint64List, List<Uint64List>, List<Float32List>) self,
SseSerializer serializer);
@protected
void sse_encode_record_list_prim_u_64_strict_list_prim_f_32_strict(
(Uint64List, Float32List) self, SseSerializer serializer);
@protected
void sse_encode_record_usize_usize_usize_usize_usize_usize_usize(
(BigInt, BigInt, BigInt, BigInt, BigInt, BigInt, BigInt) self,
SseSerializer serializer);
@protected
void sse_encode_u_64(BigInt self, SseSerializer serializer);
@protected
void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_usize(BigInt self, SseSerializer serializer);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
}
// Section: wire_class
class RustLibWire implements BaseWire {
factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) =>
RustLibWire(lib.ffiDynamicLibrary);
/// Holds the symbol lookup function.
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
_lookup;
/// The symbols are looked up in [dynamicLibrary].
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup;
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB(
ptr,
);
}
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDBPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_photos_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB');
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDBPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB(
ptr,
);
}
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDBPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_photos_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB');
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDB =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerVectorDBPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
}

View File

@@ -19,26 +19,8 @@ class EnteTheme {
required this.shadowButton,
});
static bool isDark(ThemeData theme) {
return theme.brightness == Brightness.dark;
}
static EnteColorScheme getColorScheme(
ThemeData theme, {
bool inverse = false,
}) {
return inverse
? theme.colorScheme.inverseEnteTheme.colorScheme
: theme.colorScheme.enteTheme.colorScheme;
}
static EnteTextTheme getTextTheme(
ThemeData theme, {
bool inverse = false,
}) {
return inverse
? theme.colorScheme.inverseEnteTheme.textTheme
: theme.colorScheme.enteTheme.textTheme;
static bool isDark(BuildContext context) {
return Theme.of(context).brightness == Brightness.dark;
}
}
@@ -58,7 +40,6 @@ EnteTheme darkTheme = EnteTheme(
shadowButton: shadowButtonDark,
);
@Deprecated('Use EnteTheme.getColorScheme instead')
EnteColorScheme getEnteColorScheme(
BuildContext context, {
bool inverse = false,
@@ -68,7 +49,6 @@ EnteColorScheme getEnteColorScheme(
: Theme.of(context).colorScheme.enteTheme.colorScheme;
}
@Deprecated('Use EnteTheme.getTextTheme instead')
EnteTextTheme getEnteTextTheme(
BuildContext context, {
bool inverse = false,

View File

@@ -43,15 +43,14 @@ class _DeleteAccountPageState extends State<DeleteAccountPage> {
_dropdownValue ??= _defaultSelection;
final double dropDownTextSize = MediaQuery.of(context).size.width - 120;
final theme = Theme.of(context);
final colorScheme = EnteTheme.getColorScheme(theme);
final colorScheme = getEnteColorScheme(context);
return Scaffold(
appBar: AppBar(
elevation: 0,
title: Text(AppLocalizations.of(context).deleteAccount),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
@@ -69,7 +68,7 @@ class _DeleteAccountPageState extends State<DeleteAccountPage> {
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
AppLocalizations.of(context).askDeleteReason,
style: EnteTheme.getTextTheme(theme).body,
style: getEnteTextTheme(context).body,
),
),
const SizedBox(height: 8),
@@ -99,7 +98,7 @@ class _DeleteAccountPageState extends State<DeleteAccountPage> {
width: dropDownTextSize,
child: Text(
value,
style: EnteTheme.getTextTheme(theme).smallMuted,
style: getEnteTextTheme(context).smallMuted,
overflow: TextOverflow.visible,
),
),
@@ -112,12 +111,12 @@ class _DeleteAccountPageState extends State<DeleteAccountPage> {
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
AppLocalizations.of(context).deleteAccountFeedbackPrompt,
style: EnteTheme.getTextTheme(theme).body,
style: getEnteTextTheme(context).body,
),
),
const SizedBox(height: 8),
TextFormField(
style: EnteTheme.getTextTheme(theme).smallMuted,
style: getEnteTextTheme(context).smallMuted,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderSide:
@@ -156,7 +155,7 @@ class _DeleteAccountPageState extends State<DeleteAccountPage> {
child: Text(
AppLocalizations.of(context)
.kindlyHelpUsWithThisInformation,
style: EnteTheme.getTextTheme(theme)
style: getEnteTextTheme(context)
.smallBold
.copyWith(color: colorScheme.warning700),
),
@@ -175,7 +174,7 @@ class _DeleteAccountPageState extends State<DeleteAccountPage> {
children: [
Checkbox(
value: _hasConfirmedDeletion,
side: theme.checkboxTheme.side,
side: CheckboxTheme.of(context).side,
onChanged: (value) {
setState(() {
_hasConfirmedDeletion = value!;
@@ -187,7 +186,7 @@ class _DeleteAccountPageState extends State<DeleteAccountPage> {
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
AppLocalizations.of(context).confirmDeletePrompt,
style: EnteTheme.getTextTheme(theme).bodyMuted,
style: getEnteTextTheme(context).bodyMuted,
textAlign: TextAlign.left,
),
),

View File

@@ -74,7 +74,6 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
@override
Widget build(BuildContext context) {
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
final theme = Theme.of(context);
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
@@ -88,7 +87,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
@@ -98,16 +97,17 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
child: StepProgressIndicator(
totalSteps: 4,
currentStep: 1,
selectedColor: theme.colorScheme.greenAlternative,
selectedColor: Theme.of(context).colorScheme.greenAlternative,
roundedEdges: const Radius.circular(10),
unselectedColor: theme.colorScheme.stepProgressUnselectedColor,
unselectedColor:
Theme.of(context).colorScheme.stepProgressUnselectedColor,
),
),
);
return Scaffold(
resizeToAvoidBottomInset: isKeypadOpen,
appBar: appBar,
body: _getBody(theme),
body: _getBody(),
floatingActionButton: DynamicFAB(
isKeypadOpen: isKeypadOpen,
isFormValid: _isFormValid(),
@@ -130,7 +130,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
);
}
Widget _getBody(ThemeData theme) {
Widget _getBody() {
var passwordStrengthText = AppLocalizations.of(context).weakStrength;
var passwordStrengthColor = Colors.redAccent;
if (_passwordStrength > kStrongPasswordStrengthThreshold) {
@@ -151,18 +151,18 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Text(
AppLocalizations.of(context).createNewAccount,
style: theme.textTheme.headlineMedium,
style: Theme.of(context).textTheme.headlineMedium,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: TextFormField(
style: theme.textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
autofillHints: const [AutofillHints.email],
decoration: InputDecoration(
fillColor: _emailIsValid
? _validFieldValueColor
: EnteTheme.getColorScheme(theme).fillFaint,
: getEnteColorScheme(context).fillFaint,
filled: true,
hintText: AppLocalizations.of(context).email,
contentPadding: const EdgeInsets.symmetric(
@@ -176,8 +176,11 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
suffixIcon: _emailIsValid
? Icon(
Icons.check,
color: theme.inputDecorationTheme.focusedBorder!
.borderSide.color,
color: Theme.of(context)
.inputDecorationTheme
.focusedBorder!
.borderSide
.color,
)
: null,
),
@@ -207,7 +210,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
decoration: InputDecoration(
fillColor: _passwordIsValid
? _validFieldValueColor
: EnteTheme.getColorScheme(theme).fillFaint,
: getEnteColorScheme(context).fillFaint,
filled: true,
hintText: AppLocalizations.of(context).password,
contentPadding: const EdgeInsets.symmetric(
@@ -220,7 +223,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
_password1Visible
? Icons.visibility
: Icons.visibility_off,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
size: 20,
),
onPressed: () {
@@ -232,8 +235,11 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
: _passwordIsValid
? Icon(
Icons.check,
color: theme.inputDecorationTheme
.focusedBorder!.borderSide.color,
color: Theme.of(context)
.inputDecorationTheme
.focusedBorder!
.borderSide
.color,
)
: null,
border: UnderlineInputBorder(
@@ -273,7 +279,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
decoration: InputDecoration(
fillColor: _passwordsMatch && _passwordIsValid
? _validFieldValueColor
: EnteTheme.getColorScheme(theme).fillFaint,
: getEnteColorScheme(context).fillFaint,
filled: true,
hintText: AppLocalizations.of(context).confirmPassword,
contentPadding: const EdgeInsets.symmetric(
@@ -286,7 +292,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
_password2Visible
? Icons.visibility
: Icons.visibility_off,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
size: 20,
),
onPressed: () {
@@ -298,8 +304,11 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
: _passwordsMatch
? Icon(
Icons.check,
color: theme.inputDecorationTheme
.focusedBorder!.borderSide.color,
color: Theme.of(context)
.inputDecorationTheme
.focusedBorder!
.borderSide
.color,
)
: null,
border: UnderlineInputBorder(
@@ -336,8 +345,9 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
child: Row(
children: [
Text(
AppLocalizations.of(context)
.passwordStrength(passwordStrengthText),
AppLocalizations.of(context).passwordStrength(
passwordStrengthValue: passwordStrengthText,
),
style: TextStyle(
color: passwordStrengthColor,
fontWeight: FontWeight.w500,
@@ -348,7 +358,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
Icon(
Icons.info_outline,
size: 16,
color: EnteTheme.getColorScheme(theme).fillStrong,
color: getEnteColorScheme(context).fillStrong,
),
],
),
@@ -361,16 +371,16 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
const EdgeInsets.symmetric(vertical: 0, horizontal: 20),
child: Text(
AppLocalizations.of(context).hearUsWhereTitle,
style: EnteTheme.getTextTheme(theme).smallFaint,
style: getEnteTextTheme(context).smallFaint,
),
),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: TextFormField(
style: theme.textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
decoration: InputDecoration(
fillColor: EnteTheme.getColorScheme(theme).fillFaint,
fillColor: getEnteColorScheme(context).fillFaint,
filled: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
@@ -390,7 +400,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
},
child: Icon(
Icons.info_outline_rounded,
color: EnteTheme.getColorScheme(theme).fillStrong,
color: getEnteColorScheme(context).fillStrong,
),
),
),
@@ -404,10 +414,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
),
Divider(
thickness: 1,
color: EnteTheme.getColorScheme(theme).strokeFaint,
color: getEnteColorScheme(context).strokeFaint,
),
const SizedBox(height: 12),
_getAgreement(theme),
_getAgreement(),
const SizedBox(height: 40),
],
),
@@ -417,19 +427,19 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
);
}
Container _getAgreement(ThemeData theme) {
Container _getAgreement() {
return Container(
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Column(
children: [
_getTOSAgreement(theme),
_getPasswordAgreement(theme),
_getTOSAgreement(),
_getPasswordAgreement(),
],
),
);
}
Widget _getTOSAgreement(ThemeData theme) {
Widget _getTOSAgreement() {
return GestureDetector(
onTap: () {
setState(() {
@@ -441,7 +451,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
children: [
Checkbox(
value: _hasAgreedToTOS,
side: theme.checkboxTheme.side,
side: CheckboxTheme.of(context).side,
onChanged: (value) {
setState(() {
_hasAgreedToTOS = value!;
@@ -451,7 +461,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
Expanded(
child: StyledText(
text: AppLocalizations.of(context).signUpTerms,
style: theme.textTheme.titleMedium!.copyWith(fontSize: 12),
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 12),
tags: {
'u-terms': StyledTextActionTag(
(String? text, Map<String?, String?> attrs) =>
@@ -493,7 +506,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
);
}
Widget _getPasswordAgreement(ThemeData theme) {
Widget _getPasswordAgreement() {
return GestureDetector(
onTap: () {
setState(() {
@@ -505,7 +518,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
children: [
Checkbox(
value: _hasAgreedToE2E,
side: theme.checkboxTheme.side,
side: CheckboxTheme.of(context).side,
onChanged: (value) {
setState(() {
_hasAgreedToE2E = value!;
@@ -515,7 +528,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
Expanded(
child: StyledText(
text: AppLocalizations.of(context).ackPasswordLostWarning,
style: theme.textTheme.titleMedium!.copyWith(fontSize: 12),
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 12),
tags: {
'underline': StyledTextActionTag(
(String? text, Map<String?, String?> attrs) =>

View File

@@ -41,7 +41,6 @@ class _LoginPageState extends State<LoginPage> {
@override
Widget build(BuildContext context) {
final isKeypadOpen = MediaQuery.viewInsetsOf(context).bottom > 100;
final theme = Theme.of(context);
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
@@ -57,7 +56,7 @@ class _LoginPageState extends State<LoginPage> {
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
@@ -111,7 +110,6 @@ class _LoginPageState extends State<LoginPage> {
Widget _getBody() {
final l10n = context.l10n;
final theme = Theme.of(context);
return Column(
children: [
Expanded(
@@ -123,7 +121,7 @@ class _LoginPageState extends State<LoginPage> {
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Text(
l10n.accountWelcomeBack,
style: theme.textTheme.headlineMedium,
style: Theme.of(context).textTheme.headlineMedium,
),
),
Padding(
@@ -147,8 +145,11 @@ class _LoginPageState extends State<LoginPage> {
? Icon(
Icons.check,
size: 20,
color: theme.inputDecorationTheme.focusedBorder!
.borderSide.color,
color: Theme.of(context)
.inputDecorationTheme
.focusedBorder!
.borderSide
.color,
)
: null,
),
@@ -167,7 +168,7 @@ class _LoginPageState extends State<LoginPage> {
padding: const EdgeInsets.symmetric(vertical: 18),
child: Divider(
thickness: 1,
color: EnteTheme.getColorScheme(theme).strokeFaint,
color: getEnteColorScheme(context).strokeFaint,
),
),
Padding(
@@ -178,7 +179,9 @@ class _LoginPageState extends State<LoginPage> {
flex: 5,
child: StyledText(
text: AppLocalizations.of(context).loginTerms,
style: theme.textTheme.titleMedium!
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 12),
tags: {
'u-terms': StyledTextActionTag(
@@ -240,9 +243,7 @@ class _LoginPageState extends State<LoginPage> {
if (_emailIsValid) {
_emailInputFieldColor = const Color.fromRGBO(45, 194, 98, 0.2);
} else {
// Using Theme.of(context) directly since this method is outside build and 'theme' isn't stored.
_emailInputFieldColor =
EnteTheme.getColorScheme(Theme.of(context)).fillFaint;
_emailInputFieldColor = getEnteColorScheme(context).fillFaint;
}
}
}

View File

@@ -61,7 +61,6 @@ class _LoginPasswordVerificationPageState
@override
Widget build(BuildContext context) {
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
late final theme = Theme.of(context);
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
@@ -77,13 +76,13 @@ class _LoginPasswordVerificationPageState
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
),
),
body: _getBody(theme),
body: _getBody(),
floatingActionButton: DynamicFAB(
key: const ValueKey("verifyPasswordButton"),
isKeypadOpen: isKeypadOpen,
@@ -202,7 +201,7 @@ class _LoginPasswordVerificationPageState
}
}
Widget _getBody(ThemeData theme) {
Widget _getBody() {
return Column(
children: [
Expanded(
@@ -213,7 +212,7 @@ class _LoginPasswordVerificationPageState
padding: const EdgeInsets.only(top: 30, left: 20, right: 20),
child: Text(
AppLocalizations.of(context).enterPassword,
style: theme.textTheme.headlineMedium,
style: Theme.of(context).textTheme.headlineMedium,
),
),
Padding(
@@ -224,7 +223,7 @@ class _LoginPasswordVerificationPageState
),
child: Text(
email ?? '',
style: EnteTheme.getTextTheme(theme).smallMuted,
style: getEnteTextTheme(context).smallMuted,
),
),
Visibility(
@@ -254,14 +253,14 @@ class _LoginPasswordVerificationPageState
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(6),
),
fillColor: EnteTheme.getColorScheme(theme).fillFaint,
fillColor: getEnteColorScheme(context).fillFaint,
suffixIcon: _passwordInFocus
? IconButton(
icon: Icon(
_passwordVisible
? Icons.visibility
: Icons.visibility_off,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
size: 20,
),
onPressed: () {
@@ -290,7 +289,7 @@ class _LoginPasswordVerificationPageState
padding: const EdgeInsets.symmetric(vertical: 18),
child: Divider(
thickness: 1,
color: EnteTheme.getColorScheme(theme).strokeFaint,
color: getEnteColorScheme(context).strokeFaint,
),
),
Padding(
@@ -310,10 +309,13 @@ class _LoginPasswordVerificationPageState
child: Center(
child: Text(
AppLocalizations.of(context).forgotPassword,
style: theme.textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
),
@@ -333,10 +335,13 @@ class _LoginPasswordVerificationPageState
child: Center(
child: Text(
AppLocalizations.of(context).changeEmail,
style: theme.textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
),

View File

@@ -31,7 +31,6 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
@override
Widget build(BuildContext context) {
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
final theme = Theme.of(context);
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
@@ -47,7 +46,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
@@ -58,15 +57,15 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
child: StepProgressIndicator(
totalSteps: 4,
currentStep: 2,
selectedColor: theme.colorScheme.greenAlternative,
selectedColor: Theme.of(context).colorScheme.greenAlternative,
roundedEdges: const Radius.circular(10),
unselectedColor:
theme.colorScheme.stepProgressUnselectedColor,
Theme.of(context).colorScheme.stepProgressUnselectedColor,
),
)
: null,
),
body: _getBody(theme),
body: _getBody(),
floatingActionButton: DynamicFAB(
key: const ValueKey("verifyOttButton"),
isKeypadOpen: isKeypadOpen,
@@ -94,7 +93,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
);
}
Widget _getBody(ThemeData theme) {
Widget _getBody() {
return ListView(
children: [
Column(
@@ -104,7 +103,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
padding: const EdgeInsets.fromLTRB(20, 30, 20, 15),
child: Text(
AppLocalizations.of(context).verifyEmail,
style: theme.textTheme.headlineMedium,
style: Theme.of(context).textTheme.headlineMedium,
),
),
Padding(
@@ -119,13 +118,17 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
padding: const EdgeInsets.fromLTRB(0, 0, 0, 12),
child: StyledText(
text: AppLocalizations.of(context)
.weHaveSendEmailTo(widget.email),
style: theme.textTheme.titleMedium!
.weHaveSendEmailTo(email: widget.email),
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 14),
tags: {
'green': StyledTextTag(
style: TextStyle(
color: theme.colorScheme.greenAlternative,
color: Theme.of(context)
.colorScheme
.greenAlternative,
),
),
},
@@ -134,13 +137,17 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
widget.isResetPasswordScreen
? Text(
AppLocalizations.of(context).toResetVerifyEmail,
style: theme.textTheme.titleMedium!
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 14),
)
: Text(
AppLocalizations.of(context)
.checkInboxAndSpamFolder,
style: theme.textTheme.titleMedium!
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 14),
),
],
@@ -157,7 +164,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
child: TextFormField(
key: const ValueKey("ottVerificationInputField"),
style: theme.textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
decoration: InputDecoration(
filled: true,
hintText: AppLocalizations.of(context).tapToEnterCode,
@@ -166,7 +173,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(6),
),
fillColor: EnteTheme.getColorScheme(theme).fillFaint,
fillColor: getEnteColorScheme(context).fillFaint,
),
controller: _verificationCodeController,
autofocus: false,
@@ -179,7 +186,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
),
Divider(
thickness: 1,
color: EnteTheme.getColorScheme(theme).strokeFaint,
color: getEnteColorScheme(context).strokeFaint,
),
Padding(
padding: const EdgeInsets.all(20),
@@ -198,10 +205,10 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
},
child: Text(
AppLocalizations.of(context).resendEmail,
style: theme.textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
],

View File

@@ -88,7 +88,6 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
@override
Widget build(BuildContext context) {
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
final theme = Theme.of(context);
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
@@ -113,14 +112,14 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
? const SizedBox.shrink()
: IconButton(
icon: const Icon(Icons.arrow_back),
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
),
elevation: 0,
),
body: _getBody(title, theme),
body: _getBody(title),
floatingActionButton: DynamicFAB(
isKeypadOpen: isKeypadOpen,
isFormValid: _passwordsMatch && _isPasswordValid,
@@ -139,7 +138,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
);
}
Widget _getBody(String buttonTextAndHeading, ThemeData theme) {
Widget _getBody(String buttonTextAndHeading) {
final email = Configuration.instance.getEmail();
var passwordStrengthText = AppLocalizations.of(context).weakStrength;
var passwordStrengthColor = Colors.redAccent;
@@ -164,7 +163,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Text(
buttonTextAndHeading,
style: theme.textTheme.headlineMedium,
style: Theme.of(context).textTheme.headlineMedium,
),
),
Padding(
@@ -175,7 +174,10 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
: AppLocalizations.of(context)
.enterNewPasswordToEncrypt,
textAlign: TextAlign.start,
style: theme.textTheme.titleMedium!.copyWith(fontSize: 14),
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 14),
),
),
const Padding(padding: EdgeInsets.all(8)),
@@ -183,13 +185,17 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
padding: const EdgeInsets.symmetric(horizontal: 20),
child: StyledText(
text: AppLocalizations.of(context).passwordWarning,
style: theme.textTheme.titleMedium!.copyWith(fontSize: 14),
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 14),
tags: {
'underline': StyledTextTag(
style: theme.textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
style:
Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
},
),
@@ -216,7 +222,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
decoration: InputDecoration(
fillColor: _isPasswordValid
? _validFieldValueColor
: EnteTheme.getColorScheme(theme).fillFaint,
: getEnteColorScheme(context).fillFaint,
filled: true,
hintText: AppLocalizations.of(context).password,
contentPadding: const EdgeInsets.all(20),
@@ -230,7 +236,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
_password1Visible
? Icons.visibility
: Icons.visibility_off,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
size: 20,
),
onPressed: () {
@@ -242,8 +248,11 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
: _isPasswordValid
? Icon(
Icons.check,
color: theme.inputDecorationTheme
.focusedBorder!.borderSide.color,
color: Theme.of(context)
.inputDecorationTheme
.focusedBorder!
.borderSide
.color,
)
: null,
),
@@ -278,7 +287,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
decoration: InputDecoration(
fillColor: _passwordsMatch
? _validFieldValueColor
: EnteTheme.getColorScheme(theme).fillFaint,
: getEnteColorScheme(context).fillFaint,
filled: true,
hintText: AppLocalizations.of(context).confirmPassword,
contentPadding: const EdgeInsets.symmetric(
@@ -291,7 +300,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
_password2Visible
? Icons.visibility
: Icons.visibility_off,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
size: 20,
),
onPressed: () {
@@ -303,8 +312,11 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
: _passwordsMatch
? Icon(
Icons.check,
color: theme.inputDecorationTheme
.focusedBorder!.borderSide.color,
color: Theme.of(context)
.inputDecorationTheme
.focusedBorder!
.borderSide
.color,
)
: null,
border: UnderlineInputBorder(
@@ -331,8 +343,8 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Text(
AppLocalizations.of(context)
.passwordStrength(passwordStrengthText),
AppLocalizations.of(context).passwordStrength(
passwordStrengthValue: passwordStrengthText,),
style: TextStyle(
color: passwordStrengthColor,
),
@@ -359,10 +371,11 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
child: RichText(
text: TextSpan(
text: AppLocalizations.of(context).howItWorks,
style: theme.textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
style:
Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
),

View File

@@ -63,7 +63,6 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
@override
Widget build(BuildContext context) {
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
final theme = Theme.of(context);
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
@@ -79,13 +78,13 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
),
),
body: _getBody(theme),
body: _getBody(),
floatingActionButton: DynamicFAB(
key: const ValueKey("verifyPasswordButton"),
isKeypadOpen: isKeypadOpen,
@@ -187,7 +186,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
}
}
Widget _getBody(ThemeData theme) {
Widget _getBody() {
return Column(
children: [
Expanded(
@@ -199,7 +198,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Text(
AppLocalizations.of(context).welcomeBack,
style: theme.textTheme.headlineMedium,
style: Theme.of(context).textTheme.headlineMedium,
),
),
Visibility(
@@ -224,7 +223,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
decoration: InputDecoration(
hintText: AppLocalizations.of(context).enterYourPassword,
filled: true,
fillColor: EnteTheme.getColorScheme(theme).fillFaint,
fillColor: getEnteColorScheme(context).fillFaint,
contentPadding: const EdgeInsets.all(20),
border: UnderlineInputBorder(
borderSide: BorderSide.none,
@@ -236,7 +235,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
_passwordVisible
? Icons.visibility
: Icons.visibility_off,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
size: 20,
),
onPressed: () {
@@ -265,7 +264,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
padding: const EdgeInsets.symmetric(vertical: 18),
child: Divider(
thickness: 1,
color: EnteTheme.getColorScheme(theme).strokeFaint,
color: getEnteColorScheme(context).strokeFaint,
),
),
Padding(
@@ -286,10 +285,11 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
},
child: Text(
AppLocalizations.of(context).forgotPassword,
style: theme.textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
style:
Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
GestureDetector(
@@ -307,10 +307,11 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
},
child: Text(
AppLocalizations.of(context).changeEmail,
style: theme.textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
style:
Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
],

View File

@@ -51,7 +51,6 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
@override
Widget build(BuildContext context) {
final String recoveryKey = bip39.entropyToMnemonic(widget.recoveryKey);
final theme = Theme.of(context);
if (recoveryKey.split(' ').length != mnemonicKeyWordCount) {
throw AssertionError(
'recovery code should have $mnemonicKeyWordCount words',
@@ -73,10 +72,10 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
child: StepProgressIndicator(
totalSteps: 4,
currentStep: 3,
selectedColor: theme.colorScheme.greenAlternative,
selectedColor: Theme.of(context).colorScheme.greenAlternative,
roundedEdges: const Radius.circular(10),
unselectedColor:
theme.colorScheme.stepProgressUnselectedColor,
Theme.of(context).colorScheme.stepProgressUnselectedColor,
),
),
)
@@ -108,7 +107,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
: Text(
widget.title ??
AppLocalizations.of(context).recoveryKey,
style: theme.textTheme.headlineMedium,
style: Theme.of(context).textTheme.headlineMedium,
),
Padding(
padding: EdgeInsets.all(widget.showAppBar! ? 0 : 12),
@@ -117,7 +116,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
widget.text ??
AppLocalizations.of(context)
.recoveryKeyOnForgotPassword,
style: theme.textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
),
const Padding(padding: EdgeInsets.only(top: 24)),
DottedBorder(
@@ -163,14 +162,16 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
borderRadius: const BorderRadius.all(
Radius.circular(2),
),
color:
theme.colorScheme.recoveryKeyBoxColor,
color: Theme.of(context)
.colorScheme
.recoveryKeyBoxColor,
),
padding: const EdgeInsets.all(20),
width: double.infinity,
child: Text(
recoveryKey,
style: theme.textTheme.bodyLarge,
style:
Theme.of(context).textTheme.bodyLarge,
),
),
),
@@ -184,7 +185,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
widget.subText ??
AppLocalizations.of(context)
.recoveryKeySaveDescription,
style: theme.textTheme.bodyLarge,
style: Theme.of(context).textTheme.bodyLarge,
),
),
Expanded(
@@ -195,7 +196,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _saveOptions(context, recoveryKey, theme),
children: _saveOptions(context, recoveryKey),
),
),
),
@@ -210,16 +211,12 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
);
}
List<Widget> _saveOptions(
BuildContext context,
String recoveryKey,
ThemeData theme,
) {
List<Widget> _saveOptions(BuildContext context, String recoveryKey) {
final List<Widget> childrens = [];
if (!_hasTriedToSave) {
childrens.add(
ElevatedButton(
style: theme.colorScheme.optionalActionButtonStyle,
style: Theme.of(context).colorScheme.optionalActionButtonStyle,
onPressed: () async {
await _saveKeys();
},

View File

@@ -20,7 +20,6 @@ class _RecoveryPageState extends State<RecoveryPage> {
@override
Widget build(BuildContext context) {
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
final theme = Theme.of(context);
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
return null;
@@ -35,7 +34,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
@@ -100,7 +99,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Text(
AppLocalizations.of(context).forgotPassword,
style: theme.textTheme.headlineMedium,
style: Theme.of(context).textTheme.headlineMedium,
),
),
Padding(
@@ -108,7 +107,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
child: TextFormField(
decoration: InputDecoration(
filled: true,
fillColor: EnteTheme.getColorScheme(theme).fillFaint,
fillColor: getEnteColorScheme(context).fillFaint,
hintText:
AppLocalizations.of(context).enterYourRecoveryKey,
contentPadding: const EdgeInsets.all(20),
@@ -135,7 +134,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
padding: const EdgeInsets.symmetric(vertical: 18),
child: Divider(
thickness: 1,
color: EnteTheme.getColorScheme(theme).strokeFaint,
color: getEnteColorScheme(context).strokeFaint,
),
),
Row(

View File

@@ -57,7 +57,6 @@ class _RequestPasswordVerificationPageState
@override
Widget build(BuildContext context) {
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
final theme = Theme.of(context);
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
@@ -73,13 +72,13 @@ class _RequestPasswordVerificationPageState
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
),
),
body: _getBody(theme),
body: _getBody(),
floatingActionButton: DynamicFAB(
key: const ValueKey("verifyPasswordButton"),
isKeypadOpen: isKeypadOpen,
@@ -128,7 +127,7 @@ class _RequestPasswordVerificationPageState
);
}
Widget _getBody(ThemeData theme) {
Widget _getBody() {
return Column(
children: [
Expanded(
@@ -139,7 +138,7 @@ class _RequestPasswordVerificationPageState
padding: const EdgeInsets.only(top: 30, left: 20, right: 20),
child: Text(
context.l10n.enterPassword,
style: theme.textTheme.headlineMedium,
style: Theme.of(context).textTheme.headlineMedium,
),
),
Padding(
@@ -150,7 +149,7 @@ class _RequestPasswordVerificationPageState
),
child: Text(
email ?? '',
style: EnteTheme.getTextTheme(theme).smallMuted,
style: getEnteTextTheme(context).smallMuted,
),
),
Visibility(
@@ -175,7 +174,7 @@ class _RequestPasswordVerificationPageState
decoration: InputDecoration(
hintText: context.l10n.enterYourPassword,
filled: true,
fillColor: EnteTheme.getColorScheme(theme).fillFaint,
fillColor: getEnteColorScheme(context).fillFaint,
contentPadding: const EdgeInsets.all(20),
border: UnderlineInputBorder(
borderSide: BorderSide.none,
@@ -187,7 +186,7 @@ class _RequestPasswordVerificationPageState
_passwordVisible
? Icons.visibility
: Icons.visibility_off,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
size: 20,
),
onPressed: () {
@@ -216,7 +215,7 @@ class _RequestPasswordVerificationPageState
padding: const EdgeInsets.symmetric(vertical: 18),
child: Divider(
thickness: 1,
color: EnteTheme.getColorScheme(theme).strokeFaint,
color: getEnteColorScheme(context).strokeFaint,
),
),
],

View File

@@ -35,24 +35,23 @@ class _SessionsPageState extends State<SessionsPage> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
elevation: 0,
title: Text(AppLocalizations.of(context).activeSessions),
),
body: _getBody(theme),
body: _getBody(),
);
}
Widget _getBody(ThemeData theme) {
Widget _getBody() {
if (_sessions == null) {
return const Center(child: EnteLoadingWidget());
}
final List<Widget> rows = [];
rows.add(const Padding(padding: EdgeInsets.all(4)));
for (final session in _sessions!.sessions) {
rows.add(_getSessionWidget(session, theme));
rows.add(_getSessionWidget(session));
}
return SingleChildScrollView(
child: Column(
@@ -61,21 +60,21 @@ class _SessionsPageState extends State<SessionsPage> {
);
}
Widget _getSessionWidget(Session session, ThemeData theme) {
Widget _getSessionWidget(Session session) {
final lastUsedTime =
DateTime.fromMicrosecondsSinceEpoch(session.lastUsedTime);
return Column(
children: [
InkWell(
onTap: () async {
_showSessionTerminationDialog(session, theme);
_showSessionTerminationDialog(session);
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_getUAWidget(session, theme),
_getUAWidget(session),
const Padding(padding: EdgeInsets.all(4)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -84,7 +83,9 @@ class _SessionsPageState extends State<SessionsPage> {
child: Text(
session.ip,
style: TextStyle(
color: theme.colorScheme.onSurface
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.8),
fontSize: 14,
),
@@ -95,7 +96,9 @@ class _SessionsPageState extends State<SessionsPage> {
child: Text(
getFormattedTime(context, lastUsedTime),
style: TextStyle(
color: theme.colorScheme.onSurface
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.8),
fontSize: 12,
),
@@ -108,7 +111,7 @@ class _SessionsPageState extends State<SessionsPage> {
),
),
Divider(
color: EnteTheme.getColorScheme(theme).strokeFaint,
color: getEnteColorScheme(context).strokeFaint,
),
],
);
@@ -149,7 +152,7 @@ class _SessionsPageState extends State<SessionsPage> {
}
}
void _showSessionTerminationDialog(Session session, ThemeData theme) {
void _showSessionTerminationDialog(Session session) {
final isLoggingOutFromThisDevice =
session.token == Configuration.instance.getToken();
Widget text;
@@ -168,7 +171,7 @@ class _SessionsPageState extends State<SessionsPage> {
const Padding(padding: EdgeInsets.all(8)),
Text(
session.ua,
style: theme.textTheme.bodySmall,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
@@ -199,8 +202,8 @@ class _SessionsPageState extends State<SessionsPage> {
AppLocalizations.of(context).cancel,
style: TextStyle(
color: isLoggingOutFromThisDevice
? theme.colorScheme.greenAlternative
: theme.colorScheme.defaultTextColor,
? Theme.of(context).colorScheme.greenAlternative
: Theme.of(context).colorScheme.defaultTextColor,
),
),
onPressed: () {
@@ -219,13 +222,13 @@ class _SessionsPageState extends State<SessionsPage> {
);
}
Widget _getUAWidget(Session session, ThemeData theme) {
Widget _getUAWidget(Session session) {
if (session.token == Configuration.instance.getToken()) {
return Text(
AppLocalizations.of(context).thisDevice,
style: TextStyle(
fontWeight: FontWeight.bold,
color: theme.colorScheme.greenAlternative,
color: Theme.of(context).colorScheme.greenAlternative,
),
);
}

View File

@@ -28,7 +28,6 @@ class _TwoFactorRecoveryPageState extends State<TwoFactorRecoveryPage> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(
@@ -92,7 +91,7 @@ class _TwoFactorRecoveryPageState extends State<TwoFactorRecoveryPage> {
context,
AppLocalizations.of(context).contactSupport,
AppLocalizations.of(context)
.dropSupportEmail("support@ente.io"),
.dropSupportEmail(supportEmail: "support@ente.io"),
);
},
child: Container(
@@ -103,7 +102,7 @@ class _TwoFactorRecoveryPageState extends State<TwoFactorRecoveryPage> {
style: TextStyle(
decoration: TextDecoration.underline,
fontSize: 12,
color: EnteTheme.getColorScheme(theme)
color: getEnteColorScheme(context)
.textBase
.withValues(alpha: 0.9),
),

View File

@@ -76,7 +76,6 @@ class _TwoFactorSetupPageState extends State<TwoFactorSetupPage>
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
elevation: 0,
@@ -84,11 +83,11 @@ class _TwoFactorSetupPageState extends State<TwoFactorSetupPage>
AppLocalizations.of(context).twofactorSetup,
),
),
body: _getBody(theme),
body: _getBody(),
);
}
Widget _getBody(ThemeData theme) {
Widget _getBody() {
return SingleChildScrollView(
reverse: true,
child: Center(
@@ -100,7 +99,7 @@ class _TwoFactorSetupPageState extends State<TwoFactorSetupPage>
child: Column(
children: [
TabBar(
labelColor: theme.colorScheme.greenAlternative,
labelColor: Theme.of(context).colorScheme.greenAlternative,
unselectedLabelColor: Colors.grey,
tabs: [
Tab(
@@ -117,7 +116,7 @@ class _TwoFactorSetupPageState extends State<TwoFactorSetupPage>
child: TabBarView(
controller: _tabController,
children: [
_getSecretCode(theme),
_getSecretCode(),
_getBarCode(),
],
),
@@ -128,7 +127,7 @@ class _TwoFactorSetupPageState extends State<TwoFactorSetupPage>
Divider(
height: 1,
thickness: 1,
color: theme.colorScheme.secondary,
color: Theme.of(context).colorScheme.secondary,
),
_getVerificationWidget(),
],
@@ -137,8 +136,8 @@ class _TwoFactorSetupPageState extends State<TwoFactorSetupPage>
);
}
Widget _getSecretCode(ThemeData theme) {
final Color textColor = theme.colorScheme.onSurface;
Widget _getSecretCode() {
final Color textColor = Theme.of(context).colorScheme.onSurface;
return GestureDetector(
onTap: () async {
await Clipboard.setData(ClipboardData(text: widget.secretCode));

View File

@@ -120,14 +120,13 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final enteTheme = theme.colorScheme.enteTheme;
final enteTheme = Theme.of(context).colorScheme.enteTheme;
return Scaffold(
appBar: AppBar(
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
@@ -165,7 +164,7 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
TextFormField(
decoration: InputDecoration(
filled: true,
fillColor: EnteTheme.getColorScheme(theme).fillFaint,
fillColor: getEnteColorScheme(context).fillFaint,
hintText:
AppLocalizations.of(context).enterYourRecoveryKey,
contentPadding: const EdgeInsets.all(20),

View File

@@ -18,7 +18,7 @@ import 'package:photos/services/account/user_service.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/hidden_service.dart';
import 'package:photos/theme/colors.dart';
import "package:photos/theme/ente_theme.dart";
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/common/progress_dialog.dart';
import "package:photos/ui/common/user_dialogs.dart";
import 'package:photos/ui/components/action_sheet_widget.dart';
@@ -93,7 +93,7 @@ class CollectionActions {
body:
//'This will remove the public link for accessing "${collection.name}".',
AppLocalizations.of(context)
.disableLinkMessage(collection.displayName),
.disableLinkMessage(albumName: collection.displayName),
);
if (actionResult?.action != null) {
if (actionResult!.action == ButtonAction.error) {
@@ -195,7 +195,7 @@ class CollectionActions {
],
title: AppLocalizations.of(context).removeWithQuestionMark,
body: AppLocalizations.of(context)
.removeParticipantBody(user.displayName ?? user.email),
.removeParticipantBody(userEmail: user.displayName ?? user.email),
);
if (actionResult?.action != null) {
if (actionResult!.action == ButtonAction.error) {
@@ -296,7 +296,7 @@ class CollectionActions {
context: context,
title: AppLocalizations.of(context).inviteToEnte,
icon: Icons.info_outline,
body: AppLocalizations.of(context).emailNoEnteAccount(email),
body: AppLocalizations.of(context).emailNoEnteAccount(email: email),
isDismissible: true,
buttons: [
ButtonWidget(
@@ -339,8 +339,7 @@ class CollectionActions {
BuildContext context,
List<Collection> collections,
) async {
final theme = Theme.of(context);
final textTheme = EnteTheme.getTextTheme(theme);
final textTheme = getEnteTextTheme(context);
final actionResult = await showActionSheet(
context: context,
buttons: [
@@ -395,7 +394,7 @@ class CollectionActions {
],
bodyWidget: StyledText(
text: AppLocalizations.of(context)
.deleteMultipleAlbumDialog(collections.length),
.deleteMultipleAlbumDialog(count: collections.length),
style: textTheme.body.copyWith(color: textMutedDark),
tags: {
'bold': StyledTextTag(
@@ -423,27 +422,26 @@ class CollectionActions {
// deleteCollectionSheet returns true if the album is successfully deleted
Future<bool> deleteCollectionSheet(
BuildContext context,
BuildContext bContext,
Collection collection,
) async {
final theme = Theme.of(context);
final textTheme = EnteTheme.getTextTheme(theme);
final textTheme = getEnteTextTheme(bContext);
final currentUserID = Configuration.instance.getUserID()!;
if (collection.owner.id != currentUserID) {
throw AssertionError("Can not delete album owned by others");
}
if (collection.hasSharees) {
final bool confirmDelete =
await _confirmSharedAlbumDeletion(context, collection);
await _confirmSharedAlbumDeletion(bContext, collection);
if (!confirmDelete) {
return false;
}
}
final actionResult = await showActionSheet(
context: context,
context: bContext,
buttons: [
ButtonWidget(
labelText: AppLocalizations.of(context).keepPhotos,
labelText: AppLocalizations.of(bContext).keepPhotos,
buttonType: ButtonType.neutral,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.first,
@@ -451,7 +449,7 @@ class CollectionActions {
isInAlert: true,
onTap: () async {
try {
await trashCollectionKeepingPhotos(collection, context);
await trashCollectionKeepingPhotos(collection, bContext);
} catch (e, s) {
logger.severe("Failed to keep photos & delete collection", e, s);
rethrow;
@@ -459,7 +457,7 @@ class CollectionActions {
},
),
ButtonWidget(
labelText: AppLocalizations.of(context).deletePhotos,
labelText: AppLocalizations.of(bContext).deletePhotos,
buttonType: ButtonType.critical,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.second,
@@ -475,7 +473,7 @@ class CollectionActions {
},
),
ButtonWidget(
labelText: AppLocalizations.of(context).cancel,
labelText: AppLocalizations.of(bContext).cancel,
buttonType: ButtonType.secondary,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.third,
@@ -484,7 +482,7 @@ class CollectionActions {
),
],
bodyWidget: StyledText(
text: AppLocalizations.of(context).deleteAlbumDialog,
text: AppLocalizations.of(bContext).deleteAlbumDialog,
style: textTheme.body.copyWith(color: textMutedDark),
tags: {
'bold': StyledTextTag(
@@ -497,7 +495,7 @@ class CollectionActions {
if (actionResult?.action != null &&
actionResult!.action == ButtonAction.error) {
await showGenericErrorDialog(
context: context,
context: bContext,
error: actionResult.exception,
);
return false;

View File

@@ -1,7 +1,6 @@
import "dart:async";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/events/details_sheet_event.dart";
@@ -36,12 +35,14 @@ Future<void> showSingleFileDeleteSheet(
AppLocalizations.of(context).singleFileDeleteHighlight;
String body = "";
if (isBothLocalAndRemote) {
body =
AppLocalizations.of(context).singleFileInBothLocalAndRemote(fileType);
body = AppLocalizations.of(context)
.singleFileInBothLocalAndRemote(fileType: fileType);
} else if (isRemoteOnly) {
body = AppLocalizations.of(context).singleFileInRemoteOnly(fileType);
body =
AppLocalizations.of(context).singleFileInRemoteOnly(fileType: fileType);
} else if (isLocalOnly) {
body = AppLocalizations.of(context).singleFileDeleteFromDevice(fileType);
body = AppLocalizations.of(context)
.singleFileDeleteFromDevice(fileType: fileType);
} else {
throw AssertionError("Unexpected state");
}
@@ -142,8 +143,7 @@ Future<void> showSingleFileDeleteSheet(
Future<void> showDetailsSheet(BuildContext context, EnteFile file) async {
guardedCheckPanorama(file).ignore();
final theme = Theme.of(context);
final colorScheme = EnteTheme.getColorScheme(theme);
final colorScheme = getEnteColorScheme(context);
Bus.instance.fire(
DetailsSheetEvent(
localID: file.localID,

View File

@@ -27,8 +27,7 @@ class _AutoCastDialogState extends State<AutoCastDialog> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textStyle = EnteTheme.getTextTheme(theme);
final textStyle = getEnteTextTheme(context);
final AlertDialog alert = AlertDialog(
title: Text(
AppLocalizations.of(context).connectToDevice,

View File

@@ -19,8 +19,7 @@ class _CastChooseDialogState extends State<CastChooseDialog> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textStyle = EnteTheme.getTextTheme(theme);
final textStyle = getEnteTextTheme(context);
final AlertDialog alert = AlertDialog(
title: Text(
context.l10n.playOnTv,

View File

@@ -23,9 +23,8 @@ class AlbumColumnItemWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = EnteTheme.getTextTheme(theme);
final colorScheme = EnteTheme.getColorScheme(theme);
final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
const sideOfThumbnail = 60.0;
final isSelected = selectedCollections.contains(collection);
return AnimatedContainer(
@@ -90,8 +89,9 @@ class AlbumColumnItemWidget extends StatelessWidget {
if (snapshot.hasData) {
return Text(
AppLocalizations.of(context).memoryCount(
snapshot.data!,
NumberFormat().format(snapshot.data!),
count: snapshot.data!,
formattedCount:
NumberFormat().format(snapshot.data!),
),
style: textTheme.miniMuted,
);

View File

@@ -2,7 +2,6 @@ import "dart:async";
import "dart:math";
import "package:flutter/cupertino.dart";
import 'package:flutter/material.dart';
import "package:logging/logging.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/events/collection_updated_event.dart";
@@ -63,7 +62,6 @@ class _AlbumHorizontalListState extends State<AlbumHorizontalList> {
return FutureBuilder<List<Collection>>(
future: widget.collectionsFuture(),
builder: (context, snapshot) {
final theme = Theme.of(context);
if (snapshot.hasError) {
_logger.severe("failed to fetch albums", snapshot.error);
return Text(AppLocalizations.of(context).somethingWentWrong);
@@ -82,7 +80,7 @@ class _AlbumHorizontalListState extends State<AlbumHorizontalList> {
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text(
AppLocalizations.of(context).albums,
style: EnteTheme.getTextTheme(theme).large,
style: getEnteTextTheme(context).large,
),
),
Align(

View File

@@ -27,9 +27,8 @@ class AlbumListItemWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = EnteTheme.getTextTheme(theme);
final colorScheme = EnteTheme.getColorScheme(theme);
final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
const sideOfThumbnail = 60.0;
final albumWidget = Flexible(
@@ -76,8 +75,8 @@ class AlbumListItemWidget extends StatelessWidget {
if (snapshot.hasData) {
return Text(
AppLocalizations.of(context).memoryCount(
snapshot.data!,
NumberFormat().format(snapshot.data!),
count: snapshot.data!,
formattedCount: NumberFormat().format(snapshot.data!),
),
style: textTheme.small.copyWith(
color: colorScheme.textMuted,

View File

@@ -12,9 +12,8 @@ class NewAlbumListItemWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = EnteTheme.getTextTheme(theme);
final colorScheme = EnteTheme.getColorScheme(theme);
final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
const sideOfThumbnail = 60.0;
return LayoutBuilder(
builder: (context, constraints) {
@@ -28,7 +27,7 @@ class NewAlbumListItemWidget extends StatelessWidget {
child: Container(
height: sideOfThumbnail,
width: sideOfThumbnail,
color: !EnteTheme.isDark(theme)
color: Theme.of(context).brightness == Brightness.light
? colorScheme.backdropBase
: colorScheme.backdropFaint,
child: Icon(

View File

@@ -23,8 +23,7 @@ class NewAlbumRowItemWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = EnteTheme.getColorScheme(theme);
final colorScheme = getEnteColorScheme(context);
return GestureDetector(
onTap: () async {
final result = await showTextInputDialog(
@@ -74,7 +73,7 @@ class NewAlbumRowItemWidget extends StatelessWidget {
child: Container(
height: height,
width: width,
color: theme.brightness == Brightness.light
color: Theme.of(context).brightness == Brightness.light
? colorScheme.backdropBase
: colorScheme.backdropFaint,
child: DottedBorder(
@@ -96,7 +95,7 @@ class NewAlbumRowItemWidget extends StatelessWidget {
const SizedBox(height: 6),
Text(
AppLocalizations.of(context).addNew,
style: EnteTheme.getTextTheme(theme).smallFaint,
style: getEnteTextTheme(context).smallFaint,
),
],
),

View File

@@ -48,8 +48,7 @@ class AlbumRowItemWidget extends StatelessWidget {
tag +
"_" +
c.id.toString();
final theme = Theme.of(context);
final enteTextTheme = EnteTheme.getTextTheme(theme);
final enteTextTheme = getEnteTextTheme(context);
final Widget? linkIcon = c.hasLink && isOwner
? Icon(
Icons.link,
@@ -74,7 +73,7 @@ class AlbumRowItemWidget extends StatelessWidget {
cornerSmoothing: _cornerSmoothing,
),
child: Container(
color: EnteTheme.getColorScheme(theme).strokeFaint,
color: getEnteColorScheme(context).strokeFaint,
width: sideOfThumbnail,
height: sideOfThumbnail,
),
@@ -132,8 +131,7 @@ class AlbumRowItemWidget extends StatelessWidget {
);
} else {
return Container(
color: EnteTheme.getColorScheme(theme)
.backdropBase,
color: getEnteColorScheme(context).backdropBase,
child: const NoThumbnailWidget(
borderRadius: 12,
addBorder: false,

View File

@@ -243,24 +243,24 @@ class _AlbumVerticalListWidgetState extends State<AlbumVerticalListWidget> {
bool hasVerifiedLock = false;
if (widget.actionType == CollectionActionType.addFiles) {
toastMessage =
AppLocalizations.of(context).addedSuccessfullyTo(item.displayName);
toastMessage = AppLocalizations.of(context)
.addedSuccessfullyTo(albumName: item.displayName);
shouldNavigateToCollection = true;
} else if (widget.actionType == CollectionActionType.moveFiles ||
widget.actionType == CollectionActionType.restoreFiles ||
widget.actionType == CollectionActionType.unHide) {
toastMessage =
AppLocalizations.of(context).movedSuccessfullyTo(item.displayName);
toastMessage = AppLocalizations.of(context)
.movedSuccessfullyTo(albumName: item.displayName);
shouldNavigateToCollection = true;
} else if (widget.actionType ==
CollectionActionType.moveToHiddenCollection) {
toastMessage =
AppLocalizations.of(context).movedSuccessfullyTo(item.displayName);
toastMessage = AppLocalizations.of(context)
.movedSuccessfullyTo(albumName: item.displayName);
shouldNavigateToCollection = true;
hasVerifiedLock = true;
} else if (widget.actionType == CollectionActionType.addToHiddenAlbum) {
toastMessage =
AppLocalizations.of(context).addedSuccessfullyTo(item.displayName);
toastMessage = AppLocalizations.of(context)
.addedSuccessfullyTo(albumName: item.displayName);
shouldNavigateToCollection = true;
hasVerifiedLock = true;
} else {

View File

@@ -17,19 +17,18 @@ class ArchivedCollectionsButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final Set<int> hiddenCollectionId =
CollectionsService.instance.getHiddenCollectionIds();
return OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(0),
side: BorderSide(
width: 0.5,
color: theme.iconTheme.color!.withValues(alpha: 0.24),
color: Theme.of(context).iconTheme.color!.withValues(alpha: 0.24),
),
),
child: SizedBox(
@@ -44,7 +43,7 @@ class ArchivedCollectionsButton extends StatelessWidget {
children: [
Icon(
Icons.archive_outlined,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
),
const Padding(padding: EdgeInsets.all(6)),
FutureBuilder<int>(
@@ -61,7 +60,7 @@ class ArchivedCollectionsButton extends StatelessWidget {
children: [
TextSpan(
text: AppLocalizations.of(context).archive,
style: theme.textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
),
const TextSpan(text: " \u2022 "),
TextSpan(
@@ -78,7 +77,7 @@ class ArchivedCollectionsButton extends StatelessWidget {
children: [
TextSpan(
text: AppLocalizations.of(context).archive,
style: theme.textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
),
//need to query in db and bring this value
],
@@ -91,7 +90,7 @@ class ArchivedCollectionsButton extends StatelessWidget {
),
Icon(
Icons.chevron_right,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
),
],
),

View File

@@ -14,16 +14,16 @@ class HiddenCollectionsButtonWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(0),
side: BorderSide(
color: theme.iconTheme.color!.withValues(alpha: 0.24),
width: 0.5,
color: Theme.of(context).iconTheme.color!.withValues(alpha: 0.24),
),
),
child: SizedBox(
@@ -38,7 +38,7 @@ class HiddenCollectionsButtonWidget extends StatelessWidget {
children: [
Icon(
Icons.visibility_off,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
),
const Padding(padding: EdgeInsets.all(6)),
RichText(
@@ -47,14 +47,14 @@ class HiddenCollectionsButtonWidget extends StatelessWidget {
children: [
TextSpan(
text: AppLocalizations.of(context).hidden,
style: theme.textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
),
const TextSpan(text: " \u2022 "),
WidgetSpan(
child: Icon(
Icons.lock_outline,
size: 16,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
),
),
//need to query in db and bring this value
@@ -65,7 +65,7 @@ class HiddenCollectionsButtonWidget extends StatelessWidget {
),
Icon(
Icons.chevron_right,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
),
],
),

View File

@@ -43,17 +43,16 @@ class _TrashSectionButtonState extends State<TrashSectionButton> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(0),
side: BorderSide(
width: 0.5,
color: theme.iconTheme.color!.withValues(alpha: 0.24),
color: Theme.of(context).iconTheme.color!.withValues(alpha: 0.24),
),
),
child: SizedBox(
@@ -68,7 +67,7 @@ class _TrashSectionButtonState extends State<TrashSectionButton> {
children: [
Icon(
Icons.delete,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
),
const Padding(padding: EdgeInsets.all(6)),
FutureBuilder<int>(
@@ -81,7 +80,7 @@ class _TrashSectionButtonState extends State<TrashSectionButton> {
children: [
TextSpan(
text: AppLocalizations.of(context).trash,
style: theme.textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
),
const TextSpan(text: " \u2022 "),
TextSpan(
@@ -98,7 +97,7 @@ class _TrashSectionButtonState extends State<TrashSectionButton> {
children: [
TextSpan(
text: AppLocalizations.of(context).trash,
style: theme.textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
),
//need to query in db and bring this value
],
@@ -111,7 +110,7 @@ class _TrashSectionButtonState extends State<TrashSectionButton> {
),
Icon(
Icons.chevron_right,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
),
],
),

View File

@@ -17,7 +17,6 @@ class UnCategorizedCollections extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final Collection? collection = CollectionsService.instance
.getActiveCollections()
.firstWhereOrNull((e) => e.type == CollectionType.uncategorized);
@@ -27,14 +26,14 @@ class UnCategorizedCollections extends StatelessWidget {
}
return OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(0),
side: BorderSide(
width: 0.5,
color: theme.iconTheme.color!.withValues(alpha: 0.24),
color: Theme.of(context).iconTheme.color!.withValues(alpha: 0.24),
),
),
child: SizedBox(
@@ -49,7 +48,7 @@ class UnCategorizedCollections extends StatelessWidget {
children: [
Icon(
Icons.category_outlined,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
),
const Padding(padding: EdgeInsets.all(6)),
FutureBuilder<int>(
@@ -65,7 +64,7 @@ class UnCategorizedCollections extends StatelessWidget {
TextSpan(
text:
AppLocalizations.of(context).uncategorized,
style: theme.textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
),
const TextSpan(text: " \u2022 "),
TextSpan(
@@ -83,7 +82,7 @@ class UnCategorizedCollections extends StatelessWidget {
TextSpan(
text:
AppLocalizations.of(context).uncategorized,
style: theme.textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
),
//need to query in db and bring this value
],
@@ -96,7 +95,7 @@ class UnCategorizedCollections extends StatelessWidget {
),
Icon(
Icons.chevron_right,
color: theme.iconTheme.color,
color: Theme.of(context).iconTheme.color,
),
],
),

View File

@@ -55,10 +55,10 @@ String _actionName(
String text = "";
switch (type) {
case CollectionActionType.addFiles:
text = AppLocalizations.of(context).addItem(fileCount);
text = AppLocalizations.of(context).addItem(count: fileCount);
break;
case CollectionActionType.moveFiles:
text = AppLocalizations.of(context).moveItem(fileCount);
text = AppLocalizations.of(context).moveItem(count: fileCount);
break;
case CollectionActionType.restoreFiles:
text = AppLocalizations.of(context).restoreToAlbum;
@@ -108,9 +108,7 @@ void showCollectionActionSheet(
),
),
topControl: const SizedBox.shrink(),
// Use current theme (cannot rely on an existing 'theme' variable here)
backgroundColor:
EnteTheme.getColorScheme(Theme.of(context)).backgroundElevated,
backgroundColor: getEnteColorScheme(context).backgroundElevated,
barrierColor: backdropFaintDark,
enableDrag: true,
);
@@ -172,7 +170,6 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final filesCount = widget.sharedFiles != null
? widget.sharedFiles!.length
: widget.selectedPeople != null
@@ -245,7 +242,7 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
border: Border(
top: BorderSide(
color: _enableSelection
? EnteTheme.getColorScheme(theme).strokeFaint
? getEnteColorScheme(context).strokeFaint
: Colors.transparent,
),
),
@@ -315,7 +312,7 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
showShortToast(
context,
AppLocalizations.of(context)
.addedToAlbums(_selectedCollections.length),
.addedToAlbums(count: _selectedCollections.length),
);
widget.selectedFiles?.clearAll();
}

View File

@@ -141,9 +141,8 @@ class _CollectionListPageState extends State<CollectionListPage> {
}
Widget _sortMenu(List<Collection> collections) {
final theme = Theme.of(context);
final colorTheme = EnteTheme.getColorScheme(theme);
final isLightMode = !EnteTheme.isDark(theme);
final colorTheme = getEnteColorScheme(context);
final isLightMode = Theme.of(context).brightness == Brightness.light;
Widget sortOptionText(AlbumSortKey key) {
String text = key.toString();
switch (key) {
@@ -175,7 +174,7 @@ class _CollectionListPageState extends State<CollectionListPage> {
}
return Theme(
data: theme.copyWith(
data: Theme.of(context).copyWith(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
),

View File

@@ -25,7 +25,6 @@ class DeviceFolderItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isBackedUp = deviceCollection.shouldBackup;
return GestureDetector(
child: Column(
@@ -44,7 +43,7 @@ class DeviceFolderItem extends StatelessWidget {
cornerSmoothing: _cornerSmoothing,
),
child: Container(
color: EnteTheme.getColorScheme(theme).strokeFaint,
color: getEnteColorScheme(context).strokeFaint,
width: sideOfThumbnail,
height: sideOfThumbnail,
),
@@ -90,7 +89,7 @@ class DeviceFolderItem extends StatelessWidget {
child: Text(
deviceCollection.name,
textAlign: TextAlign.left,
style: theme.colorScheme.enteTheme.textTheme.small,
style: Theme.of(context).colorScheme.enteTheme.textTheme.small,
overflow: TextOverflow.ellipsis,
),
),
@@ -100,7 +99,8 @@ class DeviceFolderItem extends StatelessWidget {
child: Text(
deviceCollection.count.toString(),
textAlign: TextAlign.left,
style: theme.colorScheme.enteTheme.textTheme.miniMuted,
style:
Theme.of(context).colorScheme.enteTheme.textTheme.miniMuted,
overflow: TextOverflow.ellipsis,
),
),

View File

@@ -7,14 +7,13 @@ class BottomShadowWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
height: 8,
decoration: BoxDecoration(
color: Colors.transparent,
boxShadow: [
BoxShadow(
color: shadowColor ?? theme.colorScheme.surface,
color: shadowColor ?? Theme.of(context).colorScheme.surface,
spreadRadius: 42,
blurRadius: 42,
offset: Offset(0, offsetDy), // changes position of shadow

Some files were not shown because too many files have changed in this diff Show More