From c34fc4bbf7597c275ca5fee236e0a8fdfcbb1e8a Mon Sep 17 00:00:00 2001 From: Moseco Date: Sun, 10 Aug 2025 11:09:02 +0900 Subject: [PATCH 01/20] build: update for flutter 3.32 upgrade (#70) --- android/settings.gradle.kts | 4 +- ios/Podfile.lock | 224 +++++++++--------- .../xcshareddata/xcschemes/Runner.xcscheme | 2 + pubspec.lock | 221 ++++++++--------- pubspec.yaml | 19 +- 5 files changed, 241 insertions(+), 229 deletions(-) diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index f2ceda0..27a702f 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -18,8 +18,8 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.7.0" apply false - id("org.jetbrains.kotlin.android") version "1.8.22" apply false + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.2.0" apply false id("com.google.gms.google-services") version "4.3.15" apply false id("com.google.firebase.crashlytics") version "2.8.1" apply false } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 84a351f..150f47b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,75 +3,70 @@ PODS: - Flutter - disk_space_plus (0.0.1): - Flutter - - Firebase/Analytics (11.10.0): - - Firebase/Core - - Firebase/Core (11.10.0): + - Firebase/CoreOnly (12.0.0): + - FirebaseCore (~> 12.0.0) + - Firebase/Crashlytics (12.0.0): - Firebase/CoreOnly - - FirebaseAnalytics (~> 11.10.0) - - Firebase/CoreOnly (11.10.0): - - FirebaseCore (~> 11.10.0) - - Firebase/Crashlytics (11.10.0): - - Firebase/CoreOnly - - FirebaseCrashlytics (~> 11.10.0) - - firebase_analytics (11.4.5): - - Firebase/Analytics (= 11.10.0) + - FirebaseCrashlytics (~> 12.0.0) + - firebase_analytics (12.0.0): - firebase_core + - FirebaseAnalytics (= 12.0.0) - Flutter - - firebase_core (3.13.0): - - Firebase/CoreOnly (= 11.10.0) + - firebase_core (4.0.0): + - Firebase/CoreOnly (= 12.0.0) - Flutter - - firebase_crashlytics (4.3.5): - - Firebase/Crashlytics (= 11.10.0) + - firebase_crashlytics (5.0.0): + - Firebase/Crashlytics (= 12.0.0) - firebase_core - Flutter - - FirebaseAnalytics (11.10.0): - - FirebaseAnalytics/AdIdSupport (= 11.10.0) - - FirebaseCore (~> 11.10.0) - - FirebaseInstallations (~> 11.0) - - GoogleUtilities/AppDelegateSwizzler (~> 8.0) - - GoogleUtilities/MethodSwizzler (~> 8.0) - - GoogleUtilities/Network (~> 8.0) - - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseAnalytics (12.0.0): + - FirebaseAnalytics/Default (= 12.0.0) + - FirebaseCore (~> 12.0.0) + - FirebaseInstallations (~> 12.0.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" - nanopb (~> 3.30910.0) - - FirebaseAnalytics/AdIdSupport (11.10.0): - - FirebaseCore (~> 11.10.0) - - FirebaseInstallations (~> 11.0) - - GoogleAppMeasurement (= 11.10.0) - - GoogleUtilities/AppDelegateSwizzler (~> 8.0) - - GoogleUtilities/MethodSwizzler (~> 8.0) - - GoogleUtilities/Network (~> 8.0) - - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseAnalytics/Default (12.0.0): + - FirebaseCore (~> 12.0.0) + - FirebaseInstallations (~> 12.0.0) + - GoogleAppMeasurement/Default (= 12.0.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" - nanopb (~> 3.30910.0) - - FirebaseCore (11.10.0): - - FirebaseCoreInternal (~> 11.10.0) - - GoogleUtilities/Environment (~> 8.0) - - GoogleUtilities/Logger (~> 8.0) - - FirebaseCoreExtension (11.10.0): - - FirebaseCore (~> 11.10.0) - - FirebaseCoreInternal (11.10.0): - - "GoogleUtilities/NSData+zlib (~> 8.0)" - - FirebaseCrashlytics (11.10.0): - - FirebaseCore (~> 11.10.0) - - FirebaseInstallations (~> 11.0) - - FirebaseRemoteConfigInterop (~> 11.0) - - FirebaseSessions (~> 11.0) - - GoogleDataTransport (~> 10.0) - - GoogleUtilities/Environment (~> 8.0) + - FirebaseCore (12.0.0): + - FirebaseCoreInternal (~> 12.0.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreExtension (12.0.0): + - FirebaseCore (~> 12.0.0) + - FirebaseCoreInternal (12.0.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - FirebaseCrashlytics (12.0.0): + - FirebaseCore (~> 12.0.0) + - FirebaseInstallations (~> 12.0.0) + - FirebaseRemoteConfigInterop (~> 12.0.0) + - FirebaseSessions (~> 12.0.0) + - GoogleDataTransport (~> 10.1) + - GoogleUtilities/Environment (~> 8.1) - nanopb (~> 3.30910.0) - PromisesObjC (~> 2.4) - - FirebaseInstallations (11.10.0): - - FirebaseCore (~> 11.10.0) - - GoogleUtilities/Environment (~> 8.0) - - GoogleUtilities/UserDefaults (~> 8.0) + - FirebaseInstallations (12.0.0): + - FirebaseCore (~> 12.0.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) - PromisesObjC (~> 2.4) - - FirebaseRemoteConfigInterop (11.11.0) - - FirebaseSessions (11.10.0): - - FirebaseCore (~> 11.10.0) - - FirebaseCoreExtension (~> 11.10.0) - - FirebaseInstallations (~> 11.0) - - GoogleDataTransport (~> 10.0) - - GoogleUtilities/Environment (~> 8.0) - - GoogleUtilities/UserDefaults (~> 8.0) + - FirebaseRemoteConfigInterop (12.0.0) + - FirebaseSessions (12.0.0): + - FirebaseCore (~> 12.0.0) + - FirebaseCoreExtension (~> 12.0.0) + - FirebaseInstallations (~> 12.0.0) + - GoogleDataTransport (~> 10.1) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) - nanopb (~> 3.30910.0) - PromisesSwift (~> 2.1) - Flutter (1.0.0) @@ -94,25 +89,31 @@ PODS: - Flutter - google_mlkit_commons - GoogleMLKit/TextRecognition (~> 7.0.0) - - GoogleAppMeasurement (11.10.0): - - GoogleAppMeasurement/AdIdSupport (= 11.10.0) - - GoogleUtilities/AppDelegateSwizzler (~> 8.0) - - GoogleUtilities/MethodSwizzler (~> 8.0) - - GoogleUtilities/Network (~> 8.0) - - "GoogleUtilities/NSData+zlib (~> 8.0)" + - GoogleAdsOnDeviceConversion (2.1.0): + - GoogleUtilities/Logger (~> 8.1) + - GoogleUtilities/Network (~> 8.1) - nanopb (~> 3.30910.0) - - GoogleAppMeasurement/AdIdSupport (11.10.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 11.10.0) - - GoogleUtilities/AppDelegateSwizzler (~> 8.0) - - GoogleUtilities/MethodSwizzler (~> 8.0) - - GoogleUtilities/Network (~> 8.0) - - "GoogleUtilities/NSData+zlib (~> 8.0)" + - GoogleAppMeasurement/Core (12.0.0): + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" - nanopb (~> 3.30910.0) - - GoogleAppMeasurement/WithoutAdIdSupport (11.10.0): - - GoogleUtilities/AppDelegateSwizzler (~> 8.0) - - GoogleUtilities/MethodSwizzler (~> 8.0) - - GoogleUtilities/Network (~> 8.0) - - "GoogleUtilities/NSData+zlib (~> 8.0)" + - GoogleAppMeasurement/Default (12.0.0): + - GoogleAdsOnDeviceConversion (= 2.1.0) + - GoogleAppMeasurement/Core (= 12.0.0) + - GoogleAppMeasurement/IdentitySupport (= 12.0.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/IdentitySupport (12.0.0): + - GoogleAppMeasurement/Core (= 12.0.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" - nanopb (~> 3.30910.0) - GoogleDataTransport (10.1.0): - nanopb (~> 3.30910.0) @@ -133,31 +134,31 @@ PODS: - GoogleToolboxForMac/Defines (= 4.2.1) - "GoogleToolboxForMac/NSData+zlib (4.2.1)": - GoogleToolboxForMac/Defines (= 4.2.1) - - GoogleUtilities/AppDelegateSwizzler (8.0.2): + - GoogleUtilities/AppDelegateSwizzler (8.1.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - GoogleUtilities/Privacy - - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Environment (8.1.0): - GoogleUtilities/Privacy - - GoogleUtilities/Logger (8.0.2): + - GoogleUtilities/Logger (8.1.0): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - GoogleUtilities/MethodSwizzler (8.0.2): + - GoogleUtilities/MethodSwizzler (8.1.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/Network (8.0.2): + - GoogleUtilities/Network (8.1.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (8.0.2)": + - "GoogleUtilities/NSData+zlib (8.1.0)": - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (8.0.2) - - GoogleUtilities/Reachability (8.0.2): + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/UserDefaults (8.0.2): + - GoogleUtilities/UserDefaults (8.1.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy - GTMSessionFetcher/Core (3.5.0) @@ -218,28 +219,31 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqlite3 (3.49.1): - - sqlite3/common (= 3.49.1) - - sqlite3/common (3.49.1) - - sqlite3/dbstatvtab (3.49.1): + - sqlite3 (3.50.4): + - sqlite3/common (= 3.50.4) + - sqlite3/common (3.50.4) + - sqlite3/dbstatvtab (3.50.4): + - sqlite3/common + - sqlite3/fts5 (3.50.4): - sqlite3/common - - sqlite3/fts5 (3.49.1): + - sqlite3/math (3.50.4): - sqlite3/common - - sqlite3/math (3.49.1): + - sqlite3/perf-threadsafe (3.50.4): - sqlite3/common - - sqlite3/perf-threadsafe (3.49.1): + - sqlite3/rtree (3.50.4): - sqlite3/common - - sqlite3/rtree (3.49.1): + - sqlite3/session (3.50.4): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - FlutterMacOS - - sqlite3 (~> 3.49.1) + - sqlite3 (~> 3.50.4) - sqlite3/dbstatvtab - sqlite3/fts5 - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree + - sqlite3/session - SSZipArchive (2.6.0) - url_launcher_ios (0.0.1): - Flutter @@ -281,6 +285,7 @@ SPEC REPOS: - FirebaseInstallations - FirebaseRemoteConfigInterop - FirebaseSessions + - GoogleAdsOnDeviceConversion - GoogleAppMeasurement - GoogleDataTransport - GoogleMLKit @@ -352,18 +357,18 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 disk_space_plus: a36391fb5f732dbdd29628b0bac5a1acbd43aaef - Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 - firebase_analytics: 1998960b8fa16fd0cd9e77a6f9fd35a2009ad65e - firebase_core: 2d4534e7b489907dcede540c835b48981d890943 - firebase_crashlytics: 961a0812ba79ed8f89a8d5d1e3763daa6267a87a - FirebaseAnalytics: 4e42333f02cf78ed93703a5c36f36dd518aebdef - FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7 - FirebaseCoreExtension: 6f357679327f3614e995dc7cf3f2d600bdc774ac - FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679 - FirebaseCrashlytics: 84b073c997235740e6a951b7ee49608932877e5c - FirebaseInstallations: 9980995bdd06ec8081dfb6ab364162bdd64245c3 - FirebaseRemoteConfigInterop: 85bdce8babed7814816496bb6f082bc05b0a45e1 - FirebaseSessions: 9b3b30947b97a15370e0902ee7a90f50ef60ead6 + Firebase: 800d487043c0557d9faed71477a38d9aafb08a41 + firebase_analytics: cd56fc56f75c1df30a6ff5290cd56e230996a76d + firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4 + firebase_crashlytics: 2c6c1a17900a38081d938330e9f48e60ec5b255d + FirebaseAnalytics: 6d790cd1b159b4eb61a99948df0934ce505a34f7 + FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a + FirebaseCoreExtension: 639afb3de6abd611952be78a794c54a47fa0f361 + FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55 + FirebaseCrashlytics: db75aa0cab8d00f68406fa247c32fe17ade884d7 + FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988 + FirebaseRemoteConfigInterop: bfa0ea72ba3dc5af739777296424e46bd6f42613 + FirebaseSessions: 4e784acda213108aafef536535cdfc03504acc42 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_exif_rotation: 5406455759acacf1f7b1b83e85dbb545340d6e9c flutter_file_dialog: ca8d7fbd1772d4f0c2777b4ab20a7787ef4e7dd8 @@ -372,11 +377,12 @@ SPEC CHECKSUMS: google_mlkit_commons: 2abe6a70e1824e431d16a51085cb475b672c8aab google_mlkit_digital_ink_recognition: 17bf08581ec4c778fe1ac525302fd3a10e8799e6 google_mlkit_text_recognition: ec2122ec89bfe0d7200763336a6e4ef44810674c - GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d + GoogleAdsOnDeviceConversion: 2be6297a4f048459e0ae17fad9bfd2844e10cf64 + GoogleAppMeasurement: 8f6ab04ad6ae493b53fcf56bd26323fb2f1384f3 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 - GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457 @@ -397,8 +403,8 @@ SPEC CHECKSUMS: security_scoped_resource: 8b95b9c0e93ae4e03b60e9b6be9c2d61e3751ec1 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 - sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b + sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 SSZipArchive: 8a6ee5677c8e304bebc109e39cf0da91ccef22ea url_launcher_ios: 694010445543906933d732453a59da0a173ae33d diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c53e2b3..9c12df5 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> =3.7.2 <4.0.0" - flutter: ">=3.29.3" + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.32.8" diff --git a/pubspec.yaml b/pubspec.yaml index 4ed5c8b..8818205 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ version: 1.4.2+23 environment: sdk: '>=3.0.5 <4.0.0' - flutter: 3.29.3 + flutter: 3.32.8 isar_version: &isar_version 3.1.8 @@ -24,9 +24,9 @@ dependencies: stacked_themes: ^0.3.13 # Firebase - firebase_core: 3.13.0 - firebase_analytics: 11.4.5 - firebase_crashlytics: 4.3.5 + firebase_core: 4.0.0 + firebase_analytics: 12.0.0 + firebase_crashlytics: 5.0.0 # UI google_nav_bar: ^5.0.6 @@ -35,11 +35,14 @@ dependencies: git: url: https://github.com/Moseco/flip_card keyboard_actions: ^4.2.0 - fl_chart: ^0.71.0 + fl_chart: ^1.0.0 flutter_svg: ^2.0.7 - flutter_sticky_header: ^0.7.0 + flutter_sticky_header: ^0.8.0 shimmer: ^3.0.0 - ruby_text: ^3.0.3 + ruby_text: + git: + url: https://github.com/Moseco/RubyText + ref: '24c7a22' introduction_screen: ^3.1.11 percent_indicator: ^4.2.3 tutorial_coach_mark: ^1.2.11 @@ -99,7 +102,7 @@ dependency_overrides: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 build_runner: any stacked_generator: ^1.4.0 From 5f5d6c938ea9a59574e73d371d23a47d7d112dbe Mon Sep 17 00:00:00 2001 From: Hampus Hammarlund Date: Sun, 10 Aug 2025 15:21:46 +0900 Subject: [PATCH 02/20] refactor: combine textfield and split widgets --- lib/ui/views/search/search_view.dart | 320 ++++-------------- .../search/widgets/hand_writing_input.dart | 146 ++++++++ 2 files changed, 216 insertions(+), 250 deletions(-) create mode 100644 lib/ui/views/search/widgets/hand_writing_input.dart diff --git a/lib/ui/views/search/search_view.dart b/lib/ui/views/search/search_view.dart index 98191d7..03a317c 100644 --- a/lib/ui/views/search/search_view.dart +++ b/lib/ui/views/search/search_view.dart @@ -15,6 +15,7 @@ import 'package:stacked_hooks/stacked_hooks.dart'; import 'search_viewmodel.dart'; import 'widgets/analysis_prompt.dart'; +import 'widgets/hand_writing_input.dart'; class SearchView extends StatelessWidget { const SearchView({super.key}); @@ -52,137 +53,11 @@ class _Body extends StackedHookView { ? _SearchHistory(searchController) : const _SearchResults(), if (viewModel.showHandWriting) - Expanded( - child: Column( - children: [ - const Divider(height: 1, thickness: 1), - SizedBox( - height: 40, - child: Row( - children: [ - Expanded( - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: viewModel.handWritingResult.length, - itemBuilder: (context, index) { - return GestureDetector( - onTap: () { - int cursorPosition = - searchController.selection.base.offset; - if (cursorPosition == 0) { - searchController.text = - viewModel.handWritingResult[index] + - searchController.text; - } else { - searchController.text = searchController - .text - .substring(0, cursorPosition) + - viewModel.handWritingResult[index] + - searchController.text - .substring(cursorPosition); - } - - searchController.selection = - TextSelection.fromPosition(TextPosition( - offset: cursorPosition + - viewModel - .handWritingResult[index].length, - )); - handWritingController.clear(); - viewModel - .searchOnChange(searchController.text); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 13, - ), - decoration: const BoxDecoration( - border: Border( - right: BorderSide(color: Colors.black), - ), - ), - child: Center( - child: Text( - viewModel.handWritingResult[index], - ), - ), - ), - ); - }, - ), - ), - const VerticalDivider(width: 1, thickness: 1), - IconButton( - onPressed: () { - viewModel.toggleHandWriting(); - keyboardFocusNode.requestFocus(); - }, - icon: const Icon(Icons.text_fields), - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - IconButton( - onPressed: viewModel.toggleHandWriting, - icon: const Icon(Icons.keyboard_hide), - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - ], - ), - ), - const Divider(height: 1, thickness: 1), - Expanded( - child: HandWritingCanvas( - onHandWritingChanged: viewModel.recognizeWriting, - controller: handWritingController, - ), - ), - const Divider(indent: 16, endIndent: 16, height: 1), - Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - onPressed: () => handWritingController.undo(), - icon: const Icon(Icons.undo), - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - IconButton( - onPressed: () => handWritingController.clear(), - icon: const Icon(Icons.delete), - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - IconButton( - onPressed: () { - int cursorPosition = - searchController.selection.base.offset; - if (cursorPosition != 0) { - searchController.text = searchController.text - .substring(0, cursorPosition - 1) + - searchController.text - .substring(cursorPosition); - - searchController.selection = - TextSelection.fromPosition( - TextPosition(offset: cursorPosition - 1)); - viewModel.searchOnChange(searchController.text); - } - }, - icon: const Icon(Icons.backspace), - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - ], - ), - ), - ], - ), - ), + HandWritingInput( + searchController: searchController, + handWritingController: handWritingController, + keyboardFocusNode: keyboardFocusNode, + ) ], ), ); @@ -241,126 +116,71 @@ class _SearchTextField extends ViewModelWidget { child: Row( children: [ Expanded( - child: viewModel.showHandWriting - ? TextField( - autofocus: true, - readOnly: true, - showCursor: true, - autocorrect: false, - enableIMEPersonalizedLearning: false, - maxLines: 1, - focusNode: handWritingFocusNode, - controller: searchController, - inputFormatters: [ - LengthLimitingTextInputFormatter(1000), - FilteringTextInputFormatter.deny( - RegExp(r'‘|’'), - replacementString: '\'', - ), - FilteringTextInputFormatter.deny( - RegExp(r'“|”'), - replacementString: '"', - ), - ], - decoration: InputDecoration( - hintText: viewModel.searchFilter.displayTitle, - contentPadding: const EdgeInsets.symmetric( - horizontal: 24, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - borderSide: const BorderSide( - color: Colors.transparent, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - borderSide: const BorderSide( - color: Colors.transparent, - ), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - borderSide: const BorderSide( - color: Colors.transparent, - ), - ), - filled: true, - fillColor: Theme.of(context).scaffoldBackgroundColor, - suffixIcon: IconButton( - onPressed: () { - viewModel.searchOnChange(''); - searchController.clear(); - handWritingFocusNode.requestFocus(); - }, - icon: Icon( - Icons.clear, - color: Theme.of(context).iconTheme.color, - ), - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - ), - ) - : TextField( - autocorrect: false, - enableIMEPersonalizedLearning: false, - maxLines: 1, - textInputAction: TextInputAction.done, - focusNode: keyboardFocusNode, - controller: searchController, - onChanged: viewModel.searchOnChange, - inputFormatters: [ - LengthLimitingTextInputFormatter(1000), - FilteringTextInputFormatter.deny( - RegExp(r'‘|’'), - replacementString: '\'', - ), - FilteringTextInputFormatter.deny( - RegExp(r'“|”'), - replacementString: '"', - ), - ], - decoration: InputDecoration( - hintText: viewModel.searchFilter.displayTitle, - contentPadding: const EdgeInsets.symmetric( - horizontal: 24, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - borderSide: const BorderSide( - color: Colors.transparent, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - borderSide: const BorderSide( - color: Colors.transparent, - ), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - borderSide: const BorderSide( - color: Colors.transparent, - ), - ), - filled: true, - fillColor: Theme.of(context).scaffoldBackgroundColor, - suffixIcon: IconButton( - onPressed: () { - viewModel.searchOnChange(''); - searchController.clear(); - keyboardFocusNode.requestFocus(); - }, - icon: Icon( - Icons.clear, - color: Theme.of(context).iconTheme.color, - ), - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - ), + child: TextField( + autofocus: viewModel.showHandWriting, + readOnly: viewModel.showHandWriting, + showCursor: true, + autocorrect: false, + enableIMEPersonalizedLearning: false, + maxLines: 1, + textInputAction: + viewModel.showHandWriting ? null : TextInputAction.done, + focusNode: viewModel.showHandWriting + ? handWritingFocusNode + : keyboardFocusNode, + controller: searchController, + onChanged: viewModel.searchOnChange, + inputFormatters: [ + LengthLimitingTextInputFormatter(1000), + FilteringTextInputFormatter.deny( + RegExp(r'‘|’'), + replacementString: '\'', + ), + FilteringTextInputFormatter.deny( + RegExp(r'“|”'), + replacementString: '"', + ), + ], + decoration: InputDecoration( + hintText: viewModel.searchFilter.displayTitle, + contentPadding: const EdgeInsets.symmetric( + horizontal: 24, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: const BorderSide( + color: Colors.transparent, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: const BorderSide( + color: Colors.transparent, + ), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: const BorderSide( + color: Colors.transparent, + ), + ), + filled: true, + fillColor: Theme.of(context).scaffoldBackgroundColor, + suffixIcon: IconButton( + onPressed: () { + viewModel.searchOnChange(''); + searchController.clear(); + handWritingFocusNode.requestFocus(); + }, + icon: Icon( + Icons.clear, + color: Theme.of(context).iconTheme.color, ), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + ), + ), ), IconButton( onPressed: viewModel.setSearchFilter, diff --git a/lib/ui/views/search/widgets/hand_writing_input.dart b/lib/ui/views/search/widgets/hand_writing_input.dart new file mode 100644 index 0000000..e3a1a7e --- /dev/null +++ b/lib/ui/views/search/widgets/hand_writing_input.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:sagase/ui/widgets/hand_writing_canvas.dart'; +import 'package:stacked/stacked.dart'; + +import '../search_viewmodel.dart'; + +class HandWritingInput extends ViewModelWidget { + final TextEditingController searchController; + final HandWritingController handWritingController; + final FocusNode keyboardFocusNode; + + const HandWritingInput({ + super.key, + required this.searchController, + required this.handWritingController, + required this.keyboardFocusNode, + }); + + @override + Widget build(BuildContext context, SearchViewModel viewModel) { + return Expanded( + child: Column( + children: [ + const Divider(height: 1, thickness: 1), + SizedBox( + height: 40, + child: Row( + children: [ + Expanded( + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: viewModel.handWritingResult.length, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + int cursorPosition = + searchController.selection.base.offset; + if (cursorPosition == 0) { + searchController.text = + viewModel.handWritingResult[index] + + searchController.text; + } else { + searchController.text = searchController.text + .substring(0, cursorPosition) + + viewModel.handWritingResult[index] + + searchController.text.substring(cursorPosition); + } + + searchController.selection = + TextSelection.fromPosition(TextPosition( + offset: cursorPosition + + viewModel.handWritingResult[index].length, + )); + handWritingController.clear(); + viewModel.searchOnChange(searchController.text); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 13, + ), + decoration: const BoxDecoration( + border: Border( + right: BorderSide(color: Colors.black), + ), + ), + child: Center( + child: Text( + viewModel.handWritingResult[index], + ), + ), + ), + ); + }, + ), + ), + const VerticalDivider(width: 1, thickness: 1), + IconButton( + onPressed: () { + viewModel.toggleHandWriting(); + keyboardFocusNode.requestFocus(); + }, + icon: const Icon(Icons.text_fields), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + IconButton( + onPressed: viewModel.toggleHandWriting, + icon: const Icon(Icons.keyboard_hide), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + ], + ), + ), + const Divider(height: 1, thickness: 1), + Expanded( + child: HandWritingCanvas( + onHandWritingChanged: viewModel.recognizeWriting, + controller: handWritingController, + ), + ), + const Divider(indent: 16, endIndent: 16, height: 1), + Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + onPressed: () => handWritingController.undo(), + icon: const Icon(Icons.undo), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + IconButton( + onPressed: () => handWritingController.clear(), + icon: const Icon(Icons.delete), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + IconButton( + onPressed: () { + int cursorPosition = searchController.selection.base.offset; + if (cursorPosition != 0) { + searchController.text = searchController.text + .substring(0, cursorPosition - 1) + + searchController.text.substring(cursorPosition); + + searchController.selection = TextSelection.fromPosition( + TextPosition(offset: cursorPosition - 1)); + viewModel.searchOnChange(searchController.text); + } + }, + icon: const Icon(Icons.backspace), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + ], + ), + ), + ], + ), + ); + } +} From 4476ce0909b0f7a1c530b964156ee6dac47ca0f8 Mon Sep 17 00:00:00 2001 From: Hampus Hammarlund Date: Sun, 14 Sep 2025 09:30:32 +0900 Subject: [PATCH 03/20] feat: implement camera and ocr image widgets --- .../ocr_painter.dart} | 13 +- lib/ui/views/ocr/ocr_view.dart | 104 ++---- lib/ui/views/ocr/ocr_viewmodel.dart | 122 ++----- lib/ui/views/search/search_view.dart | 36 +- lib/ui/views/search/search_viewmodel.dart | 84 ++++- .../search/widgets/hand_writing_input.dart | 14 +- lib/ui/views/search/widgets/ocr_widget.dart | 83 +++++ lib/ui/widgets/camera_viewfinder.dart | 132 ++++++++ lib/ui/widgets/ocr_image.dart | 307 ++++++++++++++++++ pubspec.lock | 40 +++ pubspec.yaml | 1 + 11 files changed, 726 insertions(+), 210 deletions(-) rename lib/ui/{views/ocr/painters/text_detector_painter.dart => painters/ocr_painter.dart} (88%) create mode 100644 lib/ui/views/search/widgets/ocr_widget.dart create mode 100644 lib/ui/widgets/camera_viewfinder.dart create mode 100644 lib/ui/widgets/ocr_image.dart diff --git a/lib/ui/views/ocr/painters/text_detector_painter.dart b/lib/ui/painters/ocr_painter.dart similarity index 88% rename from lib/ui/views/ocr/painters/text_detector_painter.dart rename to lib/ui/painters/ocr_painter.dart index d385cb6..8447556 100644 --- a/lib/ui/views/ocr/painters/text_detector_painter.dart +++ b/lib/ui/painters/ocr_painter.dart @@ -4,11 +4,12 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:sagase/datamodels/recognized_text_block.dart'; -class TextRecognizerPainter extends CustomPainter { +class OcrPainter extends CustomPainter { final List recognizedTextBlocks; final Size imageSize; + final void Function(RecognizedTextBlock) onSelect; - TextRecognizerPainter(this.recognizedTextBlocks, this.imageSize); + OcrPainter(this.recognizedTextBlocks, this.imageSize, this.onSelect); @override void paint(Canvas canvas, Size size) { @@ -42,17 +43,15 @@ class TextRecognizerPainter extends CustomPainter { } @override - bool shouldRepaint(TextRecognizerPainter oldDelegate) { - //TODO the painting isn't that intense so probably fine? - return true; - // return oldDelegate.recognizedTextBlocks != recognizedTextBlocks; + bool shouldRepaint(OcrPainter oldDelegate) { + return oldDelegate.recognizedTextBlocks != recognizedTextBlocks; } @override bool? hitTest(Offset position) { for (final textBlock in recognizedTextBlocks) { if (_pointInsideRectangle(position, textBlock.offsets)) { - textBlock.selected = true; + onSelect(textBlock); return true; } } diff --git a/lib/ui/views/ocr/ocr_view.dart b/lib/ui/views/ocr/ocr_view.dart index f4dd94a..e61fdfd 100644 --- a/lib/ui/views/ocr/ocr_view.dart +++ b/lib/ui/views/ocr/ocr_view.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:sagase/ui/widgets/list_item_loading.dart'; +import 'package:sagase/ui/widgets/ocr_image.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stacked/stacked.dart'; import 'ocr_viewmodel.dart'; -import 'painters/text_detector_painter.dart'; class OcrView extends StackedView { final bool cameraStart; @@ -19,7 +19,7 @@ class OcrView extends StackedView { return Scaffold( appBar: AppBar( title: Text('Character Detection'), - actions: viewModel.currentImageBytes == null + actions: viewModel.image == null ? null : [ IconButton( @@ -35,13 +35,21 @@ class OcrView extends StackedView { body: Column( children: [ Expanded( - child: viewModel.currentImageBytes == null + child: viewModel.image == null ? _OcrImageLoading() - : _OcrImage(), + : OcrImage( + key: ValueKey(viewModel.image!.path.hashCode), + image: viewModel.image!, + onImageProcessed: viewModel.handleImageProcessed, + onImageError: viewModel.handleImageError, + onTextSelected: viewModel.handleTextSelected, + locked: false, + singleSelection: false, + ), ), const Divider(indent: 8, endIndent: 8), Expanded( - child: viewModel.recognizedTextBlocks == null + child: viewModel.selectedText == null ? Column( children: [ ListItemLoading(), @@ -80,79 +88,29 @@ class _OcrImageLoading extends StatelessWidget { } } -class _OcrImage extends ViewModelWidget { - @override - Widget build(BuildContext context, OcrViewModel viewModel) { - return Center( - child: Container( - padding: const EdgeInsets.all(8), - child: GestureDetector( - onTap: () => viewModel.rebuildUi(), - child: CustomPaint( - foregroundPainter: viewModel.recognizedTextBlocks == null - ? null - : TextRecognizerPainter( - viewModel.recognizedTextBlocks!, - viewModel.imageSize, - ), - child: IgnorePointer( - child: Image.memory(viewModel.currentImageBytes!), - ), - ), - ), - ), - ); - } -} - class _SelectedText extends ViewModelWidget { @override Widget build(BuildContext context, OcrViewModel viewModel) { - if (viewModel.recognizedTextBlocks!.isEmpty) { - return Center(child: Text('No Japanese text was found')); - } - - late Widget textSection; - - if (viewModel.recognizedTextBlocks!.length == 1) { - textSection = SelectionArea( - child: SingleChildScrollView( - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(8), - child: Text( - viewModel.recognizedTextBlocks![0].text, - textAlign: TextAlign.left, - style: TextStyle(fontSize: 16), - ), - ), - ), - ); - } else { - textSection = ReorderableListView.builder( - itemCount: viewModel.recognizedTextBlocks!.length, - onReorder: viewModel.reorderList, - itemBuilder: (context, index) { - final current = viewModel.recognizedTextBlocks![index]; - return ListTile( - key: ValueKey(current), - leading: Checkbox( - value: current.selected, - onChanged: (value) { - if (value == null) return; - viewModel.toggleCheckBox(index, value); - }, - ), - title: Text(current.text), - trailing: Icon(Icons.drag_indicator), - ); - }, - ); + if (viewModel.selectedText!.isEmpty) { + return Center(child: Text('Select text from the image')); } return Column( children: [ - Expanded(child: textSection), + Expanded( + child: ReorderableListView.builder( + itemCount: viewModel.selectedText!.length, + onReorder: viewModel.reorderList, + itemBuilder: (context, index) { + final current = viewModel.selectedText![index]; + return ListTile( + key: ValueKey(current), + title: Text(current), + trailing: Icon(Icons.drag_indicator), + ); + }, + ), + ), Container( padding: EdgeInsets.only( bottom: MediaQuery.of(context).padding.bottom, @@ -162,9 +120,7 @@ class _SelectedText extends ViewModelWidget { child: TextButton.icon( icon: const Icon(Icons.text_snippet, color: Colors.white), label: Text( - viewModel.recognizedTextBlocks!.length == 1 - ? 'Analyze text' - : 'Analyze selected text', + 'Analyze text', style: TextStyle(color: Colors.white), ), onPressed: viewModel.analyzeSelectedText, diff --git a/lib/ui/views/ocr/ocr_viewmodel.dart b/lib/ui/views/ocr/ocr_viewmodel.dart index c447e6d..0325faa 100644 --- a/lib/ui/views/ocr/ocr_viewmodel.dart +++ b/lib/ui/views/ocr/ocr_viewmodel.dart @@ -1,36 +1,19 @@ -import 'dart:io'; - -import 'package:flutter/painting.dart'; -import 'package:flutter/services.dart'; -import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:kana_kit/kana_kit.dart'; -import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart' as path_provider; import 'package:sagase/app/app.locator.dart'; -import 'package:sagase/datamodels/recognized_text_block.dart'; -import 'package:sagase/utils/constants.dart' as constants; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; -import 'package:flutter_exif_rotation/flutter_exif_rotation.dart'; class OcrViewModel extends BaseViewModel { final _navigationService = locator(); final _snackbarService = locator(); - final _kanaKit = const KanaKit(); - final ImagePicker _imagePicker = ImagePicker(); - final _textRecognizer = - TextRecognizer(script: TextRecognitionScript.japanese); - Uint8List? _currentImageBytes; - Uint8List? get currentImageBytes => _currentImageBytes; - late Size _imageSize; - Size get imageSize => _imageSize; + XFile? _image; + XFile? get image => _image; - List? _recognizedTextBlocks; - List? get recognizedTextBlocks => _recognizedTextBlocks; + List? _selectedText; + List? get selectedText => _selectedText; OcrViewModel(bool cameraStart) { if (cameraStart) { @@ -49,108 +32,51 @@ class OcrViewModel extends BaseViewModel { } Future _processImage(ImageSource imageSource) async { - _currentImageBytes = null; - _recognizedTextBlocks = null; + _image = null; + _selectedText = null; rebuildUi(); - XFile? image; - try { - image = await _imagePicker.pickImage(source: imageSource); + _image = await _imagePicker.pickImage(source: imageSource); } catch (_) { _snackbarService.showSnackbar( message: 'Failed to open camera or gallery', ); } - if (image == null) { - _navigationService.back(); - return; - } - - final ocrImageDir = path.join( - (await path_provider.getApplicationCacheDirectory()).path, - constants.ocrImagesDir, - ); - await Directory(ocrImageDir).create(); - - final imagePath = path.join(ocrImageDir, image.name); - try { - await File(image.path).rename(imagePath); - } catch (_) { - if (File(image.path).existsSync()) { - File(image.path).delete(); - } - _snackbarService.showSnackbar(message: 'Failed to open image'); - _navigationService.back(); - return; - } - - final rotatedImage = await FlutterExifRotation.rotateImage(path: imagePath); - - final inputImage = InputImage.fromFilePath(rotatedImage.path); - - _currentImageBytes = await rotatedImage.readAsBytes(); rebuildUi(); + } - if (inputImage.metadata != null) { - _imageSize = inputImage.metadata!.size; - } else { - final decodedImage = await decodeImageFromList(_currentImageBytes!); - _imageSize = Size( - decodedImage.width.toDouble(), - decodedImage.height.toDouble(), - ); - } - - final recognizedText = await _textRecognizer.processImage(inputImage); - - _recognizedTextBlocks = []; - for (final textBlock in recognizedText.blocks) { - if (_kanaKit.isRomaji(textBlock.text)) continue; - _recognizedTextBlocks!.add( - RecognizedTextBlock( - text: textBlock.text, - points: textBlock.cornerPoints, - ), - ); - } + void handleImageProcessed() { + _selectedText = []; + rebuildUi(); + } + void handleImageError() { + _image = null; + _snackbarService.showSnackbar(message: 'Failed to process image'); rebuildUi(); + } - await rotatedImage.delete(); + void handleTextSelected(String text) { + _selectedText?.add(text); + rebuildUi(); } void reorderList(int oldIndex, int newIndex) { if (oldIndex < newIndex) newIndex -= 1; - _recognizedTextBlocks!.insert( + _selectedText!.insert( newIndex, - _recognizedTextBlocks!.removeAt(oldIndex), + _selectedText!.removeAt(oldIndex), ); rebuildUi(); } - void toggleCheckBox(int index, bool value) { - _recognizedTextBlocks![index].selected = value; - rebuildUi(); - } - void analyzeSelectedText() { - if (_recognizedTextBlocks == null) return; - if (_recognizedTextBlocks!.length == 1) { - _navigationService.back(result: _recognizedTextBlocks![0].text); - return; - } - - List lines = []; - - for (final textBlock in _recognizedTextBlocks!) { - if (textBlock.selected) lines.add(textBlock.text); - } - - if (lines.isEmpty) return; + if (_selectedText == null) return; + if (_selectedText!.isEmpty) return; - _navigationService.back(result: lines.join('\n')); + _navigationService.back(result: _selectedText!.join('\n')); } } diff --git a/lib/ui/views/search/search_view.dart b/lib/ui/views/search/search_view.dart index 03a317c..536bfd8 100644 --- a/lib/ui/views/search/search_view.dart +++ b/lib/ui/views/search/search_view.dart @@ -16,6 +16,7 @@ import 'package:stacked_hooks/stacked_hooks.dart'; import 'search_viewmodel.dart'; import 'widgets/analysis_prompt.dart'; import 'widgets/hand_writing_input.dart'; +import 'widgets/ocr_widget.dart'; class SearchView extends StatelessWidget { const SearchView({super.key}); @@ -52,12 +53,18 @@ class _Body extends StackedHookView { viewModel.searchResult == null ? _SearchHistory(searchController) : const _SearchResults(), - if (viewModel.showHandWriting) + if (viewModel.inputMode == InputMode.handWriting) HandWritingInput( searchController: searchController, handWritingController: handWritingController, keyboardFocusNode: keyboardFocusNode, - ) + ), + if (viewModel.inputMode == InputMode.ocr) + OcrWidget( + searchController: searchController, + handWritingController: handWritingController, + keyboardFocusNode: keyboardFocusNode, + ), ], ), ); @@ -88,12 +95,23 @@ class _SearchTextField extends ViewModelWidget { focusNode: keyboardFocusNode, displayArrows: false, toolbarButtons: [ + // Open OCR and dismiss keyboard + (node) { + return IconButton( + onPressed: () { + node.unfocus(); + viewModel.setInputMode(InputMode.ocr); + handWritingFocusNode.requestFocus(); + }, + icon: const Icon(Icons.camera_alt), + ); + }, // Open hand writing and dismiss keyboard (node) { return IconButton( onPressed: () { node.unfocus(); - viewModel.toggleHandWriting(); + viewModel.setInputMode(InputMode.handWriting); handWritingFocusNode.requestFocus(); }, icon: const Icon(Icons.draw), @@ -117,15 +135,16 @@ class _SearchTextField extends ViewModelWidget { children: [ Expanded( child: TextField( - autofocus: viewModel.showHandWriting, - readOnly: viewModel.showHandWriting, + autofocus: viewModel.inputMode != InputMode.text, + readOnly: viewModel.inputMode != InputMode.text, showCursor: true, autocorrect: false, enableIMEPersonalizedLearning: false, maxLines: 1, - textInputAction: - viewModel.showHandWriting ? null : TextInputAction.done, - focusNode: viewModel.showHandWriting + textInputAction: viewModel.inputMode != InputMode.text + ? null + : TextInputAction.done, + focusNode: viewModel.inputMode != InputMode.text ? handWritingFocusNode : keyboardFocusNode, controller: searchController, @@ -213,7 +232,6 @@ class _SearchResults extends ViewModelWidget { ); } - // Show results return Expanded( child: ListView.separated( key: UniqueKey(), diff --git a/lib/ui/views/search/search_viewmodel.dart b/lib/ui/views/search/search_viewmodel.dart index 1f8a132..dd9ee7b 100644 --- a/lib/ui/views/search/search_viewmodel.dart +++ b/lib/ui/views/search/search_viewmodel.dart @@ -1,5 +1,7 @@ import 'package:async/async.dart'; +import 'package:camera/camera.dart'; import 'package:google_mlkit_digital_ink_recognition/google_mlkit_digital_ink_recognition.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:kana_kit/kana_kit.dart'; import 'package:sagase/app/app.dialogs.dart'; import 'package:sagase/app/app.locator.dart'; @@ -30,8 +32,8 @@ class SearchViewModel extends FutureViewModel { CancelableOperation<(List, bool)>? _searchOperation; - bool _showHandWriting = false; - bool get showHandWriting => _showHandWriting; + InputMode _inputMode = InputMode.text; + InputMode get inputMode => _inputMode; List _handWritingResult = []; List get handWritingResult => _handWritingResult; @@ -45,6 +47,12 @@ class SearchViewModel extends FutureViewModel { bool _promptAnalysis = false; bool get promptAnalysis => _promptAnalysis; + XFile? _image; + XFile? get image => _image; + + bool _ocrError = false; + bool get ocrError => _ocrError; + @override Future futureToRun() async { await loadSearchHistory(); @@ -132,22 +140,6 @@ class SearchViewModel extends FutureViewModel { return (results, promptAnalysis); } - void toggleHandWriting() { - if (!_digitalInkService.ready) { - if (!_snackbarService.isSnackbarOpen) { - _snackbarService.showSnackbar( - message: - 'Hand writing detection is setting up. Please try again later.', - ); - } - return; - } - _showHandWriting = !showHandWriting; - handWritingResult.clear(); - locator().setShowNavigationBar(!_showHandWriting); - notifyListeners(); - } - Future recognizeWriting(Ink ink) async { _handWritingResult = await _digitalInkService.recognizeWriting(ink); notifyListeners(); @@ -210,4 +202,60 @@ class SearchViewModel extends FutureViewModel { arguments: ProperNounViewArguments(properNoun: properNoun), ); } + + void setInputMode(InputMode mode) { + if (_inputMode == mode) return; + + if (mode == InputMode.handWriting) { + if (!_digitalInkService.ready) { + if (!_snackbarService.isSnackbarOpen) { + _snackbarService.showSnackbar( + message: + 'Hand writing detection is setting up. Please try again later.', + ); + } + return; + } + + handWritingResult.clear(); + } + + _inputMode = mode; + _image = null; + locator().setShowNavigationBar(mode == InputMode.text); + + rebuildUi(); + } + + Future handlePictureTaken(XFile image) async { + _image = image; + rebuildUi(); + } + + void handleImageProcessed() { + //TODO maybe don't need + // rebuildUi(); + } + + void handleImageError() { + _image = null; + _snackbarService.showSnackbar(message: 'Failed to process image'); + _ocrError = true; + rebuildUi(); + } + + void handleTextSelected(String text) { + searchOnChange(text); + } + + void resetImage() { + _image = null; + rebuildUi(); + } +} + +enum InputMode { + text, + handWriting, + ocr, } diff --git a/lib/ui/views/search/widgets/hand_writing_input.dart b/lib/ui/views/search/widgets/hand_writing_input.dart index e3a1a7e..f0880bb 100644 --- a/lib/ui/views/search/widgets/hand_writing_input.dart +++ b/lib/ui/views/search/widgets/hand_writing_input.dart @@ -74,18 +74,24 @@ class HandWritingInput extends ViewModelWidget { ), ), const VerticalDivider(width: 1, thickness: 1), + IconButton( + onPressed: () => viewModel.setInputMode(InputMode.ocr), + icon: const Icon(Icons.camera_alt), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), IconButton( onPressed: () { - viewModel.toggleHandWriting(); + viewModel.setInputMode(InputMode.text); keyboardFocusNode.requestFocus(); }, - icon: const Icon(Icons.text_fields), + icon: const Icon(Icons.keyboard), splashColor: Colors.transparent, highlightColor: Colors.transparent, ), IconButton( - onPressed: viewModel.toggleHandWriting, - icon: const Icon(Icons.keyboard_hide), + onPressed: () => viewModel.setInputMode(InputMode.text), + icon: const Icon(Icons.close), splashColor: Colors.transparent, highlightColor: Colors.transparent, ), diff --git a/lib/ui/views/search/widgets/ocr_widget.dart b/lib/ui/views/search/widgets/ocr_widget.dart new file mode 100644 index 0000000..afd7c36 --- /dev/null +++ b/lib/ui/views/search/widgets/ocr_widget.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:sagase/ui/widgets/camera_viewfinder.dart'; +import 'package:sagase/ui/widgets/hand_writing_canvas.dart'; +import 'package:sagase/ui/widgets/ocr_image.dart'; +import 'package:stacked/stacked.dart'; + +import '../search_viewmodel.dart'; + +class OcrWidget extends ViewModelWidget { + final TextEditingController searchController; + final HandWritingController handWritingController; + final FocusNode keyboardFocusNode; + + const OcrWidget({ + super.key, + required this.searchController, + required this.handWritingController, + required this.keyboardFocusNode, + }); + + @override + Widget build(BuildContext context, SearchViewModel viewModel) { + return Expanded( + child: Column( + children: [ + const Divider(height: 1, thickness: 1), + SizedBox( + height: 40, + child: Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: TextButton( + onPressed: viewModel.resetImage, + child: const Text('Retake photo'), + ), + ), + ), + IconButton( + onPressed: () => + viewModel.setInputMode(InputMode.handWriting), + icon: const Icon(Icons.draw), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + IconButton( + onPressed: () { + viewModel.setInputMode(InputMode.text); + keyboardFocusNode.requestFocus(); + }, + icon: const Icon(Icons.keyboard), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + IconButton( + onPressed: () => viewModel.setInputMode(InputMode.text), + icon: const Icon(Icons.close), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + ], + ), + ), + const Divider(height: 1, thickness: 1), + Expanded( + child: viewModel.image == null + ? CameraViewfinder(onPictureTaken: viewModel.handlePictureTaken) + : OcrImage( + key: ValueKey(viewModel.image!.path.hashCode), + image: viewModel.image!, + onImageProcessed: viewModel.handleImageProcessed, + onImageError: viewModel.handleImageError, + onTextSelected: viewModel.handleTextSelected, + locked: true, + singleSelection: true, + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/camera_viewfinder.dart b/lib/ui/widgets/camera_viewfinder.dart new file mode 100644 index 0000000..3b00af9 --- /dev/null +++ b/lib/ui/widgets/camera_viewfinder.dart @@ -0,0 +1,132 @@ +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; + +class CameraViewfinder extends StatefulWidget { + final void Function(XFile) onPictureTaken; + + const CameraViewfinder({super.key, required this.onPictureTaken}); + + @override + State createState() => _CameraViewfinderState(); +} + +class _CameraViewfinderState extends State + with WidgetsBindingObserver { + CameraController? _controller; + List? _cameras; + bool _cameraInitialized = false; + bool _cameraError = false; + + @override + void initState() { + super.initState(); + _initCamera(); + } + + Future _initCamera() async { + _cameras = await availableCameras(); + if (_cameras!.isEmpty) { + setState(() { + _cameraError = true; + }); + } + CameraDescription cameraToUse = _cameras!.first; + for (final camera in _cameras!) { + if (camera.lensDirection == CameraLensDirection.back) { + cameraToUse = camera; + break; + } + } + _controller = CameraController( + cameraToUse, + ResolutionPreset.max, + enableAudio: false, + ); + + _controller!.initialize().then((_) { + if (mounted) { + setState(() => _cameraInitialized = true); + } + }).catchError((Object e) { + setState(() { + _cameraError = true; + }); + }); + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (_controller == null || !_controller!.value.isInitialized) { + return; + } + + if (state == AppLifecycleState.inactive) { + setState(() => _cameraInitialized = false); + _controller!.dispose(); + } else if (state == AppLifecycleState.resumed) { + _initCamera(); + } + } + + Future _takePhoto() async { + if (!_controller!.value.isInitialized) return; + final image = await _controller!.takePicture(); + widget.onPictureTaken(image); + } + + @override + Widget build(BuildContext context) { + if (_cameraError) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4, + children: [ + Text( + 'Failed to start camera', + style: TextStyle(fontSize: 16), + ), + Text('Confirm camera permissions in system settings'), + ], + ), + ); + } + + if (_cameraInitialized) { + return Stack( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width, + child: FittedBox( + clipBehavior: Clip.hardEdge, + fit: BoxFit.cover, + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: CameraPreview(_controller!), + ), + ), + ), + Positioned( + bottom: MediaQuery.of(context).padding.bottom + 16, + left: 0, + right: 0, + child: Center( + child: FloatingActionButton( + onPressed: _takePhoto, + child: const Icon(Icons.camera_alt), + ), + ), + ), + ], + ); + } + + return const Center(child: CircularProgressIndicator()); + } +} diff --git a/lib/ui/widgets/ocr_image.dart b/lib/ui/widgets/ocr_image.dart new file mode 100644 index 0000000..1d7b42b --- /dev/null +++ b/lib/ui/widgets/ocr_image.dart @@ -0,0 +1,307 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_exif_rotation/flutter_exif_rotation.dart'; +import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:sagase/datamodels/recognized_text_block.dart'; +import 'package:sagase/ui/painters/ocr_painter.dart'; +import 'package:sagase/utils/constants.dart' as constants; +import 'package:shimmer/shimmer.dart'; + +class OcrImage extends StatefulWidget { + final XFile image; + final void Function() onImageProcessed; + final void Function() onImageError; + final void Function(String) onTextSelected; + final bool locked; + final bool singleSelection; + + const OcrImage({ + super.key, + required this.image, + required this.onImageProcessed, + required this.onImageError, + required this.locked, + required this.onTextSelected, + required this.singleSelection, + }); + + @override + State createState() => _OcrImageState(); +} + +class _OcrImageState extends State { + final _textRecognizer = + TextRecognizer(script: TextRecognitionScript.japanese); + Uint8List? _currentImageBytes; + late Size _imageSize; + List? _recognizedTextBlocks; + + @override + void initState() { + super.initState(); + _loadImage(); + } + + Future _loadImage() async { + final ocrImageDir = path.join( + (await path_provider.getApplicationCacheDirectory()).path, + constants.ocrImagesDir, + ); + await Directory(ocrImageDir).create(); + + final imagePath = path.join(ocrImageDir, widget.image.name); + try { + await File(widget.image.path).rename(imagePath); + } catch (_) { + if (File(widget.image.path).existsSync()) { + File(widget.image.path).delete(); + } + + widget.onImageError(); + return; + } + + final rotatedImage = await FlutterExifRotation.rotateImage(path: imagePath); + + final inputImage = InputImage.fromFilePath(rotatedImage.path); + _currentImageBytes = await rotatedImage.readAsBytes(); + + if (inputImage.metadata != null) { + _imageSize = inputImage.metadata!.size; + } else { + final decodedImage = await decodeImageFromList(_currentImageBytes!); + _imageSize = Size( + decodedImage.width.toDouble(), + decodedImage.height.toDouble(), + ); + } + + setState(() {}); + + final recognizedText = await _textRecognizer.processImage(inputImage); + + _recognizedTextBlocks = []; + for (final textBlock in recognizedText.blocks) { + _recognizedTextBlocks!.add( + RecognizedTextBlock( + text: textBlock.text, + points: textBlock.cornerPoints, + ), + ); + } + + setState(() {}); + widget.onImageProcessed(); + + await rotatedImage.delete(); + } + + void handleSelect(RecognizedTextBlock textBlock) { + setState(() { + if (!textBlock.selected) { + widget.onTextSelected(textBlock.text); + } + + if (widget.singleSelection) { + for (final textBlock in _recognizedTextBlocks!) { + textBlock.selected = false; + } + } + + textBlock.selected = true; + _recognizedTextBlocks = List.from(_recognizedTextBlocks!); + }); + } + + @override + Widget build(BuildContext context) { + return _currentImageBytes == null + ? _OcrImageLoading() + : _OcrImage( + recognizedTextBlocks: _recognizedTextBlocks, + imageSize: _imageSize, + image: _currentImageBytes!, + locked: widget.locked, + onSelect: handleSelect, + ); + } +} + +class _OcrImageLoading extends StatelessWidget { + const _OcrImageLoading(); + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Padding( + padding: const EdgeInsets.all(16), + child: Shimmer.fromColors( + baseColor: isDark ? const Color(0xFF3a3a3a) : Colors.grey.shade300, + highlightColor: isDark ? const Color(0xFF4a4a4a) : Colors.grey.shade100, + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + ); + } +} + +class _OcrImage extends StatelessWidget { + final List? recognizedTextBlocks; + final Size imageSize; + final Uint8List image; + final bool locked; + final void Function(RecognizedTextBlock) onSelect; + + const _OcrImage({ + this.recognizedTextBlocks, + required this.imageSize, + required this.image, + required this.locked, + required this.onSelect, + }); + + @override + Widget build(BuildContext context) { + if (locked) { + return LayoutBuilder( + builder: (context, constraints) { + return SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + child: FittedBox( + clipBehavior: Clip.hardEdge, + fit: BoxFit.cover, + child: CustomPaint( + foregroundPainter: recognizedTextBlocks == null + ? null + : OcrPainter( + recognizedTextBlocks!, + imageSize, + onSelect, + ), + child: Image.memory(image), + ), + ), + ); + }, + ); + } else { + // TODO + return LayoutBuilder( + builder: (context, constraints) { + return InteractiveViewer( + constrained: true, + child: CustomPaint( + foregroundPainter: recognizedTextBlocks == null + ? null + : OcrPainter( + recognizedTextBlocks!, + imageSize, + onSelect, + ), + child: Image.memory(image), + ), + ); + }, + ); + // return LayoutBuilder( + // builder: (context, constraints) { + // return InteractiveViewer( + // constrained: false, + // child: CustomPaint( + // foregroundPainter: recognizedTextBlocks == null + // ? null + // : OcrPainter( + // recognizedTextBlocks!, + // imageSize, + // ), + // // child: IgnorePointer( + // // child: FittedBox( + // // clipBehavior: Clip.hardEdge, + // // fit: BoxFit.contain, + // // child: Image.memory(image), + // // ), + // // ), + // // child: IgnorePointer( + // // child: SizedBox( + // // width: MediaQuery.of(context).size.width, + // // child: FittedBox( + // // clipBehavior: Clip.hardEdge, + // // fit: BoxFit.contain, + // // child: Image.memory(image), + // // ), + // // ), + // // ), + // child: IgnorePointer( + // child: SizedBox( + // width: constraints.maxWidth, + // child: Image.memory( + // image, + // // fit: BoxFit.fitWidth, + // fit: BoxFit.cover, + // ), + // ), + // ), + // ), + // ); + // }, + // ); + } + + // return CustomPaint( + // foregroundPainter: recognizedTextBlocks == null + // ? null + // : OcrPainter( + // recognizedTextBlocks!, + // imageSize, + // ), + // child: Container( + // decoration: BoxDecoration( + // image: DecorationImage( + // image: MemoryImage(image), + // fit: BoxFit.cover, + // ), + // ), + // ), + // ); + + // return Container( + // decoration: BoxDecoration( + // image: DecorationImage( + // image: MemoryImage(image), + // fit: BoxFit.cover, + // ), + // ), + // ); + + // return Center( + // child: GestureDetector( + // // onTap: () => viewModel.rebuildUi(), + // child: CustomPaint( + // foregroundPainter: recognizedTextBlocks == null + // ? null + // : OcrPainter( + // recognizedTextBlocks!, + // imageSize, + // ), + // child: IgnorePointer( + // child: Image.memory( + // image, + // fit: BoxFit.fitWidth, + // ), + // ), + // ), + // ), + // ); + } +} diff --git a/pubspec.lock b/pubspec.lock index ff32da9..390d243 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -158,6 +158,46 @@ packages: url: "https://pub.dev" source: hosted version: "8.11.1" + camera: + dependency: "direct main" + description: + name: camera + sha256: d6ec2cbdbe2fa8f5e0d07d8c06368fe4effa985a4a5ddade9cc58a8cd849557d + url: "https://pub.dev" + source: hosted + version: "0.11.2" + camera_android_camerax: + dependency: transitive + description: + name: camera_android_camerax + sha256: "58b8fe843a3c83fd1273c00cb35f5a8ae507f6cc9b2029bcf7e2abba499e28d8" + url: "https://pub.dev" + source: hosted + version: "0.6.19+1" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "180c1b368cf3485a18c46e219e3ad80271a779a6d66a749376e31244fd927cde" + url: "https://pub.dev" + source: hosted + version: "0.9.20+7" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" + url: "https://pub.dev" + source: hosted + version: "0.3.5" characters: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8818205..ce088ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -87,6 +87,7 @@ dependencies: ref: 'ddb5dff' security_scoped_resource: ^0.0.2 sanitize_filename: ^1.0.5 + camera: ^0.11.2 image_picker: ^1.1.2 flutter_exif_rotation: git: From 85f80ed76544b6f68ac343cd449cba2162595313 Mon Sep 17 00:00:00 2001 From: Moseco Date: Sat, 20 Sep 2025 09:23:11 +0900 Subject: [PATCH 04/20] update podfile --- ios/Podfile.lock | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 150f47b..285aef0 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,6 +1,8 @@ PODS: - app_links (0.0.2): - Flutter + - camera_avfoundation (0.0.1): + - Flutter - disk_space_plus (0.0.1): - Flutter - Firebase/CoreOnly (12.0.0): @@ -250,6 +252,7 @@ PODS: DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) + - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - disk_space_plus (from `.symlinks/plugins/disk_space_plus/ios`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -309,6 +312,8 @@ SPEC REPOS: EXTERNAL SOURCES: app_links: :path: ".symlinks/plugins/app_links/ios" + camera_avfoundation: + :path: ".symlinks/plugins/camera_avfoundation/ios" disk_space_plus: :path: ".symlinks/plugins/disk_space_plus/ios" firebase_analytics: @@ -356,6 +361,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 + camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436 disk_space_plus: a36391fb5f732dbdd29628b0bac5a1acbd43aaef Firebase: 800d487043c0557d9faed71477a38d9aafb08a41 firebase_analytics: cd56fc56f75c1df30a6ff5290cd56e230996a76d From 02f32a8da336f06b5221b9a212b49e0474507a1b Mon Sep 17 00:00:00 2001 From: Moseco Date: Sat, 20 Sep 2025 09:28:52 +0900 Subject: [PATCH 05/20] fix unlocked ocr image --- lib/ui/views/search/widgets/ocr_widget.dart | 5 +- lib/ui/widgets/ocr_image.dart | 99 ++------------------- 2 files changed, 13 insertions(+), 91 deletions(-) diff --git a/lib/ui/views/search/widgets/ocr_widget.dart b/lib/ui/views/search/widgets/ocr_widget.dart index afd7c36..839abce 100644 --- a/lib/ui/views/search/widgets/ocr_widget.dart +++ b/lib/ui/views/search/widgets/ocr_widget.dart @@ -71,7 +71,10 @@ class OcrWidget extends ViewModelWidget { image: viewModel.image!, onImageProcessed: viewModel.handleImageProcessed, onImageError: viewModel.handleImageError, - onTextSelected: viewModel.handleTextSelected, + onTextSelected: (text) { + searchController.text = text; + viewModel.handleTextSelected(text); + }, locked: true, singleSelection: true, ), diff --git a/lib/ui/widgets/ocr_image.dart b/lib/ui/widgets/ocr_image.dart index 1d7b42b..6ddfbe7 100644 --- a/lib/ui/widgets/ocr_image.dart +++ b/lib/ui/widgets/ocr_image.dart @@ -196,11 +196,16 @@ class _OcrImage extends StatelessWidget { }, ); } else { - // TODO return LayoutBuilder( builder: (context, constraints) { return InteractiveViewer( - constrained: true, + constrained: false, + child: SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + child: FittedBox( + clipBehavior: Clip.hardEdge, + fit: BoxFit.contain, child: CustomPaint( foregroundPainter: recognizedTextBlocks == null ? null @@ -210,98 +215,12 @@ class _OcrImage extends StatelessWidget { onSelect, ), child: Image.memory(image), + ), + ), ), ); }, ); - // return LayoutBuilder( - // builder: (context, constraints) { - // return InteractiveViewer( - // constrained: false, - // child: CustomPaint( - // foregroundPainter: recognizedTextBlocks == null - // ? null - // : OcrPainter( - // recognizedTextBlocks!, - // imageSize, - // ), - // // child: IgnorePointer( - // // child: FittedBox( - // // clipBehavior: Clip.hardEdge, - // // fit: BoxFit.contain, - // // child: Image.memory(image), - // // ), - // // ), - // // child: IgnorePointer( - // // child: SizedBox( - // // width: MediaQuery.of(context).size.width, - // // child: FittedBox( - // // clipBehavior: Clip.hardEdge, - // // fit: BoxFit.contain, - // // child: Image.memory(image), - // // ), - // // ), - // // ), - // child: IgnorePointer( - // child: SizedBox( - // width: constraints.maxWidth, - // child: Image.memory( - // image, - // // fit: BoxFit.fitWidth, - // fit: BoxFit.cover, - // ), - // ), - // ), - // ), - // ); - // }, - // ); } - - // return CustomPaint( - // foregroundPainter: recognizedTextBlocks == null - // ? null - // : OcrPainter( - // recognizedTextBlocks!, - // imageSize, - // ), - // child: Container( - // decoration: BoxDecoration( - // image: DecorationImage( - // image: MemoryImage(image), - // fit: BoxFit.cover, - // ), - // ), - // ), - // ); - - // return Container( - // decoration: BoxDecoration( - // image: DecorationImage( - // image: MemoryImage(image), - // fit: BoxFit.cover, - // ), - // ), - // ); - - // return Center( - // child: GestureDetector( - // // onTap: () => viewModel.rebuildUi(), - // child: CustomPaint( - // foregroundPainter: recognizedTextBlocks == null - // ? null - // : OcrPainter( - // recognizedTextBlocks!, - // imageSize, - // ), - // child: IgnorePointer( - // child: Image.memory( - // image, - // fit: BoxFit.fitWidth, - // ), - // ), - // ), - // ), - // ); } } From 3481e771bf7854d17d0b458f360c7e816d3fbc43 Mon Sep 17 00:00:00 2001 From: Hampus Hammarlund Date: Sun, 21 Sep 2025 11:53:26 +0900 Subject: [PATCH 06/20] change select text method and other small changes --- lib/ui/painters/ocr_painter.dart | 2 +- lib/ui/views/ocr/ocr_view.dart | 185 ++++++++++++------ lib/ui/views/ocr/ocr_viewmodel.dart | 51 ++--- lib/ui/views/search/search_view.dart | 2 +- lib/ui/views/search/search_viewmodel.dart | 7 +- .../search/widgets/hand_writing_input.dart | 2 +- lib/ui/views/search/widgets/ocr_widget.dart | 37 ++-- lib/ui/widgets/camera_viewfinder.dart | 1 + lib/ui/widgets/ocr_image.dart | 27 +-- 9 files changed, 199 insertions(+), 115 deletions(-) diff --git a/lib/ui/painters/ocr_painter.dart b/lib/ui/painters/ocr_painter.dart index 8447556..312633e 100644 --- a/lib/ui/painters/ocr_painter.dart +++ b/lib/ui/painters/ocr_painter.dart @@ -16,7 +16,7 @@ class OcrPainter extends CustomPainter { final unselectedPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 2 - ..color = Colors.blueGrey; + ..color = Colors.lightBlueAccent; final selectedPaint = Paint() ..style = PaintingStyle.stroke diff --git a/lib/ui/views/ocr/ocr_view.dart b/lib/ui/views/ocr/ocr_view.dart index e61fdfd..4754347 100644 --- a/lib/ui/views/ocr/ocr_view.dart +++ b/lib/ui/views/ocr/ocr_view.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:sagase/ui/widgets/list_item_loading.dart'; import 'package:sagase/ui/widgets/ocr_image.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stacked/stacked.dart'; +import 'package:stacked_hooks/stacked_hooks.dart'; import 'ocr_viewmodel.dart'; @@ -15,11 +18,21 @@ class OcrView extends StackedView { OcrViewModel viewModelBuilder(context) => OcrViewModel(cameraStart); @override - Widget builder(BuildContext context, OcrViewModel viewModel, Widget? child) { + Widget builder(context, viewModel, child) => const _Body(); +} + +class _Body extends StackedHookView { + const _Body(); + + @override + Widget builder(BuildContext context, OcrViewModel viewModel) { + final controller = useTextEditingController(); + return Scaffold( appBar: AppBar( title: Text('Character Detection'), - actions: viewModel.image == null + actions: viewModel.state == OcrState.waiting || + viewModel.state == OcrState.loading ? null : [ IconButton( @@ -32,35 +45,47 @@ class OcrView extends StackedView { ), ], ), - body: Column( - children: [ - Expanded( - child: viewModel.image == null - ? _OcrImageLoading() - : OcrImage( - key: ValueKey(viewModel.image!.path.hashCode), - image: viewModel.image!, - onImageProcessed: viewModel.handleImageProcessed, - onImageError: viewModel.handleImageError, - onTextSelected: viewModel.handleTextSelected, - locked: false, - singleSelection: false, - ), - ), - const Divider(indent: 8, endIndent: 8), - Expanded( - child: viewModel.selectedText == null - ? Column( - children: [ - ListItemLoading(), - const SizedBox(height: 8), - ListItemLoading(), - ], - ) - : _SelectedText(), - ), - ], - ), + body: viewModel.state == OcrState.error + ? const _Error() + : Column( + children: [ + Expanded( + child: viewModel.image == null + ? _OcrImageLoading() + : OcrImage( + key: ValueKey(viewModel.image!.path.hashCode), + image: viewModel.image!, + onImageProcessed: viewModel.handleImageProcessed, + onImageError: viewModel.handleImageError, + onTextSelected: (text) { + controller.text = controller.text + text; + viewModel.handleTextSelected(); + }, + locked: false, + singleSelection: false, + ), + ), + const Divider(indent: 8, endIndent: 8), + Expanded( + child: switch (viewModel.state) { + OcrState.viewEmpty => const Center( + child: Text( + 'No text found in the image', + style: TextStyle(fontSize: 18), + ), + ), + OcrState.viewing => _SelectedText(controller), + _ => Column( + children: [ + ListItemLoading(), + const SizedBox(height: 8), + ListItemLoading(), + ], + ) + }, + ), + ], + ), ); } } @@ -89,44 +114,88 @@ class _OcrImageLoading extends StatelessWidget { } class _SelectedText extends ViewModelWidget { + final TextEditingController controller; + + const _SelectedText(this.controller); + @override Widget build(BuildContext context, OcrViewModel viewModel) { - if (viewModel.selectedText!.isEmpty) { - return Center(child: Text('Select text from the image')); - } - return Column( children: [ Expanded( - child: ReorderableListView.builder( - itemCount: viewModel.selectedText!.length, - onReorder: viewModel.reorderList, - itemBuilder: (context, index) { - final current = viewModel.selectedText![index]; - return ListTile( - key: ValueKey(current), - title: Text(current), - trailing: Icon(Icons.drag_indicator), - ); - }, + child: SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.all(8), + child: TextField( + controller: controller, + autofocus: true, + maxLines: null, + style: const TextStyle(fontSize: 24), + decoration: const InputDecoration.collapsed( + hintText: 'Select text from the image...', + ), + maxLength: 1000, + inputFormatters: [LengthLimitingTextInputFormatter(1000)], + ), + ), ), ), - Container( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom, - ), - width: double.infinity, - color: Colors.deepPurple, - child: TextButton.icon( - icon: const Icon(Icons.text_snippet, color: Colors.white), - label: Text( - 'Analyze text', - style: TextStyle(color: Colors.white), + AnimatedCrossFade( + crossFadeState: controller.text.isEmpty + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 200), + firstChild: SizedBox.shrink(), + secondChild: Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom, + ), + width: double.infinity, + color: Colors.deepPurple, + child: TextButton.icon( + icon: const Icon(Icons.text_snippet, color: Colors.white), + label: const Text( + 'Analyze', + style: TextStyle(color: Colors.white), + ), + onPressed: () => viewModel.analyzeText(controller.text), ), - onPressed: viewModel.analyzeSelectedText, ), ), ], ); } } + +class _Error extends ViewModelWidget { + const _Error(); + + @override + Widget build(BuildContext context, OcrViewModel viewModel) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Failed to analyze image', + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 8), + const Text('Please try again'), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: () => viewModel.openCamera(), + icon: const Icon(Icons.camera), + label: const Text('Take photo'), + ), + ElevatedButton.icon( + onPressed: () => viewModel.selectImage(), + icon: const Icon(Icons.photo), + label: const Text('Select photo'), + ), + ], + ), + ); + } +} diff --git a/lib/ui/views/ocr/ocr_viewmodel.dart b/lib/ui/views/ocr/ocr_viewmodel.dart index 0325faa..d899179 100644 --- a/lib/ui/views/ocr/ocr_viewmodel.dart +++ b/lib/ui/views/ocr/ocr_viewmodel.dart @@ -9,12 +9,12 @@ class OcrViewModel extends BaseViewModel { final ImagePicker _imagePicker = ImagePicker(); + OcrState _state = OcrState.waiting; + OcrState get state => _state; + XFile? _image; XFile? get image => _image; - List? _selectedText; - List? get selectedText => _selectedText; - OcrViewModel(bool cameraStart) { if (cameraStart) { openCamera(); @@ -33,7 +33,7 @@ class OcrViewModel extends BaseViewModel { Future _processImage(ImageSource imageSource) async { _image = null; - _selectedText = null; + _state = OcrState.waiting; rebuildUi(); try { @@ -44,39 +44,46 @@ class OcrViewModel extends BaseViewModel { ); } + if (image == null) { + _navigationService.back(); + return; + } + + _state = OcrState.loading; + rebuildUi(); } - void handleImageProcessed() { - _selectedText = []; + void handleImageProcessed(int length) { + if (length == 0) { + _state = OcrState.viewEmpty; + } else { + _state = OcrState.viewing; + } rebuildUi(); } void handleImageError() { _image = null; - _snackbarService.showSnackbar(message: 'Failed to process image'); + _state = OcrState.error; rebuildUi(); } - void handleTextSelected(String text) { - _selectedText?.add(text); + void handleTextSelected() { rebuildUi(); } - void reorderList(int oldIndex, int newIndex) { - if (oldIndex < newIndex) newIndex -= 1; + void analyzeText(String text) { + if (text.isEmpty) return; - _selectedText!.insert( - newIndex, - _selectedText!.removeAt(oldIndex), - ); - rebuildUi(); + _navigationService.back(result: text); } +} - void analyzeSelectedText() { - if (_selectedText == null) return; - if (_selectedText!.isEmpty) return; - - _navigationService.back(result: _selectedText!.join('\n')); - } +enum OcrState { + waiting, + loading, + viewing, + viewEmpty, + error, } diff --git a/lib/ui/views/search/search_view.dart b/lib/ui/views/search/search_view.dart index 536bfd8..e18bef3 100644 --- a/lib/ui/views/search/search_view.dart +++ b/lib/ui/views/search/search_view.dart @@ -121,7 +121,7 @@ class _SearchTextField extends ViewModelWidget { (node) { return IconButton( onPressed: node.unfocus, - icon: const Icon(Icons.keyboard_hide), + icon: const Icon(Icons.keyboard_arrow_down), ); }, ], diff --git a/lib/ui/views/search/search_viewmodel.dart b/lib/ui/views/search/search_viewmodel.dart index dd9ee7b..aef704b 100644 --- a/lib/ui/views/search/search_viewmodel.dart +++ b/lib/ui/views/search/search_viewmodel.dart @@ -222,6 +222,7 @@ class SearchViewModel extends FutureViewModel { _inputMode = mode; _image = null; + _ocrError = false; locator().setShowNavigationBar(mode == InputMode.text); rebuildUi(); @@ -232,11 +233,6 @@ class SearchViewModel extends FutureViewModel { rebuildUi(); } - void handleImageProcessed() { - //TODO maybe don't need - // rebuildUi(); - } - void handleImageError() { _image = null; _snackbarService.showSnackbar(message: 'Failed to process image'); @@ -250,6 +246,7 @@ class SearchViewModel extends FutureViewModel { void resetImage() { _image = null; + _ocrError = false; rebuildUi(); } } diff --git a/lib/ui/views/search/widgets/hand_writing_input.dart b/lib/ui/views/search/widgets/hand_writing_input.dart index f0880bb..e0bf95e 100644 --- a/lib/ui/views/search/widgets/hand_writing_input.dart +++ b/lib/ui/views/search/widgets/hand_writing_input.dart @@ -91,7 +91,7 @@ class HandWritingInput extends ViewModelWidget { ), IconButton( onPressed: () => viewModel.setInputMode(InputMode.text), - icon: const Icon(Icons.close), + icon: const Icon(Icons.keyboard_arrow_down), splashColor: Colors.transparent, highlightColor: Colors.transparent, ), diff --git a/lib/ui/views/search/widgets/ocr_widget.dart b/lib/ui/views/search/widgets/ocr_widget.dart index 839abce..f70be05 100644 --- a/lib/ui/views/search/widgets/ocr_widget.dart +++ b/lib/ui/views/search/widgets/ocr_widget.dart @@ -55,7 +55,7 @@ class OcrWidget extends ViewModelWidget { ), IconButton( onPressed: () => viewModel.setInputMode(InputMode.text), - icon: const Icon(Icons.close), + icon: const Icon(Icons.keyboard_arrow_down), splashColor: Colors.transparent, highlightColor: Colors.transparent, ), @@ -64,20 +64,27 @@ class OcrWidget extends ViewModelWidget { ), const Divider(height: 1, thickness: 1), Expanded( - child: viewModel.image == null - ? CameraViewfinder(onPictureTaken: viewModel.handlePictureTaken) - : OcrImage( - key: ValueKey(viewModel.image!.path.hashCode), - image: viewModel.image!, - onImageProcessed: viewModel.handleImageProcessed, - onImageError: viewModel.handleImageError, - onTextSelected: (text) { - searchController.text = text; - viewModel.handleTextSelected(text); - }, - locked: true, - singleSelection: true, - ), + child: viewModel.ocrError + ? Center( + child: Text( + 'Failed to process image', + style: TextStyle(fontSize: 18), + ), + ) + : viewModel.image == null + ? CameraViewfinder( + onPictureTaken: viewModel.handlePictureTaken) + : OcrImage( + key: ValueKey(viewModel.image!.path.hashCode), + image: viewModel.image!, + onImageError: viewModel.handleImageError, + onTextSelected: (text) { + searchController.text = text; + viewModel.handleTextSelected(text); + }, + locked: true, + singleSelection: true, + ), ), ], ), diff --git a/lib/ui/widgets/camera_viewfinder.dart b/lib/ui/widgets/camera_viewfinder.dart index 3b00af9..c678238 100644 --- a/lib/ui/widgets/camera_viewfinder.dart +++ b/lib/ui/widgets/camera_viewfinder.dart @@ -119,6 +119,7 @@ class _CameraViewfinderState extends State child: Center( child: FloatingActionButton( onPressed: _takePhoto, + backgroundColor: Colors.deepPurple, child: const Icon(Icons.camera_alt), ), ), diff --git a/lib/ui/widgets/ocr_image.dart b/lib/ui/widgets/ocr_image.dart index 6ddfbe7..4968542 100644 --- a/lib/ui/widgets/ocr_image.dart +++ b/lib/ui/widgets/ocr_image.dart @@ -14,7 +14,7 @@ import 'package:shimmer/shimmer.dart'; class OcrImage extends StatefulWidget { final XFile image; - final void Function() onImageProcessed; + final void Function(int)? onImageProcessed; final void Function() onImageError; final void Function(String) onTextSelected; final bool locked; @@ -23,7 +23,7 @@ class OcrImage extends StatefulWidget { const OcrImage({ super.key, required this.image, - required this.onImageProcessed, + this.onImageProcessed, required this.onImageError, required this.locked, required this.onTextSelected, @@ -96,7 +96,10 @@ class _OcrImageState extends State { } setState(() {}); - widget.onImageProcessed(); + + if (widget.onImageProcessed != null) { + widget.onImageProcessed!(_recognizedTextBlocks!.length); + } await rotatedImage.delete(); } @@ -206,15 +209,15 @@ class _OcrImage extends StatelessWidget { child: FittedBox( clipBehavior: Clip.hardEdge, fit: BoxFit.contain, - child: CustomPaint( - foregroundPainter: recognizedTextBlocks == null - ? null - : OcrPainter( - recognizedTextBlocks!, - imageSize, - onSelect, - ), - child: Image.memory(image), + child: CustomPaint( + foregroundPainter: recognizedTextBlocks == null + ? null + : OcrPainter( + recognizedTextBlocks!, + imageSize, + onSelect, + ), + child: Image.memory(image), ), ), ), From d8a642ebbd68cb1489af58f481b5c3465f191bf4 Mon Sep 17 00:00:00 2001 From: Moseco Date: Sun, 21 Sep 2025 15:22:30 +0900 Subject: [PATCH 07/20] fix hit test --- lib/ui/painters/ocr_painter.dart | 109 ++++++++----------------------- lib/ui/views/ocr/ocr_view.dart | 31 ++++++--- lib/ui/widgets/ocr_image.dart | 89 ++++++++++++++++++++----- 3 files changed, 122 insertions(+), 107 deletions(-) diff --git a/lib/ui/painters/ocr_painter.dart b/lib/ui/painters/ocr_painter.dart index 312633e..a5c6598 100644 --- a/lib/ui/painters/ocr_painter.dart +++ b/lib/ui/painters/ocr_painter.dart @@ -1,44 +1,44 @@ -import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:sagase/datamodels/recognized_text_block.dart'; class OcrPainter extends CustomPainter { - final List recognizedTextBlocks; + final List? recognizedTextBlocks; final Size imageSize; - final void Function(RecognizedTextBlock) onSelect; - OcrPainter(this.recognizedTextBlocks, this.imageSize, this.onSelect); + OcrPainter(this.recognizedTextBlocks, this.imageSize); @override void paint(Canvas canvas, Size size) { - final unselectedPaint = Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = 2 - ..color = Colors.lightBlueAccent; - - final selectedPaint = Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = 2 - ..color = Colors.lightGreenAccent; - - for (final textBlock in recognizedTextBlocks) { - final List cornerPoints = []; - for (final point in textBlock.points) { - double x = point.x.toDouble() * size.width / imageSize.width; - double y = point.y.toDouble() * size.height / imageSize.height; - cornerPoints.add(Offset(x, y)); + if (recognizedTextBlocks != null) { + final unselectedPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2 + ..color = Colors.lightBlueAccent; + + final selectedPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2 + ..color = Colors.lightGreenAccent; + + for (final textBlock in recognizedTextBlocks!) { + final List cornerPoints = []; + for (final point in textBlock.points) { + double x = point.x.toDouble() * size.width / imageSize.width; + double y = point.y.toDouble() * size.height / imageSize.height; + cornerPoints.add(Offset(x, y)); + } + + textBlock.offsets = List.from(cornerPoints); + + cornerPoints.add(cornerPoints.first); + canvas.drawPoints( + PointMode.polygon, + cornerPoints, + textBlock.selected ? selectedPaint : unselectedPaint, + ); } - - textBlock.offsets = List.from(cornerPoints); - - cornerPoints.add(cornerPoints.first); - canvas.drawPoints( - PointMode.polygon, - cornerPoints, - textBlock.selected ? selectedPaint : unselectedPaint, - ); } } @@ -46,55 +46,4 @@ class OcrPainter extends CustomPainter { bool shouldRepaint(OcrPainter oldDelegate) { return oldDelegate.recognizedTextBlocks != recognizedTextBlocks; } - - @override - bool? hitTest(Offset position) { - for (final textBlock in recognizedTextBlocks) { - if (_pointInsideRectangle(position, textBlock.offsets)) { - onSelect(textBlock); - return true; - } - } - - return false; - } - - bool _pointInsideRectangle(Offset point, List rectCorners) { - double x1 = rectCorners[0].dx; - double x2 = rectCorners[1].dx; - double x3 = rectCorners[2].dx; - double x4 = rectCorners[3].dx; - - double y1 = rectCorners[0].dy; - double y2 = rectCorners[1].dy; - double y3 = rectCorners[2].dy; - double y4 = rectCorners[3].dy; - - double a1 = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); - double a2 = sqrt((x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3)); - double a3 = sqrt((x3 - x4) * (x3 - x4) + (y3 - y4) * (y3 - y4)); - double a4 = sqrt((x4 - x1) * (x4 - x1) + (y4 - y1) * (y4 - y1)); - - double b1 = sqrt( - (x1 - point.dx) * (x1 - point.dx) + (y1 - point.dy) * (y1 - point.dy)); - double b2 = sqrt( - (x2 - point.dx) * (x2 - point.dx) + (y2 - point.dy) * (y2 - point.dy)); - double b3 = sqrt( - (x3 - point.dx) * (x3 - point.dx) + (y3 - point.dy) * (y3 - point.dy)); - double b4 = sqrt( - (x4 - point.dx) * (x4 - point.dx) + (y4 - point.dy) * (y4 - point.dy)); - - double u1 = (a1 + b1 + b2) / 2; - double u2 = (a2 + b2 + b3) / 2; - double u3 = (a3 + b3 + b4) / 2; - double u4 = (a4 + b4 + b1) / 2; - - double area1 = sqrt(u1 * (u1 - a1) * (u1 - b1) * (u1 - b2)); - double area2 = sqrt(u2 * (u2 - a2) * (u2 - b2) * (u2 - b3)); - double area3 = sqrt(u3 * (u3 - a3) * (u3 - b3) * (u3 - b4)); - double area4 = sqrt(u4 * (u4 - a4) * (u4 - b4) * (u4 - b1)); - - double difference = 0.95 * (area1 + area2 + area3 + area4) - a1 * a2; - return difference < 1; - } } diff --git a/lib/ui/views/ocr/ocr_view.dart b/lib/ui/views/ocr/ocr_view.dart index 4754347..0ef4055 100644 --- a/lib/ui/views/ocr/ocr_view.dart +++ b/lib/ui/views/ocr/ocr_view.dart @@ -36,17 +36,23 @@ class _Body extends StackedHookView { ? null : [ IconButton( - onPressed: viewModel.openCamera, + onPressed: () { + viewModel.openCamera(); + controller.clear(); + }, icon: Icon(Icons.camera), ), IconButton( - onPressed: viewModel.selectImage, + onPressed: () { + viewModel.selectImage(); + controller.clear(); + }, icon: Icon(Icons.photo), ), ], ), body: viewModel.state == OcrState.error - ? const _Error() + ? _Error(controller) : Column( children: [ Expanded( @@ -129,7 +135,6 @@ class _SelectedText extends ViewModelWidget { padding: const EdgeInsets.all(8), child: TextField( controller: controller, - autofocus: true, maxLines: null, style: const TextStyle(fontSize: 24), decoration: const InputDecoration.collapsed( @@ -169,7 +174,9 @@ class _SelectedText extends ViewModelWidget { } class _Error extends ViewModelWidget { - const _Error(); + final TextEditingController controller; + + const _Error(this.controller); @override Widget build(BuildContext context, OcrViewModel viewModel) { @@ -185,14 +192,20 @@ class _Error extends ViewModelWidget { const Text('Please try again'), const SizedBox(height: 16), ElevatedButton.icon( - onPressed: () => viewModel.openCamera(), + onPressed: () { + viewModel.openCamera(); + controller.clear(); + }, icon: const Icon(Icons.camera), - label: const Text('Take photo'), + label: const Text('Open camera'), ), ElevatedButton.icon( - onPressed: () => viewModel.selectImage(), + onPressed: () { + viewModel.selectImage(); + controller.clear(); + }, icon: const Icon(Icons.photo), - label: const Text('Select photo'), + label: const Text('Pick from photos'), ), ], ), diff --git a/lib/ui/widgets/ocr_image.dart b/lib/ui/widgets/ocr_image.dart index 4968542..a2710bf 100644 --- a/lib/ui/widgets/ocr_image.dart +++ b/lib/ui/widgets/ocr_image.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; import 'package:camera/camera.dart'; @@ -184,15 +185,16 @@ class _OcrImage extends StatelessWidget { child: FittedBox( clipBehavior: Clip.hardEdge, fit: BoxFit.cover, - child: CustomPaint( - foregroundPainter: recognizedTextBlocks == null - ? null - : OcrPainter( - recognizedTextBlocks!, - imageSize, - onSelect, - ), - child: Image.memory(image), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTapUp: _handleTapUp, + child: CustomPaint( + foregroundPainter: OcrPainter( + recognizedTextBlocks!, + imageSize, + ), + child: Image.memory(image), + ), ), ), ); @@ -209,15 +211,16 @@ class _OcrImage extends StatelessWidget { child: FittedBox( clipBehavior: Clip.hardEdge, fit: BoxFit.contain, - child: CustomPaint( - foregroundPainter: recognizedTextBlocks == null - ? null - : OcrPainter( - recognizedTextBlocks!, - imageSize, - onSelect, - ), - child: Image.memory(image), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTapUp: _handleTapUp, + child: CustomPaint( + foregroundPainter: OcrPainter( + recognizedTextBlocks, + imageSize, + ), + child: Image.memory(image), + ), ), ), ), @@ -226,4 +229,54 @@ class _OcrImage extends StatelessWidget { ); } } + + void _handleTapUp(TapUpDetails details) { + if (recognizedTextBlocks != null) { + for (final textBlock in recognizedTextBlocks!) { + if (_pointInsideRectangle(details.localPosition, textBlock.offsets)) { + onSelect(textBlock); + break; + } + } + } + } + + bool _pointInsideRectangle(Offset point, List rectCorners) { + double x1 = rectCorners[0].dx; + double x2 = rectCorners[1].dx; + double x3 = rectCorners[2].dx; + double x4 = rectCorners[3].dx; + + double y1 = rectCorners[0].dy; + double y2 = rectCorners[1].dy; + double y3 = rectCorners[2].dy; + double y4 = rectCorners[3].dy; + + double a1 = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); + double a2 = sqrt((x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3)); + double a3 = sqrt((x3 - x4) * (x3 - x4) + (y3 - y4) * (y3 - y4)); + double a4 = sqrt((x4 - x1) * (x4 - x1) + (y4 - y1) * (y4 - y1)); + + double b1 = sqrt( + (x1 - point.dx) * (x1 - point.dx) + (y1 - point.dy) * (y1 - point.dy)); + double b2 = sqrt( + (x2 - point.dx) * (x2 - point.dx) + (y2 - point.dy) * (y2 - point.dy)); + double b3 = sqrt( + (x3 - point.dx) * (x3 - point.dx) + (y3 - point.dy) * (y3 - point.dy)); + double b4 = sqrt( + (x4 - point.dx) * (x4 - point.dx) + (y4 - point.dy) * (y4 - point.dy)); + + double u1 = (a1 + b1 + b2) / 2; + double u2 = (a2 + b2 + b3) / 2; + double u3 = (a3 + b3 + b4) / 2; + double u4 = (a4 + b4 + b1) / 2; + + double area1 = sqrt(u1 * (u1 - a1) * (u1 - b1) * (u1 - b2)); + double area2 = sqrt(u2 * (u2 - a2) * (u2 - b2) * (u2 - b3)); + double area3 = sqrt(u3 * (u3 - a3) * (u3 - b3) * (u3 - b4)); + double area4 = sqrt(u4 * (u4 - a4) * (u4 - b4) * (u4 - b1)); + + double difference = 0.95 * (area1 + area2 + area3 + area4) - a1 * a2; + return difference < 1; + } } From 1a63358e04e40cdfce195568800b5bf4c86456ee Mon Sep 17 00:00:00 2001 From: Moseco Date: Sun, 21 Sep 2025 15:39:39 +0900 Subject: [PATCH 08/20] add keyboard dismiss --- lib/ui/views/ocr/ocr_view.dart | 75 ++++++++++++++------------- lib/ui/widgets/camera_viewfinder.dart | 1 + 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/lib/ui/views/ocr/ocr_view.dart b/lib/ui/views/ocr/ocr_view.dart index 0ef4055..1570763 100644 --- a/lib/ui/views/ocr/ocr_view.dart +++ b/lib/ui/views/ocr/ocr_view.dart @@ -126,49 +126,52 @@ class _SelectedText extends ViewModelWidget { @override Widget build(BuildContext context, OcrViewModel viewModel) { - return Column( - children: [ - Expanded( - child: SafeArea( - bottom: false, - child: Padding( - padding: const EdgeInsets.all(8), - child: TextField( - controller: controller, - maxLines: null, - style: const TextStyle(fontSize: 24), - decoration: const InputDecoration.collapsed( - hintText: 'Select text from the image...', + return GestureDetector( + onVerticalDragStart: (_) => FocusScope.of(context).unfocus(), + child: Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.all(8), + child: TextField( + controller: controller, + maxLines: null, + style: const TextStyle(fontSize: 24), + decoration: const InputDecoration.collapsed( + hintText: 'Select text from the image...', + ), + maxLength: 1000, + inputFormatters: [LengthLimitingTextInputFormatter(1000)], ), - maxLength: 1000, - inputFormatters: [LengthLimitingTextInputFormatter(1000)], ), ), ), - ), - AnimatedCrossFade( - crossFadeState: controller.text.isEmpty - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 200), - firstChild: SizedBox.shrink(), - secondChild: Container( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom, - ), - width: double.infinity, - color: Colors.deepPurple, - child: TextButton.icon( - icon: const Icon(Icons.text_snippet, color: Colors.white), - label: const Text( - 'Analyze', - style: TextStyle(color: Colors.white), + AnimatedCrossFade( + crossFadeState: controller.text.isEmpty + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 200), + firstChild: SizedBox.shrink(), + secondChild: Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom, + ), + width: double.infinity, + color: Colors.deepPurple, + child: TextButton.icon( + icon: const Icon(Icons.text_snippet, color: Colors.white), + label: const Text( + 'Analyze', + style: TextStyle(color: Colors.white), + ), + onPressed: () => viewModel.analyzeText(controller.text), ), - onPressed: () => viewModel.analyzeText(controller.text), ), ), - ), - ], + ], + ), ); } } diff --git a/lib/ui/widgets/camera_viewfinder.dart b/lib/ui/widgets/camera_viewfinder.dart index c678238..29aeef4 100644 --- a/lib/ui/widgets/camera_viewfinder.dart +++ b/lib/ui/widgets/camera_viewfinder.dart @@ -119,6 +119,7 @@ class _CameraViewfinderState extends State child: Center( child: FloatingActionButton( onPressed: _takePhoto, + foregroundColor: Colors.white, backgroundColor: Colors.deepPurple, child: const Icon(Icons.camera_alt), ), From 1c4eb8af3a612179dd4fef12145ceaeb760ea8a9 Mon Sep 17 00:00:00 2001 From: Hampus Hammarlund Date: Sun, 21 Sep 2025 17:47:26 +0900 Subject: [PATCH 09/20] fix handling of new line characters --- lib/ui/views/search/search_view.dart | 7 ++++--- lib/ui/views/search/widgets/ocr_widget.dart | 1 + lib/ui/widgets/ocr_image.dart | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/ui/views/search/search_view.dart b/lib/ui/views/search/search_view.dart index e18bef3..aca9ff4 100644 --- a/lib/ui/views/search/search_view.dart +++ b/lib/ui/views/search/search_view.dart @@ -296,10 +296,11 @@ class _SearchHistory extends ViewModelWidget { onPressed: () async { final cdata = await Clipboard.getData(Clipboard.kTextPlain); if (cdata?.text != null) { - searchController.text = cdata!.text!; + final text = cdata!.text!.replaceAll('\n', ''); + searchController.text = text; searchController.selection = TextSelection.fromPosition( - TextPosition(offset: cdata.text!.length)); - viewModel.searchOnChange(cdata.text!); + TextPosition(offset: text.length)); + viewModel.searchOnChange(text); } }, ), diff --git a/lib/ui/views/search/widgets/ocr_widget.dart b/lib/ui/views/search/widgets/ocr_widget.dart index f70be05..a595816 100644 --- a/lib/ui/views/search/widgets/ocr_widget.dart +++ b/lib/ui/views/search/widgets/ocr_widget.dart @@ -79,6 +79,7 @@ class OcrWidget extends ViewModelWidget { image: viewModel.image!, onImageError: viewModel.handleImageError, onTextSelected: (text) { + text = text.replaceAll('\n', ''); searchController.text = text; viewModel.handleTextSelected(text); }, diff --git a/lib/ui/widgets/ocr_image.dart b/lib/ui/widgets/ocr_image.dart index a2710bf..bf9feb0 100644 --- a/lib/ui/widgets/ocr_image.dart +++ b/lib/ui/widgets/ocr_image.dart @@ -190,7 +190,7 @@ class _OcrImage extends StatelessWidget { onTapUp: _handleTapUp, child: CustomPaint( foregroundPainter: OcrPainter( - recognizedTextBlocks!, + recognizedTextBlocks, imageSize, ), child: Image.memory(image), From 57d4163c9fec7eedcd79af9dd31a3b36fad88d3f Mon Sep 17 00:00:00 2001 From: Hampus Hammarlund Date: Sun, 21 Sep 2025 18:27:40 +0900 Subject: [PATCH 10/20] change icon back to close --- lib/ui/views/search/search_view.dart | 2 +- lib/ui/views/search/widgets/hand_writing_input.dart | 2 +- lib/ui/views/search/widgets/ocr_widget.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ui/views/search/search_view.dart b/lib/ui/views/search/search_view.dart index aca9ff4..e08a74a 100644 --- a/lib/ui/views/search/search_view.dart +++ b/lib/ui/views/search/search_view.dart @@ -121,7 +121,7 @@ class _SearchTextField extends ViewModelWidget { (node) { return IconButton( onPressed: node.unfocus, - icon: const Icon(Icons.keyboard_arrow_down), + icon: const Icon(Icons.close), ); }, ], diff --git a/lib/ui/views/search/widgets/hand_writing_input.dart b/lib/ui/views/search/widgets/hand_writing_input.dart index e0bf95e..f0880bb 100644 --- a/lib/ui/views/search/widgets/hand_writing_input.dart +++ b/lib/ui/views/search/widgets/hand_writing_input.dart @@ -91,7 +91,7 @@ class HandWritingInput extends ViewModelWidget { ), IconButton( onPressed: () => viewModel.setInputMode(InputMode.text), - icon: const Icon(Icons.keyboard_arrow_down), + icon: const Icon(Icons.close), splashColor: Colors.transparent, highlightColor: Colors.transparent, ), diff --git a/lib/ui/views/search/widgets/ocr_widget.dart b/lib/ui/views/search/widgets/ocr_widget.dart index a595816..b53e682 100644 --- a/lib/ui/views/search/widgets/ocr_widget.dart +++ b/lib/ui/views/search/widgets/ocr_widget.dart @@ -55,7 +55,7 @@ class OcrWidget extends ViewModelWidget { ), IconButton( onPressed: () => viewModel.setInputMode(InputMode.text), - icon: const Icon(Icons.keyboard_arrow_down), + icon: const Icon(Icons.close), splashColor: Colors.transparent, highlightColor: Colors.transparent, ), From ffd4cedb03108fcbaac2aceceae425fd338ac8ef Mon Sep 17 00:00:00 2001 From: Hampus Hammarlund Date: Sun, 21 Sep 2025 19:01:45 +0900 Subject: [PATCH 11/20] increase painter stroke width --- lib/ui/painters/ocr_painter.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ui/painters/ocr_painter.dart b/lib/ui/painters/ocr_painter.dart index a5c6598..71a8a59 100644 --- a/lib/ui/painters/ocr_painter.dart +++ b/lib/ui/painters/ocr_painter.dart @@ -14,12 +14,12 @@ class OcrPainter extends CustomPainter { if (recognizedTextBlocks != null) { final unselectedPaint = Paint() ..style = PaintingStyle.stroke - ..strokeWidth = 2 + ..strokeWidth = 4 ..color = Colors.lightBlueAccent; final selectedPaint = Paint() ..style = PaintingStyle.stroke - ..strokeWidth = 2 + ..strokeWidth = 4 ..color = Colors.lightGreenAccent; for (final textBlock in recognizedTextBlocks!) { From ebd721ca52133aece9357acb7339931959c6c183 Mon Sep 17 00:00:00 2001 From: Moseco Date: Fri, 30 Jan 2026 20:51:55 +0900 Subject: [PATCH 12/20] build: update to flutter 3.38 and 16kb pages on android --- android/app/build.gradle.kts | 9 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle.kts | 2 +- ios/Podfile.lock | 172 +++--- ios/Runner.xcodeproj/project.pbxproj | 6 +- lib/datamodels/isar/dictionary_info.dart | 2 +- lib/datamodels/isar/dictionary_item.dart | 2 +- lib/datamodels/isar/dictionary_list.dart | 2 +- lib/datamodels/isar/flashcard_set.dart | 2 +- lib/datamodels/isar/kanji.dart | 2 +- lib/datamodels/isar/kanji_radical.dart | 2 +- lib/datamodels/isar/my_dictionary_list.dart | 2 +- .../isar/predefined_dictionary_list.dart | 2 +- lib/datamodels/isar/search_history_item.dart | 2 +- .../isar/spaced_repetition_data.dart | 2 +- lib/datamodels/isar/vocab.dart | 2 +- lib/services/isar_service.dart | 2 +- lib/ui/dialogs/percent_indicator_dialog.dart | 14 +- .../dictionary_list_viewmodel.dart | 4 +- pubspec.lock | 504 +++++++++--------- pubspec.yaml | 65 ++- test/helpers/isar_helper.dart | 2 +- test/services/isar_service_test.dart | 2 +- 23 files changed, 412 insertions(+), 394 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index dd693e0..f217698 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -17,16 +17,15 @@ keystoreProperties.load(FileInputStream(keystorePropertiesFile)) android { namespace = "dev.hammarlund.sagase" compileSdk = flutter.compileSdkVersion - // ndkVersion = flutter.ndkVersion - ndkVersion = "27.0.12077973" + ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() + jvmTarget = JavaVersion.VERSION_17.toString() } defaultConfig { diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index afa1e8e..02767eb 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index 27a702f..cd07485 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -18,7 +18,7 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.7.3" apply false + id("com.android.application") version "8.12.3" apply false id("org.jetbrains.kotlin.android") version "2.2.0" apply false id("com.google.gms.google-services") version "4.3.15" apply false id("com.google.firebase.crashlytics") version "2.8.1" apply false diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 285aef0..aefd02a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,71 +1,71 @@ PODS: - - app_links (0.0.2): + - app_links (7.0.0): - Flutter - camera_avfoundation (0.0.1): - Flutter - disk_space_plus (0.0.1): - Flutter - - Firebase/CoreOnly (12.0.0): - - FirebaseCore (~> 12.0.0) - - Firebase/Crashlytics (12.0.0): + - Firebase/CoreOnly (12.8.0): + - FirebaseCore (~> 12.8.0) + - Firebase/Crashlytics (12.8.0): - Firebase/CoreOnly - - FirebaseCrashlytics (~> 12.0.0) - - firebase_analytics (12.0.0): + - FirebaseCrashlytics (~> 12.8.0) + - firebase_analytics (12.1.1): - firebase_core - - FirebaseAnalytics (= 12.0.0) + - FirebaseAnalytics (= 12.8.0) - Flutter - - firebase_core (4.0.0): - - Firebase/CoreOnly (= 12.0.0) + - firebase_core (4.4.0): + - Firebase/CoreOnly (= 12.8.0) - Flutter - - firebase_crashlytics (5.0.0): - - Firebase/Crashlytics (= 12.0.0) + - firebase_crashlytics (5.0.7): + - Firebase/Crashlytics (= 12.8.0) - firebase_core - Flutter - - FirebaseAnalytics (12.0.0): - - FirebaseAnalytics/Default (= 12.0.0) - - FirebaseCore (~> 12.0.0) - - FirebaseInstallations (~> 12.0.0) + - FirebaseAnalytics (12.8.0): + - FirebaseAnalytics/Default (= 12.8.0) + - FirebaseCore (~> 12.8.0) + - FirebaseInstallations (~> 12.8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/Network (~> 8.1) - "GoogleUtilities/NSData+zlib (~> 8.1)" - nanopb (~> 3.30910.0) - - FirebaseAnalytics/Default (12.0.0): - - FirebaseCore (~> 12.0.0) - - FirebaseInstallations (~> 12.0.0) - - GoogleAppMeasurement/Default (= 12.0.0) + - FirebaseAnalytics/Default (12.8.0): + - FirebaseCore (~> 12.8.0) + - FirebaseInstallations (~> 12.8.0) + - GoogleAppMeasurement/Default (= 12.8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/Network (~> 8.1) - "GoogleUtilities/NSData+zlib (~> 8.1)" - nanopb (~> 3.30910.0) - - FirebaseCore (12.0.0): - - FirebaseCoreInternal (~> 12.0.0) + - FirebaseCore (12.8.0): + - FirebaseCoreInternal (~> 12.8.0) - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Logger (~> 8.1) - - FirebaseCoreExtension (12.0.0): - - FirebaseCore (~> 12.0.0) - - FirebaseCoreInternal (12.0.0): + - FirebaseCoreExtension (12.8.0): + - FirebaseCore (~> 12.8.0) + - FirebaseCoreInternal (12.8.0): - "GoogleUtilities/NSData+zlib (~> 8.1)" - - FirebaseCrashlytics (12.0.0): - - FirebaseCore (~> 12.0.0) - - FirebaseInstallations (~> 12.0.0) - - FirebaseRemoteConfigInterop (~> 12.0.0) - - FirebaseSessions (~> 12.0.0) + - FirebaseCrashlytics (12.8.0): + - FirebaseCore (~> 12.8.0) + - FirebaseInstallations (~> 12.8.0) + - FirebaseRemoteConfigInterop (~> 12.8.0) + - FirebaseSessions (~> 12.8.0) - GoogleDataTransport (~> 10.1) - GoogleUtilities/Environment (~> 8.1) - nanopb (~> 3.30910.0) - PromisesObjC (~> 2.4) - - FirebaseInstallations (12.0.0): - - FirebaseCore (~> 12.0.0) + - FirebaseInstallations (12.8.0): + - FirebaseCore (~> 12.8.0) - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/UserDefaults (~> 8.1) - PromisesObjC (~> 2.4) - - FirebaseRemoteConfigInterop (12.0.0) - - FirebaseSessions (12.0.0): - - FirebaseCore (~> 12.0.0) - - FirebaseCoreExtension (~> 12.0.0) - - FirebaseInstallations (~> 12.0.0) + - FirebaseRemoteConfigInterop (12.8.0) + - FirebaseSessions (12.8.0): + - FirebaseCore (~> 12.8.0) + - FirebaseCoreExtension (~> 12.8.0) + - FirebaseInstallations (~> 12.8.0) - GoogleDataTransport (~> 10.1) - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/UserDefaults (~> 8.1) @@ -91,27 +91,28 @@ PODS: - Flutter - google_mlkit_commons - GoogleMLKit/TextRecognition (~> 7.0.0) - - GoogleAdsOnDeviceConversion (2.1.0): + - GoogleAdsOnDeviceConversion (3.2.0): + - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Logger (~> 8.1) - GoogleUtilities/Network (~> 8.1) - nanopb (~> 3.30910.0) - - GoogleAppMeasurement/Core (12.0.0): + - GoogleAppMeasurement/Core (12.8.0): - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/Network (~> 8.1) - "GoogleUtilities/NSData+zlib (~> 8.1)" - nanopb (~> 3.30910.0) - - GoogleAppMeasurement/Default (12.0.0): - - GoogleAdsOnDeviceConversion (= 2.1.0) - - GoogleAppMeasurement/Core (= 12.0.0) - - GoogleAppMeasurement/IdentitySupport (= 12.0.0) + - GoogleAppMeasurement/Default (12.8.0): + - GoogleAdsOnDeviceConversion (~> 3.2.0) + - GoogleAppMeasurement/Core (= 12.8.0) + - GoogleAppMeasurement/IdentitySupport (= 12.8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/Network (~> 8.1) - "GoogleUtilities/NSData+zlib (~> 8.1)" - nanopb (~> 3.30910.0) - - GoogleAppMeasurement/IdentitySupport (12.0.0): - - GoogleAppMeasurement/Core (= 12.0.0) + - GoogleAppMeasurement/IdentitySupport (12.8.0): + - GoogleAppMeasurement/Core (= 12.8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/Network (~> 8.1) @@ -168,7 +169,7 @@ PODS: - Flutter - in_app_review (2.0.0): - Flutter - - isar_flutter_libs (1.0.0): + - isar_community_flutter_libs (1.0.0): - Flutter - mecab_dart (0.0.1): - Flutter @@ -208,9 +209,6 @@ PODS: - nanopb/encode (= 3.30910.0) - nanopb/decode (3.30910.0) - nanopb/encode (3.30910.0) - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - PromisesObjC (2.4.0) - PromisesSwift (2.4.0): - PromisesObjC (= 2.4.0) @@ -221,25 +219,25 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqlite3 (3.50.4): - - sqlite3/common (= 3.50.4) - - sqlite3/common (3.50.4) - - sqlite3/dbstatvtab (3.50.4): + - sqlite3 (3.51.1): + - sqlite3/common (= 3.51.1) + - sqlite3/common (3.51.1) + - sqlite3/dbstatvtab (3.51.1): - sqlite3/common - - sqlite3/fts5 (3.50.4): + - sqlite3/fts5 (3.51.1): - sqlite3/common - - sqlite3/math (3.50.4): + - sqlite3/math (3.51.1): - sqlite3/common - - sqlite3/perf-threadsafe (3.50.4): + - sqlite3/perf-threadsafe (3.51.1): - sqlite3/common - - sqlite3/rtree (3.50.4): + - sqlite3/rtree (3.51.1): - sqlite3/common - - sqlite3/session (3.50.4): + - sqlite3/session (3.51.1): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - FlutterMacOS - - sqlite3 (~> 3.50.4) + - sqlite3 (~> 3.51.1) - sqlite3/dbstatvtab - sqlite3/fts5 - sqlite3/math @@ -268,9 +266,8 @@ DEPENDENCIES: - GoogleMLKit/TextRecognitionJapanese (~> 7.0.0) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) + - isar_community_flutter_libs (from `.symlinks/plugins/isar_community_flutter_libs/ios`) - mecab_dart (from `.symlinks/plugins/mecab_dart/ios`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - security_scoped_resource (from `.symlinks/plugins/security_scoped_resource/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) @@ -342,12 +339,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker_ios/ios" in_app_review: :path: ".symlinks/plugins/in_app_review/ios" - isar_flutter_libs: - :path: ".symlinks/plugins/isar_flutter_libs/ios" + isar_community_flutter_libs: + :path: ".symlinks/plugins/isar_community_flutter_libs/ios" mecab_dart: :path: ".symlinks/plugins/mecab_dart/ios" - path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/darwin" security_scoped_resource: :path: ".symlinks/plugins/security_scoped_resource/ios" share_plus: @@ -360,22 +355,22 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 - camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436 + app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8 + camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741 disk_space_plus: a36391fb5f732dbdd29628b0bac5a1acbd43aaef - Firebase: 800d487043c0557d9faed71477a38d9aafb08a41 - firebase_analytics: cd56fc56f75c1df30a6ff5290cd56e230996a76d - firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4 - firebase_crashlytics: 2c6c1a17900a38081d938330e9f48e60ec5b255d - FirebaseAnalytics: 6d790cd1b159b4eb61a99948df0934ce505a34f7 - FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a - FirebaseCoreExtension: 639afb3de6abd611952be78a794c54a47fa0f361 - FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55 - FirebaseCrashlytics: db75aa0cab8d00f68406fa247c32fe17ade884d7 - FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988 - FirebaseRemoteConfigInterop: bfa0ea72ba3dc5af739777296424e46bd6f42613 - FirebaseSessions: 4e784acda213108aafef536535cdfc03504acc42 - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d + firebase_analytics: b5a19eaf3e4bf4187b0815ef4850b8916e2bc549 + firebase_core: ee30637e6744af8e0c12a6a1e8a9718506ec2398 + firebase_crashlytics: 28b8f39df8104131376393e6af658b8b77dd120f + FirebaseAnalytics: f20bbad8cb7f65d8a5eaefeb424ae8800a31bdfc + FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c + FirebaseCoreExtension: 6605938d51f765d8b18bfcafd2085276a252bee2 + FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21 + FirebaseCrashlytics: fb31c6907e5b52aa252668394d3f1ab326df1511 + FirebaseInstallations: 6a14ab3d694ebd9f839c48d330da5547e9ca9dc0 + FirebaseRemoteConfigInterop: 869ddca16614f979e5c931ece11fbb0b8729ed41 + FirebaseSessions: d614ca154c63dbbc6c10d6c38259c2162c4e7c9b + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_exif_rotation: 5406455759acacf1f7b1b83e85dbb545340d6e9c flutter_file_dialog: ca8d7fbd1772d4f0c2777b4ab20a7787ef4e7dd8 flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 @@ -383,16 +378,16 @@ SPEC CHECKSUMS: google_mlkit_commons: 2abe6a70e1824e431d16a51085cb475b672c8aab google_mlkit_digital_ink_recognition: 17bf08581ec4c778fe1ac525302fd3a10e8799e6 google_mlkit_text_recognition: ec2122ec89bfe0d7200763336a6e4ef44810674c - GoogleAdsOnDeviceConversion: 2be6297a4f048459e0ae17fad9bfd2844e10cf64 - GoogleAppMeasurement: 8f6ab04ad6ae493b53fcf56bd26323fb2f1384f3 + GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f + GoogleAppMeasurement: 72c9a682fec6290327ea5e3c4b829b247fcb2c17 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 - image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a - in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457 - isar_flutter_libs: bc909e72c3d756c2759f14c8776c13b5b0556e26 + image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 + in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937 + isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d mecab_dart: 8690974dc789a7ee4a7ad3e77b21d896e2605728 MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56 MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d @@ -403,16 +398,15 @@ SPEC CHECKSUMS: MLKitTextRecognitionJapanese: 4a62c24ecf1cbaeccc8a235c81c0ba397807d7d8 MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 security_scoped_resource: 8b95b9c0e93ae4e03b60e9b6be9c2d61e3751ec1 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b - sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b + sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41 SSZipArchive: 8a6ee5677c8e304bebc109e39cf0da91ccef22ea - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b PODFILE CHECKSUM: 0b38879f6f651cad7f2e853fc0d85e537aca37ff diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 0edb69e..540eeca 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -387,7 +387,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -470,7 +470,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -519,7 +519,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/lib/datamodels/isar/dictionary_info.dart b/lib/datamodels/isar/dictionary_info.dart index 824c5c0..e97a8eb 100644 --- a/lib/datamodels/isar/dictionary_info.dart +++ b/lib/datamodels/isar/dictionary_info.dart @@ -1,4 +1,4 @@ -import 'package:isar/isar.dart'; +import 'package:isar_community/isar.dart'; part 'dictionary_info.g.dart'; diff --git a/lib/datamodels/isar/dictionary_item.dart b/lib/datamodels/isar/dictionary_item.dart index cd14d91..f8ce414 100644 --- a/lib/datamodels/isar/dictionary_item.dart +++ b/lib/datamodels/isar/dictionary_item.dart @@ -1,4 +1,4 @@ -import 'package:isar/isar.dart'; +import 'package:isar_community/isar.dart'; import 'package:sagase/datamodels/isar/spaced_repetition_data.dart'; abstract class DictionaryItem { diff --git a/lib/datamodels/isar/dictionary_list.dart b/lib/datamodels/isar/dictionary_list.dart index 207b797..46e353f 100644 --- a/lib/datamodels/isar/dictionary_list.dart +++ b/lib/datamodels/isar/dictionary_list.dart @@ -1,4 +1,4 @@ -import 'package:isar/isar.dart'; +import 'package:isar_community/isar.dart'; abstract class DictionaryList { Id id = Isar.autoIncrement; diff --git a/lib/datamodels/isar/flashcard_set.dart b/lib/datamodels/isar/flashcard_set.dart index 5390527..e492e30 100644 --- a/lib/datamodels/isar/flashcard_set.dart +++ b/lib/datamodels/isar/flashcard_set.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:isar/isar.dart'; +import 'package:isar_community/isar.dart'; import 'package:sagase_dictionary/sagase_dictionary.dart' show SagaseDictionaryConstants; diff --git a/lib/datamodels/isar/kanji.dart b/lib/datamodels/isar/kanji.dart index 74a5fdf..e8f7a8b 100644 --- a/lib/datamodels/isar/kanji.dart +++ b/lib/datamodels/isar/kanji.dart @@ -1,4 +1,4 @@ -import 'package:isar/isar.dart'; +import 'package:isar_community/isar.dart'; import 'package:sagase/datamodels/isar/dictionary_item.dart'; import 'package:sagase/datamodels/isar/spaced_repetition_data.dart'; diff --git a/lib/datamodels/isar/kanji_radical.dart b/lib/datamodels/isar/kanji_radical.dart index d0ab7d4..a898af1 100644 --- a/lib/datamodels/isar/kanji_radical.dart +++ b/lib/datamodels/isar/kanji_radical.dart @@ -1,4 +1,4 @@ -import 'package:isar/isar.dart'; +import 'package:isar_community/isar.dart'; part 'kanji_radical.g.dart'; diff --git a/lib/datamodels/isar/my_dictionary_list.dart b/lib/datamodels/isar/my_dictionary_list.dart index 4e323a8..b6542b1 100644 --- a/lib/datamodels/isar/my_dictionary_list.dart +++ b/lib/datamodels/isar/my_dictionary_list.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:isar/isar.dart'; +import 'package:isar_community/isar.dart'; import 'package:sagase/datamodels/isar/dictionary_list.dart'; import 'package:sagase_dictionary/sagase_dictionary.dart' show SagaseDictionaryConstants; diff --git a/lib/datamodels/isar/predefined_dictionary_list.dart b/lib/datamodels/isar/predefined_dictionary_list.dart index cc0cdfd..00e3f05 100644 --- a/lib/datamodels/isar/predefined_dictionary_list.dart +++ b/lib/datamodels/isar/predefined_dictionary_list.dart @@ -1,4 +1,4 @@ -import 'package:isar/isar.dart'; +import 'package:isar_community/isar.dart'; import 'package:sagase/datamodels/isar/dictionary_list.dart'; part 'predefined_dictionary_list.g.dart'; diff --git a/lib/datamodels/isar/search_history_item.dart b/lib/datamodels/isar/search_history_item.dart index d5e3d36..ec4bf44 100644 --- a/lib/datamodels/isar/search_history_item.dart +++ b/lib/datamodels/isar/search_history_item.dart @@ -1,4 +1,4 @@ -import 'package:isar/isar.dart'; +import 'package:isar_community/isar.dart'; part 'search_history_item.g.dart'; diff --git a/lib/datamodels/isar/spaced_repetition_data.dart b/lib/datamodels/isar/spaced_repetition_data.dart index 29f2654..8759f4c 100644 --- a/lib/datamodels/isar/spaced_repetition_data.dart +++ b/lib/datamodels/isar/spaced_repetition_data.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:math'; -import 'package:isar/isar.dart'; +import 'package:isar_community/isar.dart'; import 'package:sagase_dictionary/sagase_dictionary.dart' show SagaseDictionaryConstants; diff --git a/lib/datamodels/isar/vocab.dart b/lib/datamodels/isar/vocab.dart index 13d5073..350429d 100644 --- a/lib/datamodels/isar/vocab.dart +++ b/lib/datamodels/isar/vocab.dart @@ -1,4 +1,4 @@ -import 'package:isar/isar.dart'; +import 'package:isar_community/isar.dart'; import 'package:sagase/datamodels/isar/dictionary_item.dart'; import 'package:sagase/datamodels/isar/kanji.dart'; import 'package:sagase/datamodels/isar/spaced_repetition_data.dart'; diff --git a/lib/services/isar_service.dart b/lib/services/isar_service.dart index 9538901..8e77c01 100644 --- a/lib/services/isar_service.dart +++ b/lib/services/isar_service.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:isar/isar.dart'; +import 'package:isar_community/isar.dart'; import 'package:sagase/datamodels/isar/dictionary_info.dart'; import 'package:sagase/datamodels/isar/flashcard_set.dart'; import 'package:sagase/datamodels/isar/kanji.dart'; diff --git a/lib/ui/dialogs/percent_indicator_dialog.dart b/lib/ui/dialogs/percent_indicator_dialog.dart index 21daf88..39ed17a 100644 --- a/lib/ui/dialogs/percent_indicator_dialog.dart +++ b/lib/ui/dialogs/percent_indicator_dialog.dart @@ -23,12 +23,14 @@ class _PercentIndicatorDialogState extends State { initState() { super.initState(); - widget.request.data.listen((double event) { - double newStatus = (event * 100).floorToDouble() / 100; - if (newStatus != _downloadStatus) { - setState(() { - _downloadStatus = newStatus; - }); + widget.request.data.listen((double? event) { + if (event != null) { + double newStatus = (event * 100).floorToDouble() / 100; + if (newStatus != _downloadStatus) { + setState(() { + _downloadStatus = newStatus; + }); + } } }); } diff --git a/lib/ui/views/dictionary_list/dictionary_list_viewmodel.dart b/lib/ui/views/dictionary_list/dictionary_list_viewmodel.dart index 43934e4..0ea29a2 100644 --- a/lib/ui/views/dictionary_list/dictionary_list_viewmodel.dart +++ b/lib/ui/views/dictionary_list/dictionary_list_viewmodel.dart @@ -167,7 +167,9 @@ class DictionaryListViewModel extends FutureViewModel { .toShareJson()); // Share the file - await Share.shareXFiles([XFile(file.path)]); + SharePlus.instance.share( + ShareParams(files: [XFile(file.path)]), + ); } } diff --git a/pubspec.lock b/pubspec.lock index 390d243..748cc7d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,39 +5,34 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f url: "https://pub.dev" source: hosted - version: "76.0.0" + version: "85.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - sha256: bb84ee51e527053dd8e25ecc9f97a6abfdc19130fb4d883e4e8585e23e7e6dd8 + sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182 url: "https://pub.dev" source: hosted - version: "1.3.60" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "1.3.66" analyzer: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "7.7.1" app_links: dependency: "direct main" description: name: app_links - sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" + sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc" url: "https://pub.dev" source: hosted - version: "6.4.0" + version: "7.0.0" app_links_linux: dependency: transitive description: @@ -98,50 +93,50 @@ packages: dependency: transitive description: name: build - sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "3.1.0" build_config: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" build_daemon: dependency: transitive description: name: build_daemon - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + sha256: d1d57f7807debd7349b4726a19fd32ec8bc177c71ad0febf91a20f84cd2d4b46 url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "3.0.3" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30 url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.7.1" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b" url: "https://pub.dev" source: hosted - version: "9.1.2" + version: "9.3.1" built_collection: dependency: transitive description: @@ -154,50 +149,50 @@ packages: dependency: transitive description: name: built_value - sha256: ba95c961bafcd8686d1cf63be864eb59447e795e124d98d6a27d91fcd13602fb + sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" url: "https://pub.dev" source: hosted - version: "8.11.1" + version: "8.12.3" camera: dependency: "direct main" description: name: camera - sha256: d6ec2cbdbe2fa8f5e0d07d8c06368fe4effa985a4a5ddade9cc58a8cd849557d + sha256: eefad89f262a873f38d21e5eec853461737ea074d7c9ede39f3ceb135d201cab url: "https://pub.dev" source: hosted - version: "0.11.2" + version: "0.11.3" camera_android_camerax: dependency: transitive description: name: camera_android_camerax - sha256: "58b8fe843a3c83fd1273c00cb35f5a8ae507f6cc9b2029bcf7e2abba499e28d8" + sha256: "1abae0a9853401ee875d7f99e9ec5083335f0aab6edf6eb9a219756143a1b3a8" url: "https://pub.dev" source: hosted - version: "0.6.19+1" + version: "0.6.29" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "180c1b368cf3485a18c46e219e3ad80271a779a6d66a749376e31244fd927cde" + sha256: a600b60a7752cc5fa9de476cd0055539d7a3b9d62662f4f446bae49eba2267df url: "https://pub.dev" source: hosted - version: "0.9.20+7" + version: "0.9.22+9" camera_platform_interface: dependency: transitive description: name: camera_platform_interface - sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2" + sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.12.0" camera_web: dependency: transitive description: name: camera_web - sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" + sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7" url: "https://pub.dev" source: hosted - version: "0.3.5" + version: "0.3.5+3" characters: dependency: transitive description: @@ -230,14 +225,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" code_builder: dependency: transitive description: name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" url: "https://pub.dev" source: hosted - version: "4.10.1" + version: "4.11.1" collection: dependency: transitive description: @@ -258,18 +261,18 @@ packages: dependency: transitive description: name: cross_file - sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" url: "https://pub.dev" source: hosted - version: "0.3.4+2" + version: "0.3.5+1" crypto: dependency: transitive description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -282,10 +285,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "3.1.1" dartx: dependency: transitive description: @@ -306,10 +309,10 @@ packages: dependency: "direct main" description: name: dio - sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25 url: "https://pub.dev" source: hosted - version: "5.9.0" + version: "5.9.1" dio_web_adapter: dependency: transitive description: @@ -338,26 +341,26 @@ packages: dependency: "direct main" description: name: drift - sha256: "14a61af39d4584faf1d73b5b35e4b758a43008cf4c0fdb0576ec8e7032c0d9a5" + sha256: "3669e1b68d7bffb60192ac6ba9fd2c0306804d7a00e5879f6364c69ecde53a7f" url: "https://pub.dev" source: hosted - version: "2.26.0" + version: "2.30.0" drift_flutter: dependency: "direct main" description: name: drift_flutter - sha256: ccfb42bc942e59f81500b16228df59cf8eb40d2fbd96637ff677df923350af7b + sha256: c07120854742a0cae2f7501a0da02493addde550db6641d284983c08762e60a7 url: "https://pub.dev" source: hosted - version: "0.2.5" + version: "0.2.8" equatable: dependency: transitive description: name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.8" fake_async: dependency: transitive description: @@ -370,10 +373,10 @@ packages: dependency: transitive description: name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" file: dependency: transitive description: @@ -386,98 +389,98 @@ packages: dependency: transitive description: name: file_selector_linux - sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" url: "https://pub.dev" source: hosted - version: "0.9.3+2" + version: "0.9.4" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711" + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" url: "https://pub.dev" source: hosted - version: "0.9.4+3" + version: "0.9.5" file_selector_platform_interface: dependency: transitive description: name: file_selector_platform_interface - sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" url: "https://pub.dev" source: hosted - version: "2.6.2" + version: "2.7.0" file_selector_windows: dependency: transitive description: name: file_selector_windows - sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" url: "https://pub.dev" source: hosted - version: "0.9.3+4" + version: "0.9.3+5" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - sha256: "07146e89e11302c6b07e3465c2c556ebcdd0053a3c5b1aa9bfd3203b778e5b4c" + sha256: "91e2739bad690da2826c0cd5b28328fd15fb87adf54634cded703f34fd797a81" url: "https://pub.dev" source: hosted - version: "12.0.0" + version: "12.1.1" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - sha256: "27e81a0efc821bec6cba64abc1083b91c8ddbad28eeb4c6f6b7c78a59d06f259" + sha256: "62fd3f27f342c898bd819fb97fa87c0b971e9fbe03357477282c0e14e1a40c3c" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.0.6" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - sha256: "7d87f47462042a7d9125e3123db2783bc72917d85e2719d4cb6aeaec209605e1" + sha256: "8fc488bb008439fc3b850cfac892dec1ff4cd438eee44438919a14c5e61b9828" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1+2" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "6b343e6f7b72a4f32d7ce8df8c9a28d8f54b4ac20d7c6500f3e8b3969afca457" + sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.4.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "5dbc900677dcbe5873d22ad7fbd64b047750124f1f9b7ebe2a33b9ddccc838eb" + sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.0.2" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "5d28b14dd32282fb7ce2b22b897362453755b6b8541d491127dc72b755bb7b16" + sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.4.0" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: "95b6871850b1a7e3b09c284c59a0c71fafcad3eee8ac1b6f06aaf8979290cbb8" + sha256: a6e6cb8b2ea1214533a54e4c1b11b19c40f6a29333f3ab0854a479fdc3237c5b url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.0.7" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: ba5b7a916f1ebedc6db35b33abdc618f202fc25e0792088dfba698e19fec9c09 + sha256: fc6837c4c64c48fa94cab8a872a632b9194fa9208ca76a822f424b3da945584d url: "https://pub.dev" source: hosted - version: "3.8.11" + version: "3.8.17" fixnum: dependency: transitive description: @@ -490,10 +493,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: "577aeac8ca414c25333334d7c4bb246775234c0e44b38b10a82b559dd4d764e7" + sha256: "7ca9a40f4eb85949190e54087be8b4d6ac09dc4c54238d782a34cf1f7c011de9" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" flip_card: dependency: "direct main" description: @@ -521,10 +524,10 @@ packages: dependency: "direct main" description: name: flutter_file_dialog - sha256: "9344b8f07be6a1b6f9854b723fb0cf84a8094ba94761af1d213589d3cb087488" + sha256: ec904d15e7da3691bb60442a762b0a09afa37ded7265b9fc2088ec202b7d844f url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" flutter_hooks: dependency: "direct main" description: @@ -601,17 +604,17 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab" + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 url: "https://pub.dev" source: hosted - version: "2.0.29" + version: "2.0.33" flutter_statusbarcolor_ns: dependency: "direct overridden" description: path: "." - ref: d870cb2 - resolved-ref: d870cb252200b8273b61433fbc3047d01c544223 - url: "https://github.com/uintdev/flutter_statusbarcolor/" + ref: "3fd1a9b" + resolved-ref: "3fd1a9b933fb06a96d61f10559b5487326d967c0" + url: "https://github.com/moseco/flutter_statusbarcolor/" source: git version: "0.6.0" flutter_sticky_header: @@ -626,10 +629,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845 + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -644,10 +647,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.1.0" frontend_server_client: dependency: transitive description: @@ -660,18 +663,18 @@ packages: dependency: transitive description: name: get - sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 + sha256: "5ed34a7925b85336e15d472cc4cfe7d9ebf4ab8e8b9f688585bf6b50f4c3d79a" url: "https://pub.dev" source: hosted - version: "4.7.2" + version: "4.7.3" get_it: dependency: transitive description: name: get_it - sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b + sha256: ae78de7c3f2304b8d81f2bb6e320833e5e81de942188542328f074978cc0efa9 url: "https://pub.dev" source: hosted - version: "8.2.0" + version: "8.3.0" glob: dependency: transitive description: @@ -683,26 +686,29 @@ packages: google_mlkit_commons: dependency: transitive description: - name: google_mlkit_commons - sha256: "8f40fbac10685cad4715d11e6a0d86837d9ad7168684dfcad29610282a88e67a" - url: "https://pub.dev" - source: hosted + path: "packages/google_mlkit_commons" + ref: f339749 + resolved-ref: f3397499e96eb9901f7d259a13fb1521b03d992f + url: "https://github.com/Moseco/google_ml_kit_flutter" + source: git version: "0.11.0" google_mlkit_digital_ink_recognition: dependency: "direct main" description: - name: google_mlkit_digital_ink_recognition - sha256: "8d2b89401bdeeba97158377167429dbc5cb339ebbd21e0889dca773f1c79a884" - url: "https://pub.dev" - source: hosted + path: "packages/google_mlkit_digital_ink_recognition" + ref: ed12704 + resolved-ref: ed1270485158cad0f2bbea52d092a00f9d51eb6e + url: "https://github.com/Moseco/google_ml_kit_flutter" + source: git version: "0.14.1" google_mlkit_text_recognition: dependency: "direct main" description: - name: google_mlkit_text_recognition - sha256: "96173ad4dd7fd06c660e22ac3f9e9f1798a517fe7e48bee68eeec83853224224" - url: "https://pub.dev" - source: hosted + path: "packages/google_mlkit_text_recognition" + ref: ed12704 + resolved-ref: ed1270485158cad0f2bbea52d092a00f9d51eb6e + url: "https://github.com/Moseco/google_ml_kit_flutter" + source: git version: "0.15.0" google_nav_bar: dependency: "direct main" @@ -728,14 +734,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + hooks: + dependency: transitive + description: + name: hooks + sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" + url: "https://pub.dev" + source: hosted + version: "1.0.0" http: dependency: transitive description: name: http - sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.6.0" http_multi_server: dependency: transitive description: @@ -756,74 +770,74 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.1" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: b08e9a04d0f8d91f4a6e767a745b9871bfbc585410205c311d0492de20a7ccd6 + sha256: "518a16108529fc18657a3e6dde4a043dc465d16596d20ab2abd49a4cac2e703d" url: "https://pub.dev" source: hosted - version: "0.8.12+25" + version: "0.8.13+13" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.1" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + sha256: "46547f6d2812f31ade2c8088ec8cd51010cd6b72c17cf191901167b27776486e" url: "https://pub.dev" source: hosted - version: "0.8.12+2" + version: "0.8.13+5" image_picker_linux: dependency: transitive description: name: image_picker_linux - sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" url: "https://pub.dev" source: hosted - version: "0.2.1+2" + version: "0.2.2" image_picker_macos: dependency: transitive description: name: image_picker_macos - sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" url: "https://pub.dev" source: hosted - version: "0.2.1+2" + version: "0.2.2+1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" url: "https://pub.dev" source: hosted - version: "2.10.1" + version: "2.11.1" image_picker_windows: dependency: transitive description: name: image_picker_windows - sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae url: "https://pub.dev" source: hosted - version: "0.2.1+1" + version: "0.2.2" in_app_review: dependency: "direct main" description: name: in_app_review - sha256: "36a06771b88fb0e79985b15e7f2ac0f1142e903fe72517f3c055d78bc3bc1819" + sha256: ab26ac54dbd802896af78c670b265eaeab7ecddd6af4d0751e9604b60574817f url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.0.11" in_app_review_platform_interface: dependency: transitive description: @@ -856,30 +870,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - isar: + isar_community: dependency: "direct main" description: - name: isar - sha256: e17a9555bc7f22ff26568b8c64d019b4ffa2dc6bd4cb1c8d9b269aefd32e53ad - url: "https://pub.isar-community.dev" + name: isar_community + sha256: d92315e1862448f236489c2b2b1f9a7ad5ba2405f42d87216234be4fb2e1dd2d + url: "https://pub.dev" source: hosted - version: "3.1.8" - isar_flutter_libs: + version: "3.3.0" + isar_community_flutter_libs: dependency: "direct main" description: - name: isar_flutter_libs - sha256: "78710781e658ce4bff59b3f38c5b2735e899e627f4e926e1221934e77b95231a" - url: "https://pub.isar-community.dev" + name: isar_community_flutter_libs + sha256: "3c072d8d77e820196fa23839b88481c50d903c73b3c0db6e647b29509d04fe3b" + url: "https://pub.dev" source: hosted - version: "3.1.8" - isar_generator: + version: "3.3.0" + isar_community_generator: dependency: "direct dev" description: - name: isar_generator - sha256: "484e73d3b7e81dbd816852fe0b9497333118a9aeb646fd2d349a62cc8980ffe1" - url: "https://pub.isar-community.dev" + name: isar_community_generator + sha256: "6ca1487b7551850f7896443aa8079a12b23cdf71a99dafb1f567c83d6e031042" + url: "https://pub.dev" source: hosted - version: "3.1.8" + version: "3.3.0" js: dependency: transitive description: @@ -892,10 +906,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.10.0" kana_kit: dependency: "direct main" description: @@ -908,34 +922,34 @@ packages: dependency: "direct main" description: name: keyboard_actions - sha256: "31e0ab2a706ac8f58887efa60efc1f19aecdf37d8ab0f665a0f156d1fbeab650" + sha256: "5155a158c0d22c3a2f4a2192040445fe84977620cf0eeb29f6148a1dcb5835fa" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.2.1" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -948,10 +962,10 @@ packages: dependency: transitive description: name: logger - sha256: "7ad7215c15420a102ec687bb320a7312afd449bac63bfb1c60d9787c27b9767f" + sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "2.6.2" logging: dependency: transitive description: @@ -960,14 +974,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" markdown: dependency: transitive description: @@ -996,8 +1002,8 @@ packages: dependency: "direct main" description: path: "." - ref: "338acd9" - resolved-ref: "338acd9c5419cd50141b8394d2ea24d611573e14" + ref: bed21bb + resolved-ref: bed21bb9e1e3c035e27022c515ff57052da6ad70 url: "https://github.com/Moseco/mecab_dart" source: git version: "0.1.6" @@ -1005,10 +1011,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1021,10 +1027,18 @@ packages: dependency: "direct dev" description: name: mockito - sha256: f99d8d072e249f719a5531735d146d8cf04c580d93920b04de75bef6dfb2daf6 + sha256: "2314cbe9165bcd16106513df9cf3c3224713087f09723b128928dc11a4379f99" + url: "https://pub.dev" + source: hosted + version: "5.5.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" url: "https://pub.dev" source: hosted - version: "5.4.5" + version: "0.17.4" nested: dependency: transitive description: @@ -1033,6 +1047,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "983c7fa1501f6dcc0cb7af4e42072e9993cb28d73604d25ebf4dab08165d997e" + url: "https://pub.dev" + source: hosted + version: "9.2.5" package_config: dependency: transitive description: @@ -1069,18 +1091,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e url: "https://pub.dev" source: hosted - version: "2.2.17" + version: "2.2.22" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -1117,10 +1139,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "7.0.1" platform: dependency: transitive description: @@ -1141,10 +1163,10 @@ packages: dependency: transitive description: name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.2" posix: dependency: transitive description: @@ -1157,10 +1179,10 @@ packages: dependency: transitive description: name: provider - sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.1.5+1" pub_semver: dependency: transitive description: @@ -1206,8 +1228,8 @@ packages: dependency: "direct main" description: path: "." - ref: "4ea66ef" - resolved-ref: "4ea66efb580193c1c312efbeb828ffa44c33a999" + ref: "6e44fb2" + resolved-ref: "6e44fb245bb5892e64026bc7424eb53ba7cd532c" url: "https://github.com/Moseco/sagase_dictionary" source: git version: "1.0.0" @@ -1239,42 +1261,42 @@ packages: dependency: "direct main" description: name: share_plus - sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da + sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" url: "https://pub.dev" source: hosted - version: "10.1.4" + version: "12.0.1" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "6.1.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e" + sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.20" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.6" shared_preferences_linux: dependency: transitive description: @@ -1340,10 +1362,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "3.1.0" source_span: dependency: transitive description: @@ -1352,30 +1374,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" sqlite3: dependency: transitive description: name: sqlite3 - sha256: f393d92c71bdcc118d6203d07c991b9be0f84b1a6f89dd4f7eed348131329924 + sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2" url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.9.4" sqlite3_flutter_libs: dependency: transitive description: name: sqlite3_flutter_libs - sha256: "2b03273e71867a8a4d030861fc21706200debe5c5858a4b9e58f4a1c129586a4" + sha256: "1e800ebe7f85a80a66adacaa6febe4d5f4d8b75f244e9838a27cb2ffc7aec08d" url: "https://pub.dev" source: hosted - version: "0.5.39" + version: "0.5.41" stack_trace: dependency: transitive description: @@ -1388,18 +1402,18 @@ packages: dependency: "direct main" description: name: stacked - sha256: fe77da8b5dae6488a0caa0feea59c4f79a0fb11cd88a211f87f653411a4c142b + sha256: "5f4a6ba6cfa43c5854690de0f946eef1694250cf46ecf7859519d90bf764b1e4" url: "https://pub.dev" source: hosted - version: "3.4.4" + version: "3.5.0" stacked_generator: dependency: "direct dev" description: name: stacked_generator - sha256: eaa6447e3fd4d4010b746629b5518364d7fa7f6453ffb6416ad449fd352d1181 + sha256: "345e5eaa7ce58eb03402c33c57a0532da60264fe14f423e6e1de7c68ea4e9d94" url: "https://pub.dev" source: hosted - version: "1.6.1" + version: "2.0.0" stacked_hooks: dependency: "direct main" description: @@ -1468,18 +1482,18 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.7" time: dependency: transitive description: name: time - sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" + sha256: "46187cf30bffdab28c56be9a63861b36e4ab7347bf403297595d6a97e10c789f" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" timing: dependency: transitive description: @@ -1500,10 +1514,10 @@ packages: dependency: "direct main" description: name: tutorial_coach_mark - sha256: ccc4a2026d361d8a71011d0f131a2278add1a154ef43e33dfd165babbb551c17 + sha256: "5a325d53bcf16ce7a969e2ab8d4dc9610f39ee3eab2b3cc57d5c98936129b891" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" typed_data: dependency: transitive description: @@ -1516,10 +1530,10 @@ packages: dependency: transitive description: name: universal_io - sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.1" uri_to_file: dependency: "direct main" description: @@ -1541,34 +1555,34 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656" + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" url: "https://pub.dev" source: hosted - version: "6.3.17" + version: "6.3.28" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad url: "https://pub.dev" source: hosted - version: "6.3.3" + version: "6.3.6" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.5" url_launcher_platform_interface: dependency: transitive description: @@ -1581,26 +1595,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" uuid: dependency: transitive description: name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 url: "https://pub.dev" source: hosted - version: "4.5.1" + version: "4.5.2" value_layout_builder: dependency: transitive description: @@ -1629,34 +1643,34 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331" + sha256: "201e876b5d52753626af64b6359cd13ac6011b80728731428fd34bc840f71c9b" url: "https://pub.dev" source: hosted - version: "1.1.17" + version: "1.1.20" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "15.0.2" watcher: dependency: transitive description: name: watcher - sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.1" web: dependency: transitive description: @@ -1685,10 +1699,10 @@ packages: dependency: transitive description: name: win32 - sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e url: "https://pub.dev" source: hosted - version: "5.14.0" + version: "5.15.0" xdg_directories: dependency: transitive description: @@ -1701,10 +1715,10 @@ packages: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.1" xxh3: dependency: transitive description: @@ -1722,5 +1736,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0 <4.0.0" - flutter: ">=3.32.8" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.9" diff --git a/pubspec.yaml b/pubspec.yaml index ce088ac..1c6005c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,26 +7,32 @@ version: 1.4.2+23 environment: sdk: '>=3.0.5 <4.0.0' - flutter: 3.32.8 + flutter: 3.38.9 -isar_version: &isar_version 3.1.8 +isar_version: &isar_version 3.3.0 dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 + # Dictionary + sagase_dictionary: + git: + url: https://github.com/Moseco/sagase_dictionary + ref: '6e44fb2' + # State management - stacked: ^3.4.1 - stacked_services: ^1.1.1 - stacked_hooks: ^0.2.3 + stacked: ^3.5.0 + stacked_services: ^1.6.0 + stacked_hooks: ^0.2.4 flutter_hooks: any - stacked_themes: ^0.3.13 + stacked_themes: ^0.3.15 # Firebase - firebase_core: 4.0.0 - firebase_analytics: 12.0.0 - firebase_crashlytics: 5.0.0 + firebase_core: 4.4.0 + firebase_analytics: 12.1.1 + firebase_crashlytics: 5.0.7 # UI google_nav_bar: ^5.0.6 @@ -36,7 +42,7 @@ dependencies: url: https://github.com/Moseco/flip_card keyboard_actions: ^4.2.0 fl_chart: ^1.0.0 - flutter_svg: ^2.0.7 + flutter_svg: ^2.2.3 flutter_sticky_header: ^0.8.0 shimmer: ^3.0.0 ruby_text: @@ -51,36 +57,38 @@ dependencies: # Other drift: any # Version defined by sagase_dictionary drift_flutter: any - isar: + isar_community: version: *isar_version - hosted: https://pub.isar-community.dev/ - isar_flutter_libs: + isar_community_flutter_libs: version: *isar_version - hosted: https://pub.isar-community.dev/ path_provider: ^2.0.15 kana_kit: ^2.1.1 async: ^2.11.0 archive: ^4.0.5 - google_mlkit_digital_ink_recognition: ^0.14.1 - google_mlkit_text_recognition: ^0.15.0 + google_mlkit_digital_ink_recognition: + git: + url: https://github.com/Moseco/google_ml_kit_flutter + path: packages/google_mlkit_digital_ink_recognition + ref: 'ed12704' + google_mlkit_text_recognition: + git: + url: https://github.com/Moseco/google_ml_kit_flutter + path: packages/google_mlkit_text_recognition + ref: 'ed12704' intl: ^0.20.2 shared_preferences: ^2.3.5 mecab_dart: git: url: https://github.com/Moseco/mecab_dart - ref: '338acd9' + ref: 'bed21bb' path: ^1.8.3 - flutter_file_dialog: ^3.0.1 - sagase_dictionary: - git: - url: https://github.com/Moseco/sagase_dictionary - ref: '4ea66ef' + flutter_file_dialog: ^3.0.3 url_launcher: ^6.1.12 dio: ^5.3.3 in_app_review: ^2.0.8 disk_space_plus: ^0.2.6 - share_plus: ^10.1.4 - app_links: ^6.3.2 + share_plus: ^12.0.0 + app_links: ^7.0.0 uri_to_file: git: url: https://github.com/chan150/uri-to-file @@ -97,8 +105,8 @@ dependencies: dependency_overrides: flutter_statusbarcolor_ns: git: - url: https://github.com/uintdev/flutter_statusbarcolor/ - ref: 'd870cb2' + url: https://github.com/moseco/flutter_statusbarcolor/ + ref: '3fd1a9b' dev_dependencies: flutter_test: @@ -106,10 +114,9 @@ dev_dependencies: flutter_lints: ^6.0.0 build_runner: any - stacked_generator: ^1.4.0 - isar_generator: + stacked_generator: ^2.0.0 + isar_community_generator: version: *isar_version - hosted: https://pub.isar-community.dev/ mockito: ^5.4.2 plugin_platform_interface: ^2.1.4 path_provider_platform_interface: ^2.0.6 diff --git a/test/helpers/isar_helper.dart b/test/helpers/isar_helper.dart index 4ea85ac..c370789 100644 --- a/test/helpers/isar_helper.dart +++ b/test/helpers/isar_helper.dart @@ -1,7 +1,7 @@ import 'dart:ffi'; import 'dart:io'; -import 'package:isar/isar.dart'; +import 'package:isar_community/isar.dart'; import 'package:sagase/services/isar_service.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart' as path_provider; diff --git a/test/services/isar_service_test.dart b/test/services/isar_service_test.dart index 187fd5b..b62e4f8 100644 --- a/test/services/isar_service_test.dart +++ b/test/services/isar_service_test.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; -import 'package:isar/isar.dart'; +import 'package:isar_community/isar.dart'; import 'package:sagase/datamodels/isar/flashcard_set.dart'; import 'package:sagase/datamodels/isar/kanji.dart'; import 'package:sagase/datamodels/isar/my_dictionary_list.dart'; From 6c44c76ae2299d8a2248393032a55a29416284ff Mon Sep 17 00:00:00 2001 From: Moseco Date: Fri, 30 Jan 2026 21:09:12 +0900 Subject: [PATCH 13/20] fix: add temporary fix for iphone 17 camera --- ios/Podfile.lock | 6 ++++++ lib/ui/widgets/camera_viewfinder.dart | 11 ++++++++++- pubspec.lock | 24 ++++++++++++++++++++++++ pubspec.yaml | 1 + 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index aefd02a..0727976 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,6 +3,8 @@ PODS: - Flutter - camera_avfoundation (0.0.1): - Flutter + - device_info_plus (0.0.1): + - Flutter - disk_space_plus (0.0.1): - Flutter - Firebase/CoreOnly (12.8.0): @@ -251,6 +253,7 @@ PODS: DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - disk_space_plus (from `.symlinks/plugins/disk_space_plus/ios`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -311,6 +314,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/app_links/ios" camera_avfoundation: :path: ".symlinks/plugins/camera_avfoundation/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" disk_space_plus: :path: ".symlinks/plugins/disk_space_plus/ios" firebase_analytics: @@ -357,6 +362,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8 camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741 + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe disk_space_plus: a36391fb5f732dbdd29628b0bac5a1acbd43aaef Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d firebase_analytics: b5a19eaf3e4bf4187b0815ef4850b8916e2bc549 diff --git a/lib/ui/widgets/camera_viewfinder.dart b/lib/ui/widgets/camera_viewfinder.dart index 29aeef4..0691b18 100644 --- a/lib/ui/widgets/camera_viewfinder.dart +++ b/lib/ui/widgets/camera_viewfinder.dart @@ -1,5 +1,8 @@ +import 'dart:io' show Platform; + import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; +import 'package:device_info_plus/device_info_plus.dart'; class CameraViewfinder extends StatefulWidget { final void Function(XFile) onPictureTaken; @@ -37,9 +40,15 @@ class _CameraViewfinderState extends State break; } } + + // This is a temporary workaround for iPhone 17 family devices + final iOS = Platform.isIOS ? await DeviceInfoPlugin().iosInfo : null; + _controller = CameraController( cameraToUse, - ResolutionPreset.max, + iOS != null && iOS.utsname.machine.contains("iPhone18") + ? ResolutionPreset.ultraHigh + : ResolutionPreset.max, enableAudio: false, ); diff --git a/pubspec.lock b/pubspec.lock index 748cc7d..fad5ab8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -297,6 +297,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "4df8babf73058181227e18b08e6ea3520cf5fc5d796888d33b7cb0f33f984b7c" + url: "https://pub.dev" + source: hosted + version: "12.3.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" diacritic: dependency: transitive description: @@ -1703,6 +1719,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.15.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1c6005c..9c60130 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -101,6 +101,7 @@ dependencies: git: url: https://github.com/Moseco/flutter_exif_rotation ref: '55b72de' + device_info_plus: ^12.3.0 dependency_overrides: flutter_statusbarcolor_ns: From 00ab5a855c1c118ccb3544264865b500e92898a2 Mon Sep 17 00:00:00 2001 From: Moseco Date: Fri, 30 Jan 2026 21:28:34 +0900 Subject: [PATCH 14/20] refactor: change icons in search view and dismiss behavior --- lib/ui/views/search/search_view.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/ui/views/search/search_view.dart b/lib/ui/views/search/search_view.dart index e08a74a..0ded796 100644 --- a/lib/ui/views/search/search_view.dart +++ b/lib/ui/views/search/search_view.dart @@ -121,7 +121,7 @@ class _SearchTextField extends ViewModelWidget { (node) { return IconButton( onPressed: node.unfocus, - icon: const Icon(Icons.close), + icon: const Icon(Icons.keyboard_hide), ); }, ], @@ -143,7 +143,7 @@ class _SearchTextField extends ViewModelWidget { maxLines: 1, textInputAction: viewModel.inputMode != InputMode.text ? null - : TextInputAction.done, + : TextInputAction.search, focusNode: viewModel.inputMode != InputMode.text ? handWritingFocusNode : keyboardFocusNode, @@ -189,7 +189,9 @@ class _SearchTextField extends ViewModelWidget { onPressed: () { viewModel.searchOnChange(''); searchController.clear(); - handWritingFocusNode.requestFocus(); + viewModel.inputMode != InputMode.text + ? handWritingFocusNode.requestFocus() + : keyboardFocusNode.requestFocus(); }, icon: Icon( Icons.clear, From b1eb6d526763efc67aa16b2f0d3de0ff6e431725 Mon Sep 17 00:00:00 2001 From: Hampus Hammarlund Date: Sat, 31 Jan 2026 13:26:41 +0900 Subject: [PATCH 15/20] fix: move back navigation handling to individual home screen views --- lib/ui/views/home/home_view.dart | 105 +++--- lib/ui/views/learning/learning_view.dart | 76 ++-- lib/ui/views/learning/learning_viewmodel.dart | 5 + lib/ui/views/lists/lists_view.dart | 28 +- lib/ui/views/lists/lists_viewmodel.dart | 7 + lib/ui/views/settings/settings_view.dart | 350 +++++++++--------- lib/ui/views/settings/settings_viewmodel.dart | 5 + 7 files changed, 301 insertions(+), 275 deletions(-) diff --git a/lib/ui/views/home/home_view.dart b/lib/ui/views/home/home_view.dart index 23528b9..51d46bb 100644 --- a/lib/ui/views/home/home_view.dart +++ b/lib/ui/views/home/home_view.dart @@ -16,64 +16,57 @@ class HomeView extends StatelessWidget { Widget build(BuildContext context) { return ViewModelBuilder.reactive( viewModelBuilder: () => locator(), - builder: (context, viewModel, child) => PopScope( - canPop: viewModel.currentIndex == 0, - onPopInvokedWithResult: (bool didPop, Object? result) async { - if (!didPop) viewModel.handleBackButton(); - }, - child: Scaffold( - body: ExtendedNavigator( - navigatorKey: - StackedService.nestedNavigationKey(nestedNavigationKey), - initialRoute: viewModel.startOnLearningView - ? HomeViewRoutes.learningView - : HomeViewRoutes.searchView, - router: HomeViewRouter(), - observers: [ - FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance), - ], - ), - bottomNavigationBar: viewModel.showNavigationBar - ? SafeArea( - child: Padding( - padding: const EdgeInsets.all(8), - child: GNav( - haptic: false, - gap: 8, - color: Theme.of(context).iconTheme.color, - activeColor: Colors.white, - iconSize: 24, - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 12, - ), - tabBorderRadius: 15, - tabBackgroundColor: Colors.deepPurple, - selectedIndex: viewModel.currentIndex, - onTabChange: viewModel.handleNavigation, - tabs: const [ - GButton( - icon: Icons.search, - text: 'Search', - ), - GButton( - icon: Icons.format_list_bulleted, - text: 'Lists', - ), - GButton( - icon: Icons.school, - text: 'Learning', - ), - GButton( - icon: Icons.settings, - text: 'Settings', - ), - ], + builder: (context, viewModel, child) => Scaffold( + body: ExtendedNavigator( + navigatorKey: StackedService.nestedNavigationKey(nestedNavigationKey), + initialRoute: viewModel.startOnLearningView + ? HomeViewRoutes.learningView + : HomeViewRoutes.searchView, + router: HomeViewRouter(), + observers: [ + FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance), + ], + ), + bottomNavigationBar: viewModel.showNavigationBar + ? SafeArea( + child: Padding( + padding: const EdgeInsets.all(8), + child: GNav( + haptic: false, + gap: 8, + color: Theme.of(context).iconTheme.color, + activeColor: Colors.white, + iconSize: 24, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, ), + tabBorderRadius: 15, + tabBackgroundColor: Colors.deepPurple, + selectedIndex: viewModel.currentIndex, + onTabChange: viewModel.handleNavigation, + tabs: const [ + GButton( + icon: Icons.search, + text: 'Search', + ), + GButton( + icon: Icons.format_list_bulleted, + text: 'Lists', + ), + GButton( + icon: Icons.school, + text: 'Learning', + ), + GButton( + icon: Icons.settings, + text: 'Settings', + ), + ], ), - ) - : null, - ), + ), + ) + : null, ), ); } diff --git a/lib/ui/views/learning/learning_view.dart b/lib/ui/views/learning/learning_view.dart index 20cd51b..615c4fc 100644 --- a/lib/ui/views/learning/learning_view.dart +++ b/lib/ui/views/learning/learning_view.dart @@ -15,47 +15,51 @@ class LearningView extends StackedView { @override Widget builder(context, viewModel, child) { return Scaffold( - body: HomeHeader( - title: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: BackButton(onPressed: () {}, color: Colors.transparent), - ), - const Expanded( - child: Text( - 'Learning', - softWrap: false, - textAlign: TextAlign.center, - style: TextStyle( + body: PopScope( + canPop: false, + onPopInvokedWithResult: (_, __) => viewModel.handleBackButton(), + child: HomeHeader( + title: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: BackButton(onPressed: () {}, color: Colors.transparent), + ), + const Expanded( + child: Text( + 'Learning', + softWrap: false, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 32, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: IconButton( + onPressed: viewModel.createFlashcardSet, color: Colors.white, - fontSize: 32, + icon: const Icon(Icons.add), ), ), - ), - Padding( - padding: const EdgeInsets.all(8), - child: IconButton( - onPressed: viewModel.createFlashcardSet, - color: Colors.white, - icon: const Icon(Icons.add), + ], + ), + child: switch (viewModel.flashcardSets?.length) { + null => const _Loading(), + 0 => const _NoFlashcards(), + _ => ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: viewModel.flashcardSets!.length, + itemBuilder: (context, index) => _FlashcardSet( + viewModel.flashcardSets![index], + ), ), - ), - ], + }, ), - child: switch (viewModel.flashcardSets?.length) { - null => const _Loading(), - 0 => const _NoFlashcards(), - _ => ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: viewModel.flashcardSets!.length, - itemBuilder: (context, index) => _FlashcardSet( - viewModel.flashcardSets![index], - ), - ), - }, ), ); } diff --git a/lib/ui/views/learning/learning_viewmodel.dart b/lib/ui/views/learning/learning_viewmodel.dart index 0078e12..a4b76ed 100644 --- a/lib/ui/views/learning/learning_viewmodel.dart +++ b/lib/ui/views/learning/learning_viewmodel.dart @@ -2,6 +2,7 @@ import 'package:sagase/app/app.dialogs.dart'; import 'package:sagase/app/app.locator.dart'; import 'package:sagase/app/app.router.dart'; import 'package:sagase/services/dictionary_service.dart'; +import 'package:sagase/ui/views/home/home_viewmodel.dart'; import 'package:sagase_dictionary/sagase_dictionary.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; @@ -114,4 +115,8 @@ class LearningViewModel extends FutureViewModel { arguments: FlashcardSetInfoViewArguments(flashcardSet: flashcardSet), ); } + + void handleBackButton() { + locator().handleBackButton(); + } } diff --git a/lib/ui/views/lists/lists_view.dart b/lib/ui/views/lists/lists_view.dart index c1174ca..e66b9fa 100644 --- a/lib/ui/views/lists/lists_view.dart +++ b/lib/ui/views/lists/lists_view.dart @@ -16,17 +16,23 @@ class ListsView extends StackedView { @override Widget builder(context, viewModel, child) { return Scaffold( - body: switch (viewModel.listSelection) { - ListSelection.main => _MainList(), - ListSelection.vocab => _VocabList(), - ListSelection.kanji => _KanjiList(), - ListSelection.myLists => _MyLists(), - ListSelection.coreVocab => _CoreVocabList(), - ListSelection.jlptVocab => _JlptVocabList(), - ListSelection.jlptKanji => _JlptKanjiList(), - ListSelection.schoolKanji => _SchoolKanjiList(), - ListSelection.kanjiKentei => _KanjiKenteiList(), - }, + body: PopScope( + canPop: viewModel.listSelection != ListSelection.main, + onPopInvokedWithResult: (bool didPop, _) { + if (!didPop) viewModel.handleBackButton(); + }, + child: switch (viewModel.listSelection) { + ListSelection.main => _MainList(), + ListSelection.vocab => _VocabList(), + ListSelection.kanji => _KanjiList(), + ListSelection.myLists => _MyLists(), + ListSelection.coreVocab => _CoreVocabList(), + ListSelection.jlptVocab => _JlptVocabList(), + ListSelection.jlptKanji => _JlptKanjiList(), + ListSelection.schoolKanji => _SchoolKanjiList(), + ListSelection.kanjiKentei => _KanjiKenteiList(), + }, + ), ); } } diff --git a/lib/ui/views/lists/lists_viewmodel.dart b/lib/ui/views/lists/lists_viewmodel.dart index e706201..4e4d322 100644 --- a/lib/ui/views/lists/lists_viewmodel.dart +++ b/lib/ui/views/lists/lists_viewmodel.dart @@ -5,6 +5,7 @@ import 'package:flutter_file_dialog/flutter_file_dialog.dart'; import 'package:sagase/app/app.dialogs.dart'; import 'package:sagase/app/app.locator.dart'; import 'package:sagase/app/app.router.dart'; +import 'package:sagase/ui/views/home/home_viewmodel.dart'; import 'package:sagase/ui/views/lists/lists_view.dart'; import 'package:sagase/utils/constants.dart'; import 'package:sagase_dictionary/sagase_dictionary.dart'; @@ -159,6 +160,12 @@ class ListsViewModel extends FutureViewModel { ); } + void handleBackButton() { + if (listSelection == ListSelection.main) { + locator().handleBackButton(); + } + } + @override void dispose() { _myListsWatcher?.cancel(); diff --git a/lib/ui/views/settings/settings_view.dart b/lib/ui/views/settings/settings_view.dart index 2f09a9f..524e532 100644 --- a/lib/ui/views/settings/settings_view.dart +++ b/lib/ui/views/settings/settings_view.dart @@ -14,194 +14,200 @@ class SettingsView extends StatelessWidget { return ViewModelBuilder.reactive( viewModelBuilder: () => SettingsViewModel(), builder: (context, viewModel, child) => Scaffold( - body: HomeHeader( - title: const Text( - 'Settings', - style: TextStyle( - color: Colors.white, - fontSize: 32, - ), - ), - child: SettingsList( - lightTheme: const SettingsThemeData( - settingsListBackground: Colors.transparent, - ), - darkTheme: const SettingsThemeData( - settingsListBackground: Colors.transparent, + body: PopScope( + canPop: false, + onPopInvokedWithResult: (_, __) => viewModel.handleBackButton(), + child: HomeHeader( + title: const Text( + 'Settings', + style: TextStyle( + color: Colors.white, + fontSize: 32, + ), ), - sections: [ - if (kDebugMode) + child: SettingsList( + lightTheme: const SettingsThemeData( + settingsListBackground: Colors.transparent, + ), + darkTheme: const SettingsThemeData( + settingsListBackground: Colors.transparent, + ), + sections: [ + if (kDebugMode) + SettingsSection( + title: const Text('Debug'), + tiles: [ + SettingsTile.navigation( + leading: const Icon(Icons.bug_report), + title: const Text('Open dev screen'), + onPressed: (_) => viewModel.navigateToDev(), + ), + ], + ), SettingsSection( - title: const Text('Debug'), + title: const Text('General'), tiles: [ SettingsTile.navigation( - leading: const Icon(Icons.bug_report), - title: const Text('Open dev screen'), - onPressed: (_) => viewModel.navigateToDev(), + title: const Text('Set Japanese font'), + onPressed: (_) => viewModel.setJapaneseFont(), + ), + SettingsTile.navigation( + title: const Text('Set app theme'), + onPressed: (_) => viewModel.setAppTheme(), + ), + SettingsTile.switchTile( + initialValue: viewModel.showPitchAccent, + onToggle: viewModel.setShowPitchAccent, + activeSwitchColor: Theme.of(context).colorScheme.primary, + title: const Text('Show pitch accent'), + ), + SettingsTile.switchTile( + initialValue: viewModel.startOnLearningView, + onToggle: viewModel.setStartOnLearningView, + activeSwitchColor: Theme.of(context).colorScheme.primary, + title: const Text('Start on learning screen'), + ), + SettingsTile.switchTile( + initialValue: viewModel.properNounsEnabled, + onToggle: viewModel.setProperNounsEnabled, + activeSwitchColor: Theme.of(context).colorScheme.primary, + title: const Text('Include proper nouns'), + description: const Text('Increases app size by ~100mb'), ), ], ), - SettingsSection( - title: const Text('General'), - tiles: [ - SettingsTile.navigation( - title: const Text('Set Japanese font'), - onPressed: (_) => viewModel.setJapaneseFont(), - ), - SettingsTile.navigation( - title: const Text('Set app theme'), - onPressed: (_) => viewModel.setAppTheme(), - ), - SettingsTile.switchTile( - initialValue: viewModel.showPitchAccent, - onToggle: viewModel.setShowPitchAccent, - activeSwitchColor: Theme.of(context).colorScheme.primary, - title: const Text('Show pitch accent'), - ), - SettingsTile.switchTile( - initialValue: viewModel.startOnLearningView, - onToggle: viewModel.setStartOnLearningView, - activeSwitchColor: Theme.of(context).colorScheme.primary, - title: const Text('Start on learning screen'), - ), - SettingsTile.switchTile( - initialValue: viewModel.properNounsEnabled, - onToggle: viewModel.setProperNounsEnabled, - activeSwitchColor: Theme.of(context).colorScheme.primary, - title: const Text('Include proper nouns'), - description: const Text('Increases app size by ~100mb'), - ), - ], - ), - SettingsSection( - title: const Text('Flashcards'), - tiles: [ - SettingsTile.switchTile( - initialValue: viewModel.flashcardLearningModeEnabled, - onToggle: viewModel.setFlashcardLearningModeEnabled, - activeSwitchColor: Theme.of(context).colorScheme.primary, - title: const Text('Learning mode'), - description: const Text( - 'If enabled, a set amount of new flashcards will be included with due flashcards. You can also long press a flashcard set to open in a different mode.', + SettingsSection( + title: const Text('Flashcards'), + tiles: [ + SettingsTile.switchTile( + initialValue: viewModel.flashcardLearningModeEnabled, + onToggle: viewModel.setFlashcardLearningModeEnabled, + activeSwitchColor: Theme.of(context).colorScheme.primary, + title: const Text('Learning mode'), + description: const Text( + 'If enabled, a set amount of new flashcards will be included with due flashcards. You can also long press a flashcard set to open in a different mode.', + ), ), - ), - SettingsTile.navigation( - enabled: viewModel.flashcardLearningModeEnabled, - title: const Text('Set new flashcards per day'), - description: const Text( - 'The amount of new flashcards to be added along with due cards while in learning mode.', + SettingsTile.navigation( + enabled: viewModel.flashcardLearningModeEnabled, + title: const Text('Set new flashcards per day'), + description: const Text( + 'The amount of new flashcards to be added along with due cards while in learning mode.', + ), + onPressed: (_) => viewModel.setNewFlashcardsPerDay(), ), - onPressed: (_) => viewModel.setNewFlashcardsPerDay(), - ), - SettingsTile.navigation( - title: const Text('Set initial spaced repetition interval'), - onPressed: (_) => - viewModel.setInitialSpacedRepetitionInterval(), - ), - SettingsTile.navigation( - title: const Text('Set flashcard distance'), - description: const Text( - 'How far into the stack a flashcard is put after a wrong answer, repeat answer, or while completing a new flashcard.', + SettingsTile.navigation( + title: + const Text('Set initial spaced repetition interval'), + onPressed: (_) => + viewModel.setInitialSpacedRepetitionInterval(), ), - onPressed: (_) => viewModel.setFlashcardDistance(), - ), - SettingsTile.navigation( - title: const Text( - 'Set correct answers required to complete a new flashcard'), - onPressed: (_) => - viewModel.setFlashcardCorrectAnswersRequired(), - ), - SettingsTile.switchTile( - initialValue: viewModel.showNewInterval, - onToggle: viewModel.setShowNewInterval, - activeSwitchColor: Theme.of(context).colorScheme.primary, - title: const Text('Preview new spaced repetition interval'), - description: const Text( - 'Shown underneath flashcard answer buttons.', + SettingsTile.navigation( + title: const Text('Set flashcard distance'), + description: const Text( + 'How far into the stack a flashcard is put after a wrong answer, repeat answer, or while completing a new flashcard.', + ), + onPressed: (_) => viewModel.setFlashcardDistance(), ), - ), - SettingsTile.switchTile( - initialValue: viewModel.showDetailedProgress, - onToggle: viewModel.setShowDetailedProgress, - activeSwitchColor: Theme.of(context).colorScheme.primary, - title: const Text('Show detailed progress'), - description: const Text( - 'If enabled and in learning mode, the progress bar will display due flashcards and new flashcards as separate numbers instead of as one number.', + SettingsTile.navigation( + title: const Text( + 'Set correct answers required to complete a new flashcard'), + onPressed: (_) => + viewModel.setFlashcardCorrectAnswersRequired(), ), - ), - ], - ), - SettingsSection( - title: const Text('App data'), - tiles: [ - SettingsTile.switchTile( - initialValue: viewModel.analyticsEnabled, - onToggle: viewModel.setAnalyticsEnabled, - activeSwitchColor: Theme.of(context).colorScheme.primary, - title: const Text('Analytics collection'), - description: const Text( - 'If enabled, the app collects basic usage analytics. No personally identifying information is collected.', + SettingsTile.switchTile( + initialValue: viewModel.showNewInterval, + onToggle: viewModel.setShowNewInterval, + activeSwitchColor: Theme.of(context).colorScheme.primary, + title: + const Text('Preview new spaced repetition interval'), + description: const Text( + 'Shown underneath flashcard answer buttons.', + ), ), - ), - SettingsTile.switchTile( - initialValue: viewModel.crashlyticsEnabled, - onToggle: viewModel.setCrashlyticsEnabled, - activeSwitchColor: Theme.of(context).colorScheme.primary, - title: const Text('Crash report collection'), - description: const Text( - 'If enabled, the app collects crash report information to help with development. No personally identifying information is collected.', + SettingsTile.switchTile( + initialValue: viewModel.showDetailedProgress, + onToggle: viewModel.setShowDetailedProgress, + activeSwitchColor: Theme.of(context).colorScheme.primary, + title: const Text('Show detailed progress'), + description: const Text( + 'If enabled and in learning mode, the progress bar will display due flashcards and new flashcards as separate numbers instead of as one number.', + ), ), - ), - SettingsTile.navigation( - title: const Text('Delete analytics data'), - onPressed: (_) => viewModel.requestDataDeletion(), - ), - SettingsTile.navigation( - title: const Text('Delete search history'), - onPressed: (_) => viewModel.deleteSearchHistory(), - ), - SettingsTile.navigation( - title: const Text('Backup data'), - description: const Text( - 'Exports user created lists, flashcard sets, and spaced repetition data. The created file can then be saved in a safe place.', + ], + ), + SettingsSection( + title: const Text('App data'), + tiles: [ + SettingsTile.switchTile( + initialValue: viewModel.analyticsEnabled, + onToggle: viewModel.setAnalyticsEnabled, + activeSwitchColor: Theme.of(context).colorScheme.primary, + title: const Text('Analytics collection'), + description: const Text( + 'If enabled, the app collects basic usage analytics. No personally identifying information is collected.', + ), ), - onPressed: (_) => viewModel.backupData(), - ), - SettingsTile.navigation( - title: const Text('Restore from backup'), - description: const Text( - 'This will delete all user data and then import new user data from the selected backup file.', + SettingsTile.switchTile( + initialValue: viewModel.crashlyticsEnabled, + onToggle: viewModel.setCrashlyticsEnabled, + activeSwitchColor: Theme.of(context).colorScheme.primary, + title: const Text('Crash report collection'), + description: const Text( + 'If enabled, the app collects crash report information to help with development. No personally identifying information is collected.', + ), ), - onPressed: (_) => viewModel.restoreFromBackup(), - ), - ], - ), - SettingsSection( - title: const Text('About'), - tiles: [ - SettingsTile.navigation( - leading: const Icon(Icons.link), - title: const Text('Submit feedback'), - onPressed: (_) => viewModel.openFeedback(), - ), - SettingsTile.navigation( - leading: const Icon(Icons.history), - title: const Text('Open changelog'), - onPressed: (_) => viewModel.openChangelog(), - ), - SettingsTile.navigation( - leading: const Icon(Icons.policy), - title: const Text('Privacy policy'), - onPressed: (_) => viewModel.openPrivacyPolicy(), - ), - SettingsTile.navigation( - leading: const Icon(Icons.info), - title: const Text('About Sagase'), - onPressed: (_) => viewModel.navigateToAbout(), - ), - ], - ), - ], + SettingsTile.navigation( + title: const Text('Delete analytics data'), + onPressed: (_) => viewModel.requestDataDeletion(), + ), + SettingsTile.navigation( + title: const Text('Delete search history'), + onPressed: (_) => viewModel.deleteSearchHistory(), + ), + SettingsTile.navigation( + title: const Text('Backup data'), + description: const Text( + 'Exports user created lists, flashcard sets, and spaced repetition data. The created file can then be saved in a safe place.', + ), + onPressed: (_) => viewModel.backupData(), + ), + SettingsTile.navigation( + title: const Text('Restore from backup'), + description: const Text( + 'This will delete all user data and then import new user data from the selected backup file.', + ), + onPressed: (_) => viewModel.restoreFromBackup(), + ), + ], + ), + SettingsSection( + title: const Text('About'), + tiles: [ + SettingsTile.navigation( + leading: const Icon(Icons.link), + title: const Text('Submit feedback'), + onPressed: (_) => viewModel.openFeedback(), + ), + SettingsTile.navigation( + leading: const Icon(Icons.history), + title: const Text('Open changelog'), + onPressed: (_) => viewModel.openChangelog(), + ), + SettingsTile.navigation( + leading: const Icon(Icons.policy), + title: const Text('Privacy policy'), + onPressed: (_) => viewModel.openPrivacyPolicy(), + ), + SettingsTile.navigation( + leading: const Icon(Icons.info), + title: const Text('About Sagase'), + onPressed: (_) => viewModel.navigateToAbout(), + ), + ], + ), + ], + ), ), ), ), diff --git a/lib/ui/views/settings/settings_viewmodel.dart b/lib/ui/views/settings/settings_viewmodel.dart index 30dca6a..8c45d6b 100644 --- a/lib/ui/views/settings/settings_viewmodel.dart +++ b/lib/ui/views/settings/settings_viewmodel.dart @@ -10,6 +10,7 @@ import 'package:sagase/services/firebase_service.dart'; import 'package:sagase/services/dictionary_service.dart'; import 'package:sagase/services/shared_preferences_service.dart'; import 'package:sagase/ui/themes.dart'; +import 'package:sagase/ui/views/home/home_viewmodel.dart'; import 'package:sagase/ui/views/search/search_viewmodel.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; @@ -47,6 +48,10 @@ class SettingsViewModel extends BaseViewModel { _navigationService.navigateTo(Routes.devView); } + void handleBackButton() { + locator().handleBackButton(); + } + void setInitialCorrectInterval(int value) { _sharedPreferencesService.setInitialCorrectInterval(value); notifyListeners(); From da333f4a093fc519dc42d80039d23e5cc58d0b8b Mon Sep 17 00:00:00 2001 From: Hampus Hammarlund Date: Sat, 31 Jan 2026 22:22:22 +0900 Subject: [PATCH 16/20] feat: add option to space out flashcards on demand --- lib/services/dictionary_service.dart | 20 +++++++++ .../flashcard_set_settings_view.dart | 7 ++- .../flashcard_set_settings_viewmodel.dart | 43 +++++++++++++++++++ .../flashcards/flashcards_viewmodel.dart | 16 +------ 4 files changed, 70 insertions(+), 16 deletions(-) diff --git a/lib/services/dictionary_service.dart b/lib/services/dictionary_service.dart index 98b6b44..60426a4 100644 --- a/lib/services/dictionary_service.dart +++ b/lib/services/dictionary_service.dart @@ -11,6 +11,7 @@ import 'package:archive/archive_io.dart' as archive; import 'package:sagase/datamodels/user_backup.dart'; import 'package:sagase/services/isar_service.dart'; import 'package:sagase/utils/constants.dart' as constants; +import 'package:sagase/utils/date_time_utils.dart'; import 'package:sagase_dictionary/sagase_dictionary.dart'; import 'package:path/path.dart' as path; @@ -465,6 +466,25 @@ class DictionaryService { ); } + Future spaceOutFlashcards(List flashcards) async { + await _database.transaction(() async { + final now = DateTime.now(); + + int flashcardsPerDay = (flashcards.length - 150) ~/ 12; + for (int i = 1; i < 14; i++) { + int dueDate = now.add(Duration(days: i)).toInt(); + for (int j = 0; j < flashcardsPerDay && flashcards.length > 150; j++) { + await setSpacedRepetitionData( + flashcards + .removeLast() + .spacedRepetitionData! + .copyWith(dueDate: dueDate), + ); + } + } + }); + } + Future setSpacedRepetitionData(SpacedRepetitionData data) async { return _database.spacedRepetitionDatasDao.set(data); } diff --git a/lib/ui/views/flashcard_set_settings/flashcard_set_settings_view.dart b/lib/ui/views/flashcard_set_settings/flashcard_set_settings_view.dart index 27611ba..d4c50a6 100644 --- a/lib/ui/views/flashcard_set_settings/flashcard_set_settings_view.dart +++ b/lib/ui/views/flashcard_set_settings/flashcard_set_settings_view.dart @@ -66,13 +66,18 @@ class FlashcardSetSettingsView if (flashcardSet.usingSpacedRepetition) const PopupMenuItem( value: PopupMenuItemType.reset, - child: Text('Reset'), + child: Text('Reset progress'), ), if (flashcardSet.usingSpacedRepetition) const PopupMenuItem( value: PopupMenuItemType.statistics, child: Text('View statistics'), ), + if (flashcardSet.usingSpacedRepetition) + const PopupMenuItem( + value: PopupMenuItemType.spaceOut, + child: Text('Space out flashcards'), + ), ], onSelected: viewModel.handlePopupMenuButton, ), diff --git a/lib/ui/views/flashcard_set_settings/flashcard_set_settings_viewmodel.dart b/lib/ui/views/flashcard_set_settings/flashcard_set_settings_viewmodel.dart index 6e132d3..1af08e0 100644 --- a/lib/ui/views/flashcard_set_settings/flashcard_set_settings_viewmodel.dart +++ b/lib/ui/views/flashcard_set_settings/flashcard_set_settings_viewmodel.dart @@ -4,6 +4,7 @@ import 'package:sagase/app/app.locator.dart'; import 'package:sagase/app/app.router.dart'; import 'package:sagase/datamodels/lists_bottom_sheet_argument.dart'; import 'package:sagase/services/shared_preferences_service.dart'; +import 'package:sagase/utils/date_time_utils.dart'; import 'package:sagase_dictionary/sagase_dictionary.dart'; import 'package:sagase/datamodels/my_lists_bottom_sheet_item.dart'; import 'package:sagase/services/dictionary_service.dart'; @@ -50,6 +51,9 @@ class FlashcardSetSettingsViewModel extends FutureViewModel { case PopupMenuItemType.statistics: _openFlashcardSetInfo(); break; + case PopupMenuItemType.spaceOut: + _spaceOutDueFlashcards(); + break; } } @@ -245,6 +249,44 @@ class FlashcardSetSettingsViewModel extends FutureViewModel { bool shouldShowTutorial() { return _sharedPreferencesService.getAndSetTutorialFlashcardSetSettings(); } + + Future _spaceOutDueFlashcards() async { + final response = await _dialogService.showCustomDialog( + variant: DialogType.confirmation, + title: 'Reduce due flashcards?', + description: + 'Have too many due flashcards? To help you study due flashcards can be delayed. Pressing confirm will leave 150 due flashcards for today and spread the rest out over the next 2 weeks.', + mainButtonTitle: 'Confirm', + secondaryButtonTitle: 'Cancel', + barrierDismissible: true, + ); + + if (response != null && response.confirmed) { + _dialogService.showCustomDialog( + variant: DialogType.progressIndicator, + title: 'Updating flashcards', + barrierDismissible: false, + ); + + final allFlashcards = + await _dictionaryService.getFlashcardSetFlashcards(flashcardSet); + final dueFlashcards = []; + + final sessionDateTime = DateTime.now(); + int todayAsInt = sessionDateTime.toInt(); + for (var item in allFlashcards) { + if (item.spacedRepetitionData != null && + item.spacedRepetitionData!.dueDate! <= todayAsInt) { + dueFlashcards.add(item); + } + } + + dueFlashcards.shuffle(); + await _dictionaryService.spaceOutFlashcards(dueFlashcards); + + _dialogService.completeDialog(DialogResponse()); + } + } } enum PopupMenuItemType { @@ -252,4 +294,5 @@ enum PopupMenuItemType { delete, reset, statistics, + spaceOut, } diff --git a/lib/ui/views/flashcards/flashcards_viewmodel.dart b/lib/ui/views/flashcards/flashcards_viewmodel.dart index 6bbbf93..8feb12b 100644 --- a/lib/ui/views/flashcards/flashcards_viewmodel.dart +++ b/lib/ui/views/flashcards/flashcards_viewmodel.dart @@ -716,7 +716,6 @@ class FlashcardsViewModel extends FutureViewModel { ); if (response != null && response.confirmed) { - // Show progress indicator dialog _dialogService.showCustomDialog( variant: DialogType.progressIndicator, title: 'Updating flashcards', @@ -724,20 +723,7 @@ class FlashcardsViewModel extends FutureViewModel { ); dueFlashcards.shuffle(_random); - int flashcardsPerDay = (dueFlashcards.length - 150) ~/ 12; - for (int i = 1; i < 14; i++) { - int dueDate = sessionDateTime.add(Duration(days: i)).toInt(); - for (int j = 0; - j < flashcardsPerDay && dueFlashcards.length > 150; - j++) { - await _dictionaryService.setSpacedRepetitionData( - dueFlashcards - .removeLast() - .spacedRepetitionData! - .copyWith(dueDate: dueDate), - ); - } - } + await _dictionaryService.spaceOutFlashcards(dueFlashcards); _dialogService.completeDialog(DialogResponse()); } From a9cab7289bdcbb3641cba9161c6c78a57cefb490 Mon Sep 17 00:00:00 2001 From: Hampus Hammarlund Date: Sun, 1 Feb 2026 00:04:57 +0900 Subject: [PATCH 17/20] feat: add option to add new flashcards in batches --- lib/services/shared_preferences_service.dart | 10 ++++++ .../flashcards/flashcards_viewmodel.dart | 23 ++++++++++--- lib/ui/views/settings/settings_view.dart | 10 ++++++ lib/ui/views/settings/settings_viewmodel.dart | 7 ++++ lib/utils/constants.dart | 2 ++ test/helpers/mocks.dart | 4 +++ .../flashcards/flashcards_viewmodel_test.dart | 32 +++++++++++++++++++ 7 files changed, 84 insertions(+), 4 deletions(-) diff --git a/lib/services/shared_preferences_service.dart b/lib/services/shared_preferences_service.dart index 60cc388..b443927 100644 --- a/lib/services/shared_preferences_service.dart +++ b/lib/services/shared_preferences_service.dart @@ -237,4 +237,14 @@ class SharedPreferencesService implements InitializableDependency { Future setProperNounsEnabled(bool value) async { await _sharedPreferences.setBool(constants.keyProperNounsEnabled, value); } + + bool getAddNewFlashcardsInBatches() { + return _sharedPreferences.getBool(constants.keyAddNewFlashcardsInBatches) ?? + constants.defaultAddNewFlashcardsInBatches; + } + + Future setAddNewFlashcardsInBatches(bool value) async { + await _sharedPreferences.setBool( + constants.keyAddNewFlashcardsInBatches, value); + } } diff --git a/lib/ui/views/flashcards/flashcards_viewmodel.dart b/lib/ui/views/flashcards/flashcards_viewmodel.dart index 8feb12b..8983994 100644 --- a/lib/ui/views/flashcards/flashcards_viewmodel.dart +++ b/lib/ui/views/flashcards/flashcards_viewmodel.dart @@ -344,11 +344,26 @@ class FlashcardsViewModel extends FutureViewModel { // If active flashcards is still empty then try to add new flashcards Future?>? reportDialogResponse; if (activeFlashcards.isEmpty) { - activeFlashcards.addAll(newFlashcards); - newFlashcards.clear(); + if (_sharedPreferencesService.getFlashcardLearningModeEnabled() && + _sharedPreferencesService.getAddNewFlashcardsInBatches()) { + if (initial) newFlashcards.shuffle(_random); + + int newFlashcardsToAdd = max( + 0, + min( + _sharedPreferencesService.getNewFlashcardsPerDay() - + startedFlashcards.length, + newFlashcards.length, + )); + activeFlashcards.addAll(newFlashcards.take(newFlashcardsToAdd)); + newFlashcards.removeRange(0, newFlashcardsToAdd); + } else { + activeFlashcards.addAll(newFlashcards); + newFlashcards.clear(); + activeFlashcards.shuffle(_random); + } _answeringDueFlashcards = false; - // Randomize - activeFlashcards.shuffle(_random); + // Add any started flashcards activeFlashcards.insertAll(0, startedFlashcards..shuffle(_random)); startedFlashcards.clear(); diff --git a/lib/ui/views/settings/settings_view.dart b/lib/ui/views/settings/settings_view.dart index 524e532..54038bb 100644 --- a/lib/ui/views/settings/settings_view.dart +++ b/lib/ui/views/settings/settings_view.dart @@ -96,6 +96,16 @@ class SettingsView extends StatelessWidget { ), onPressed: (_) => viewModel.setNewFlashcardsPerDay(), ), + SettingsTile.switchTile( + enabled: viewModel.flashcardLearningModeEnabled, + initialValue: viewModel.addNewFlashcardsInBatches, + onToggle: viewModel.setAddNewFlashcardsInBatches, + activeSwitchColor: Theme.of(context).colorScheme.primary, + title: const Text('Add new flashcards in batches'), + description: const Text( + 'If enabled, when no due flashcards are available new flashcards are added in batches instead of all at once.', + ), + ), SettingsTile.navigation( title: const Text('Set initial spaced repetition interval'), diff --git a/lib/ui/views/settings/settings_viewmodel.dart b/lib/ui/views/settings/settings_viewmodel.dart index 8c45d6b..39f6cf1 100644 --- a/lib/ui/views/settings/settings_viewmodel.dart +++ b/lib/ui/views/settings/settings_viewmodel.dart @@ -31,6 +31,8 @@ class SettingsViewModel extends BaseViewModel { _sharedPreferencesService.getFlashcardLearningModeEnabled(); int get newFlashcardsPerDay => _sharedPreferencesService.getNewFlashcardsPerDay(); + bool get addNewFlashcardsInBatches => + _sharedPreferencesService.getAddNewFlashcardsInBatches(); int get flashcardDistance => _sharedPreferencesService.getFlashcardDistance(); int get flashcardCorrectAnswersRequired => _sharedPreferencesService.getFlashcardCorrectAnswersRequired(); @@ -107,6 +109,11 @@ class SettingsViewModel extends BaseViewModel { } catch (_) {} } + void setAddNewFlashcardsInBatches(bool value) { + _sharedPreferencesService.setAddNewFlashcardsInBatches(value); + notifyListeners(); + } + Future setFlashcardDistance() async { final response = await _dialogService.showCustomDialog( variant: DialogType.numberTextField, diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index cee02d3..7c8ab4b 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -25,6 +25,7 @@ const keyTutorialVocab = 'tutorial_vocab'; const keyShowDetailedProgress = 'show_detailed_progress'; const keyChangelogVersionShown = 'changelog_version_shown'; const keyProperNounsEnabled = 'proper_nouns_enabled'; +const keyAddNewFlashcardsInBatches = 'add_new_flashcards_in_batches'; const defaultInitialCorrectInterval = 1; const defaultInitialVeryCorrectInterval = 4; @@ -40,6 +41,7 @@ const defaultStartOnLearningView = false; const defaultStrokeDiagramStartExpanded = true; const defaultShowDetailedProgress = false; const defaultProperNounsEnabled = false; +const defaultAddNewFlashcardsInBatches = false; const searchQueryLimit = 1000; diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 54c41b8..c4fe416 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -164,6 +164,8 @@ MockSharedPreferencesService getAndRegisterSharedPreferencesService({ bool getShowDetailedProgress = constants.defaultShowDetailedProgress, bool getOnboardingFinished = false, bool getProperNounsEnabled = constants.defaultProperNounsEnabled, + bool getAddNewFlashcardsInBatches = + constants.defaultAddNewFlashcardsInBatches, }) { _removeRegistrationIfExists(); final service = MockSharedPreferencesService(); @@ -188,6 +190,8 @@ MockSharedPreferencesService getAndRegisterSharedPreferencesService({ when(service.getShowDetailedProgress()).thenReturn(getShowDetailedProgress); when(service.getOnboardingFinished()).thenReturn(getOnboardingFinished); when(service.getProperNounsEnabled()).thenReturn(getProperNounsEnabled); + when(service.getAddNewFlashcardsInBatches()) + .thenReturn(getAddNewFlashcardsInBatches); locator.registerSingleton(service); return service; diff --git a/test/ui/views/flashcards/flashcards_viewmodel_test.dart b/test/ui/views/flashcards/flashcards_viewmodel_test.dart index 4d9d23e..5ba7ac7 100644 --- a/test/ui/views/flashcards/flashcards_viewmodel_test.dart +++ b/test/ui/views/flashcards/flashcards_viewmodel_test.dart @@ -1981,5 +1981,37 @@ void main() { await dictionaryService.getRecentFlashcardSetReport(flashcardSet); expect(report, null); }); + + test( + 'Add new flashcards in batches when enabled and have no due flashcards', + () async { + // Set shared preferences + getAndRegisterSharedPreferencesService( + getFlashcardLearningModeEnabled: true, + getNewFlashcardsPerDay: 1, + getAddNewFlashcardsInBatches: true, + ); + + // Create dictionary lists to use + final dictionaryList = + await dictionaryService.createMyDictionaryList('list1'); + await dictionaryService.addToMyDictionaryList( + dictionaryList, getVocab1()); + await dictionaryService.addToMyDictionaryList( + dictionaryList, getVocab2()); + + // Create flashcard set and assign lists + final flashcardSet = await dictionaryService.createFlashcardSet('name'); + flashcardSet.myDictionaryLists.add(dictionaryList.id); + await dictionaryService.updateFlashcardSet(flashcardSet); + + // Call initialize + var viewModel = FlashcardsViewModel(flashcardSet, null, randomSeed: 123); + await viewModel.futureToRun(); + + // Check results + expect(viewModel.activeFlashcards.length, 1); + expect(viewModel.newFlashcards.length, 1); + }); }); } From a1ac79821f5cbea1968f07b64698549c4264dc46 Mon Sep 17 00:00:00 2001 From: Hampus Hammarlund Date: Wed, 4 Feb 2026 22:12:54 +0900 Subject: [PATCH 18/20] feat: add option to add vocab identified with text analysis to a dictionary list --- lib/app/app.dart | 2 + lib/services/dictionary_service.dart | 8 +++ .../add_to_my_list_bottom_sheet.dart | 54 +++++++++++++++++++ .../text_analysis/text_analysis_view.dart | 6 ++- .../text_analysis_viewmodel.dart | 28 +++++++++- pubspec.lock | 4 +- pubspec.yaml | 2 +- 7 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 lib/ui/bottom_sheets/add_to_my_list_bottom_sheet.dart diff --git a/lib/app/app.dart b/lib/app/app.dart index 1e49978..354db4b 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -4,6 +4,7 @@ import 'package:sagase/services/firebase_service.dart'; import 'package:sagase/services/dictionary_service.dart'; import 'package:sagase/services/mecab_service.dart'; import 'package:sagase/services/shared_preferences_service.dart'; +import 'package:sagase/ui/bottom_sheets/add_to_my_list_bottom_sheet.dart'; import 'package:sagase/ui/bottom_sheets/assign_lists_bottom_sheet.dart'; import 'package:sagase/ui/bottom_sheets/assign_my_lists_bottom_sheet.dart'; import 'package:sagase/ui/bottom_sheets/select_dictionary_item_bottom_sheet.dart'; @@ -107,6 +108,7 @@ import 'package:stacked_themes/stacked_themes.dart'; LazySingleton(classType: SearchViewModel), ], bottomsheets: [ + StackedBottomsheet(classType: AddToMyListBottomSheet), StackedBottomsheet(classType: AssignMyListsBottomSheet), StackedBottomsheet(classType: AssignListsBottomSheet), StackedBottomsheet(classType: StrokeOrderBottomSheet), diff --git a/lib/services/dictionary_service.dart b/lib/services/dictionary_service.dart index 60426a4..d511de8 100644 --- a/lib/services/dictionary_service.dart +++ b/lib/services/dictionary_service.dart @@ -328,6 +328,14 @@ class DictionaryService { .addDictionaryItem(dictionaryList, dictionaryItem); } + Future addManyToMyDictionaryList( + MyDictionaryList dictionaryList, + List dictionaryItems, + ) async { + await _database.myDictionaryListsDao + .addDictionaryItems(dictionaryList, dictionaryItems); + } + Future removeFromMyDictionaryList( MyDictionaryList dictionaryList, DictionaryItem dictionaryItem, diff --git a/lib/ui/bottom_sheets/add_to_my_list_bottom_sheet.dart b/lib/ui/bottom_sheets/add_to_my_list_bottom_sheet.dart new file mode 100644 index 0000000..b72cae4 --- /dev/null +++ b/lib/ui/bottom_sheets/add_to_my_list_bottom_sheet.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:sagase/ui/bottom_sheets/base_bottom_sheet.dart'; +import 'package:stacked_services/stacked_services.dart'; + +class AddToMyListBottomSheet extends StatelessWidget { + final SheetRequest request; + final Function(SheetResponse) completer; + + const AddToMyListBottomSheet({ + required this.request, + required this.completer, + super.key, + }); + + @override + Widget build(BuildContext context) { + return BaseBottomSheet( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 8), + child: Text( + 'Add vocab to list', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8), + child: const Text( + 'Identified vocab will be added to the selected list.\nWords with multiple options will be skipped.', + textAlign: TextAlign.center, + ), + ), + const Divider(height: 1), + Expanded( + child: ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: request.data.length, + itemBuilder: (context, index) => ListTile( + title: Text(request.data[index].name), + onTap: () => + completer(SheetResponse(data: request.data[index])), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/views/text_analysis/text_analysis_view.dart b/lib/ui/views/text_analysis/text_analysis_view.dart index 07b4a9f..927b5bd 100644 --- a/lib/ui/views/text_analysis/text_analysis_view.dart +++ b/lib/ui/views/text_analysis/text_analysis_view.dart @@ -71,8 +71,10 @@ class _Body extends StackedHookView { icon: const Icon(Icons.edit), ), IconButton( - onPressed: viewModel.copyText, - icon: const Icon(Icons.copy), + onPressed: viewModel.analysisFailed + ? null + : viewModel.addToDictionaryList, + icon: const Icon(Icons.playlist_add), ), ], }, diff --git a/lib/ui/views/text_analysis/text_analysis_viewmodel.dart b/lib/ui/views/text_analysis/text_analysis_viewmodel.dart index 24bdb21..9fed492 100644 --- a/lib/ui/views/text_analysis/text_analysis_viewmodel.dart +++ b/lib/ui/views/text_analysis/text_analysis_viewmodel.dart @@ -87,8 +87,32 @@ class TextAnalysisViewModel extends FutureViewModel { rebuildUi(); } - void copyText() { - Clipboard.setData(ClipboardData(text: _text)); + Future addToDictionaryList() async { + final myDictionaryLists = + await _dictionaryService.getAllMyDictionaryLists(); + + final response = await _bottomSheetService.showCustomSheet( + variant: BottomSheetType.addToMyListBottom, + data: myDictionaryLists, + ); + + if (response?.data == null) return; + + final List itemsToAdd = []; + for (var token in tokens!) { + if (token.associatedDictionaryItems != null && + token.associatedDictionaryItems!.length == 1 && + token.associatedDictionaryItems!.first is Vocab) { + itemsToAdd.add(token.associatedDictionaryItems!.first); + } + } + + await _dictionaryService.addManyToMyDictionaryList( + response!.data! as MyDictionaryList, + itemsToAdd, + ); + + _snackbarService.showSnackbar(message: 'Vocab added to list'); } void openAssociatedDictionaryItem(JapaneseTextToken token) { diff --git a/pubspec.lock b/pubspec.lock index fad5ab8..dbccea5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1244,8 +1244,8 @@ packages: dependency: "direct main" description: path: "." - ref: "6e44fb2" - resolved-ref: "6e44fb245bb5892e64026bc7424eb53ba7cd532c" + ref: "3df24c3" + resolved-ref: "3df24c3d43db9ff7adcc16d6b1a2d75bd242ba9e" url: "https://github.com/Moseco/sagase_dictionary" source: git version: "1.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 9c60130..49fc9ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: sagase_dictionary: git: url: https://github.com/Moseco/sagase_dictionary - ref: '6e44fb2' + ref: '3df24c3' # State management stacked: ^3.5.0 From 92b41ba0790d20d21a3cf669bf56b995c01ac33d Mon Sep 17 00:00:00 2001 From: Hampus Hammarlund Date: Thu, 5 Feb 2026 11:34:19 +0900 Subject: [PATCH 19/20] feat: split text by line break during text analysis --- .../text_analysis_viewmodel.dart | 51 +++-- .../widgets/text_analysis_viewing.dart | 201 ++++++++++-------- 2 files changed, 140 insertions(+), 112 deletions(-) diff --git a/lib/ui/views/text_analysis/text_analysis_viewmodel.dart b/lib/ui/views/text_analysis/text_analysis_viewmodel.dart index 9fed492..3fb7b8a 100644 --- a/lib/ui/views/text_analysis/text_analysis_viewmodel.dart +++ b/lib/ui/views/text_analysis/text_analysis_viewmodel.dart @@ -26,7 +26,7 @@ class TextAnalysisViewModel extends FutureViewModel { bool _addToHistory; - List? tokens; + List>? tokens; bool _analysisFailed = true; bool get analysisFailed => _analysisFailed; @@ -54,22 +54,31 @@ class TextAnalysisViewModel extends FutureViewModel { _analysisFailed = true; - tokens = _mecabService.parseText(_text); + final lines = _text.split('\n'); + tokens = []; - for (var token in tokens!) { - if (token.pos == PartOfSpeech.nounProper && - _sharedPreferencesService.getProperNounsEnabled()) { - token.associatedDictionaryItems = - await _dictionaryService.getProperNounByJapaneseTextToken(token); - } + for (var line in lines) { + String internalTrimmed = line.trim(); + if (internalTrimmed.isEmpty) continue; - if (token.associatedDictionaryItems == null || - token.associatedDictionaryItems!.isEmpty) { - token.associatedDictionaryItems = - await _dictionaryService.getVocabByJapaneseTextToken(token); - } - if (token.associatedDictionaryItems!.isNotEmpty) { - _analysisFailed = false; + final lineTokens = _mecabService.parseText(internalTrimmed); + tokens!.add(lineTokens); + + for (var token in lineTokens) { + if (token.pos == PartOfSpeech.nounProper && + _sharedPreferencesService.getProperNounsEnabled()) { + token.associatedDictionaryItems = + await _dictionaryService.getProperNounByJapaneseTextToken(token); + } + + if (token.associatedDictionaryItems == null || + token.associatedDictionaryItems!.isEmpty) { + token.associatedDictionaryItems = + await _dictionaryService.getVocabByJapaneseTextToken(token); + } + if (token.associatedDictionaryItems!.isNotEmpty) { + _analysisFailed = false; + } } } @@ -99,11 +108,13 @@ class TextAnalysisViewModel extends FutureViewModel { if (response?.data == null) return; final List itemsToAdd = []; - for (var token in tokens!) { - if (token.associatedDictionaryItems != null && - token.associatedDictionaryItems!.length == 1 && - token.associatedDictionaryItems!.first is Vocab) { - itemsToAdd.add(token.associatedDictionaryItems!.first); + for (var lineTokens in tokens!) { + for (var token in lineTokens) { + if (token.associatedDictionaryItems != null && + token.associatedDictionaryItems!.length == 1 && + token.associatedDictionaryItems!.first is Vocab) { + itemsToAdd.add(token.associatedDictionaryItems!.first); + } } } diff --git a/lib/ui/views/text_analysis/widgets/text_analysis_viewing.dart b/lib/ui/views/text_analysis/widgets/text_analysis_viewing.dart index 2636cf6..f6a74f3 100644 --- a/lib/ui/views/text_analysis/widgets/text_analysis_viewing.dart +++ b/lib/ui/views/text_analysis/widgets/text_analysis_viewing.dart @@ -12,110 +12,128 @@ class TextAnalysisViewing extends ViewModelWidget { @override Widget build(BuildContext context, TextAnalysisViewModel viewModel) { - List textChildren = []; + List lineWidgets = []; List associatedVocabChildren = []; - for (var token in viewModel.tokens!) { - List data = []; - // Create writing buffer to be used in case of multiple associated vocab - final writing = StringBuffer(); - // Add main pairs - for (var rubyPair in token.rubyTextPairs) { - writing.write(rubyPair.writing); - data.add( - RubyTextData( - rubyPair.writing, - ruby: rubyPair.reading, - ), - ); - } - // Add any trailing pairs - if (token.trailing != null) { - for (var trailing in token.trailing!) { - for (var rubyPair in trailing.rubyTextPairs) { - writing.write(rubyPair.writing); - data.add( - RubyTextData( - rubyPair.writing, - ruby: rubyPair.reading, - ), - ); + for (var lineTokens in viewModel.tokens!) { + List textChildren = []; + + for (var token in lineTokens) { + List data = []; + + // Create writing buffer to be used in case of multiple associated vocab + final writing = StringBuffer(); + // Add main pairs + for (var rubyPair in token.rubyTextPairs) { + writing.write(rubyPair.writing); + data.add( + RubyTextData( + rubyPair.writing, + ruby: rubyPair.reading, + ), + ); + } + // Add any trailing pairs + if (token.trailing != null) { + for (var trailing in token.trailing!) { + for (var rubyPair in trailing.rubyTextPairs) { + writing.write(rubyPair.writing); + data.add( + RubyTextData( + rubyPair.writing, + ruby: rubyPair.reading, + ), + ); + } } } - } - textChildren.add( - GestureDetector( - onTap: () => viewModel.openAssociatedDictionaryItem(token), - onLongPress: () => viewModel.copyToken(token), - child: Container( - decoration: BoxDecoration( - border: token.associatedDictionaryItems!.isNotEmpty - ? Border( - bottom: BorderSide( - color: Theme.of(context).textTheme.bodyMedium!.color!, - ), - ) - : null, - ), - child: RubyText( - data, - style: const TextStyle( - fontSize: 24, - letterSpacing: 0, - height: 1.1, + textChildren.add( + GestureDetector( + onTap: () => viewModel.openAssociatedDictionaryItem(token), + onLongPress: () => viewModel.copyToken(token), + child: Container( + decoration: BoxDecoration( + border: token.associatedDictionaryItems!.isNotEmpty + ? Border( + bottom: BorderSide( + color: Theme.of(context).textTheme.bodyMedium!.color!, + ), + ) + : null, + ), + child: RubyText( + data, + style: const TextStyle( + fontSize: 24, + letterSpacing: 0, + height: 1.1, + ), + rubyStyle: const TextStyle(height: 1.2), ), - rubyStyle: const TextStyle(height: 1.2), ), ), - ), - ); + ); - if (token.associatedDictionaryItems != null && - token.associatedDictionaryItems!.isNotEmpty) { - if (token.associatedDictionaryItems!.length == 1) { - if (token.associatedDictionaryItems![0] is Vocab) { - associatedVocabChildren.add( - VocabListItem( - vocab: token.associatedDictionaryItems![0] as Vocab, - onPressed: () => viewModel.openAssociatedDictionaryItem(token), - ), - ); + if (token.associatedDictionaryItems != null && + token.associatedDictionaryItems!.isNotEmpty) { + if (token.associatedDictionaryItems!.length == 1) { + if (token.associatedDictionaryItems![0] is Vocab) { + associatedVocabChildren.add( + VocabListItem( + vocab: token.associatedDictionaryItems![0] as Vocab, + onPressed: () => + viewModel.openAssociatedDictionaryItem(token), + ), + ); + } else { + associatedVocabChildren.add( + ProperNounListItem( + properNoun: token.associatedDictionaryItems![0] as ProperNoun, + onPressed: () => + viewModel.openAssociatedDictionaryItem(token), + ), + ); + } } else { + // Multiple dictionary item associatedVocabChildren.add( - ProperNounListItem( - properNoun: token.associatedDictionaryItems![0] as ProperNoun, - onPressed: () => viewModel.openAssociatedDictionaryItem(token), + InkWell( + onTap: () => viewModel.openAssociatedDictionaryItem(token), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Multiple options for $writing', + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + const Text( + 'Tap to view', + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ), + ), ), ); } - } else { - // Multiple dictionary item - associatedVocabChildren.add( - InkWell( - onTap: () => viewModel.openAssociatedDictionaryItem(token), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Multiple options for $writing', - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - const Text( - 'Tap to view', - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ], - ), - ), - ), - ); } } + + if (textChildren.isNotEmpty) { + lineWidgets.add( + Wrap( + crossAxisAlignment: WrapCrossAlignment.end, + spacing: 6, + children: textChildren, + ), + ); + } } final padding = MediaQuery.of(context).padding; @@ -133,10 +151,9 @@ class TextAnalysisViewing extends ViewModelWidget { padding: const EdgeInsets.all(8), child: SizedBox( width: double.infinity, - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.end, - spacing: 6, - children: textChildren, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: lineWidgets, ), ), ), From 800131a6f66f64835349ba7d2ea520a04b453a36 Mon Sep 17 00:00:00 2001 From: Moseco Date: Wed, 11 Feb 2026 20:53:20 +0900 Subject: [PATCH 20/20] rc/1.5.0 (#78) --- lib/ui/views/about/about_view.dart | 2 +- lib/ui/views/changelog/changelog_view.dart | 26 ++++++++++++------- .../flashcards/flashcards_viewmodel.dart | 3 +-- lib/ui/views/settings/settings_view.dart | 18 ++++++------- lib/utils/constants.dart | 2 +- pubspec.yaml | 2 +- .../flashcards/flashcards_viewmodel_test.dart | 1 - 7 files changed, 29 insertions(+), 25 deletions(-) diff --git a/lib/ui/views/about/about_view.dart b/lib/ui/views/about/about_view.dart index aad3b5d..1a431b3 100644 --- a/lib/ui/views/about/about_view.dart +++ b/lib/ui/views/about/about_view.dart @@ -32,7 +32,7 @@ class AboutView extends StackedView { 'Sagase', style: TextStyle(fontSize: 24), ), - const Text('1.4.2'), + const Text('1.5.0'), const SizedBox(height: 16), Text.rich( textAlign: TextAlign.left, diff --git a/lib/ui/views/changelog/changelog_view.dart b/lib/ui/views/changelog/changelog_view.dart index 537ff8d..a7b33f4 100644 --- a/lib/ui/views/changelog/changelog_view.dart +++ b/lib/ui/views/changelog/changelog_view.dart @@ -72,29 +72,29 @@ class _CurrentChangelog extends StatelessWidget { ), ), Text( - 'What\'s new in 1.4', + 'What\'s new in 1.5', style: TextStyle(fontSize: 24), ), SizedBox(height: 20), ListTile( - leading: Icon(Icons.format_list_bulleted), - title: Text('New vocab lists'), + leading: Icon(Icons.camera), + title: Text('New and improved OCR'), subtitle: Text( - 'Added Kaishi 1.5k and core vocab lists', + 'Find words in photos right from the search screen', ), ), ListTile( - leading: Icon(Icons.search), - title: Text('Better search'), + leading: Icon(Icons.playlist_add), + title: Text('From text to flashcards'), subtitle: Text( - 'Sorting is greatly improved and you can now use wildcards', + 'Bulk import vocab directly from text or images into your custom lists', ), ), ListTile( - leading: Icon(Icons.text_snippet), - title: Text('Better text analysis'), + leading: Icon(Icons.school), + title: Text('More flashcard options'), subtitle: Text( - 'Text analysis is now much better at identifying vocab', + 'Keep your studying on track with more options to manage new and due flashcards', ), ), ], @@ -111,6 +111,12 @@ class _ChangelogHistory extends StatelessWidget { return const SizedBox( width: double.infinity, child: Markdown(data: ''' +# [1.5.0] +- Full release of OCR with overhauled UI in the text analysis screen and ability to use OCR directly from the search screen +- Added ability to bulk import all identified vocab from text analysis into your custom lists +- Added option to space out flashcards if a large amount of due flashcards pile up +- Added option to restrict how many new flashcards are shown when no due flashcards are available +- Fixed performance issues during onboarding # [1.4.2] - Fixed disk space calculation during dictionary download - Fixed crash effecting some users the first time they opened flashcards diff --git a/lib/ui/views/flashcards/flashcards_viewmodel.dart b/lib/ui/views/flashcards/flashcards_viewmodel.dart index 8983994..258f750 100644 --- a/lib/ui/views/flashcards/flashcards_viewmodel.dart +++ b/lib/ui/views/flashcards/flashcards_viewmodel.dart @@ -344,8 +344,7 @@ class FlashcardsViewModel extends FutureViewModel { // If active flashcards is still empty then try to add new flashcards Future?>? reportDialogResponse; if (activeFlashcards.isEmpty) { - if (_sharedPreferencesService.getFlashcardLearningModeEnabled() && - _sharedPreferencesService.getAddNewFlashcardsInBatches()) { + if (_sharedPreferencesService.getAddNewFlashcardsInBatches()) { if (initial) newFlashcards.shuffle(_random); int newFlashcardsToAdd = max( diff --git a/lib/ui/views/settings/settings_view.dart b/lib/ui/views/settings/settings_view.dart index 54038bb..0525d4c 100644 --- a/lib/ui/views/settings/settings_view.dart +++ b/lib/ui/views/settings/settings_view.dart @@ -88,16 +88,7 @@ class SettingsView extends StatelessWidget { 'If enabled, a set amount of new flashcards will be included with due flashcards. You can also long press a flashcard set to open in a different mode.', ), ), - SettingsTile.navigation( - enabled: viewModel.flashcardLearningModeEnabled, - title: const Text('Set new flashcards per day'), - description: const Text( - 'The amount of new flashcards to be added along with due cards while in learning mode.', - ), - onPressed: (_) => viewModel.setNewFlashcardsPerDay(), - ), SettingsTile.switchTile( - enabled: viewModel.flashcardLearningModeEnabled, initialValue: viewModel.addNewFlashcardsInBatches, onToggle: viewModel.setAddNewFlashcardsInBatches, activeSwitchColor: Theme.of(context).colorScheme.primary, @@ -106,6 +97,15 @@ class SettingsView extends StatelessWidget { 'If enabled, when no due flashcards are available new flashcards are added in batches instead of all at once.', ), ), + SettingsTile.navigation( + enabled: viewModel.flashcardLearningModeEnabled || + viewModel.addNewFlashcardsInBatches, + title: const Text('Set new flashcard amount'), + description: const Text( + 'The amount of new flashcards added if learning mode or add new flashcards in batches are enabled.', + ), + onPressed: (_) => viewModel.setNewFlashcardsPerDay(), + ), SettingsTile.navigation( title: const Text('Set initial spaced repetition interval'), diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 7c8ab4b..40249e1 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -50,4 +50,4 @@ const isarDatabaseFile = 'default.isar'; const isarDatabaseLockFile = 'default.isar.lock'; const ocrImagesDir = 'ocr_images'; -const currentChangelogVersion = 4; +const currentChangelogVersion = 5; diff --git a/pubspec.yaml b/pubspec.yaml index 49fc9ca..2e28996 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A Japanese-English dictionary and learning app. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.4.2+23 +version: 1.5.0+26 environment: sdk: '>=3.0.5 <4.0.0' diff --git a/test/ui/views/flashcards/flashcards_viewmodel_test.dart b/test/ui/views/flashcards/flashcards_viewmodel_test.dart index 5ba7ac7..96c1742 100644 --- a/test/ui/views/flashcards/flashcards_viewmodel_test.dart +++ b/test/ui/views/flashcards/flashcards_viewmodel_test.dart @@ -1987,7 +1987,6 @@ void main() { () async { // Set shared preferences getAndRegisterSharedPreferencesService( - getFlashcardLearningModeEnabled: true, getNewFlashcardsPerDay: 1, getAddNewFlashcardsInBatches: true, );