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 f2ceda0..cd07485 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.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 84a351f..0727976 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,77 +1,76 @@ PODS: - - app_links (0.0.2): + - app_links (7.0.0): + - Flutter + - camera_avfoundation (0.0.1): + - Flutter + - device_info_plus (0.0.1): - Flutter - disk_space_plus (0.0.1): - Flutter - - Firebase/Analytics (11.10.0): - - Firebase/Core - - Firebase/Core (11.10.0): - - Firebase/CoreOnly - - FirebaseAnalytics (~> 11.10.0) - - Firebase/CoreOnly (11.10.0): - - FirebaseCore (~> 11.10.0) - - Firebase/Crashlytics (11.10.0): + - Firebase/CoreOnly (12.8.0): + - FirebaseCore (~> 12.8.0) + - Firebase/Crashlytics (12.8.0): - Firebase/CoreOnly - - FirebaseCrashlytics (~> 11.10.0) - - firebase_analytics (11.4.5): - - Firebase/Analytics (= 11.10.0) + - FirebaseCrashlytics (~> 12.8.0) + - firebase_analytics (12.1.1): - firebase_core + - FirebaseAnalytics (= 12.8.0) - Flutter - - firebase_core (3.13.0): - - Firebase/CoreOnly (= 11.10.0) + - firebase_core (4.4.0): + - Firebase/CoreOnly (= 12.8.0) - Flutter - - firebase_crashlytics (4.3.5): - - Firebase/Crashlytics (= 11.10.0) + - firebase_crashlytics (5.0.7): + - Firebase/Crashlytics (= 12.8.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.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/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.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 (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.8.0): + - FirebaseCoreInternal (~> 12.8.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreExtension (12.8.0): + - FirebaseCore (~> 12.8.0) + - FirebaseCoreInternal (12.8.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - 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 (11.10.0): - - FirebaseCore (~> 11.10.0) - - GoogleUtilities/Environment (~> 8.0) - - GoogleUtilities/UserDefaults (~> 8.0) + - FirebaseInstallations (12.8.0): + - FirebaseCore (~> 12.8.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.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) - nanopb (~> 3.30910.0) - PromisesSwift (~> 2.1) - Flutter (1.0.0) @@ -94,25 +93,32 @@ 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 (3.2.0): + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - nanopb (~> 3.30910.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/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/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/WithoutAdIdSupport (11.10.0): - - GoogleUtilities/AppDelegateSwizzler (~> 8.0) - - GoogleUtilities/MethodSwizzler (~> 8.0) - - GoogleUtilities/Network (~> 8.0) - - "GoogleUtilities/NSData+zlib (~> 8.0)" + - GoogleAppMeasurement/IdentitySupport (12.8.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) - GoogleDataTransport (10.1.0): - nanopb (~> 3.30910.0) @@ -133,31 +139,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) @@ -165,7 +171,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 @@ -205,9 +211,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) @@ -218,34 +221,39 @@ 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.51.1): + - sqlite3/common (= 3.51.1) + - sqlite3/common (3.51.1) + - sqlite3/dbstatvtab (3.51.1): + - sqlite3/common + - sqlite3/fts5 (3.51.1): - sqlite3/common - - sqlite3/fts5 (3.49.1): + - sqlite3/math (3.51.1): - sqlite3/common - - sqlite3/math (3.49.1): + - sqlite3/perf-threadsafe (3.51.1): - sqlite3/common - - sqlite3/perf-threadsafe (3.49.1): + - sqlite3/rtree (3.51.1): - sqlite3/common - - sqlite3/rtree (3.49.1): + - sqlite3/session (3.51.1): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - FlutterMacOS - - sqlite3 (~> 3.49.1) + - sqlite3 (~> 3.51.1) - sqlite3/dbstatvtab - sqlite3/fts5 - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree + - sqlite3/session - SSZipArchive (2.6.0) - url_launcher_ios (0.0.1): - Flutter 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`) @@ -261,9 +269,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`) @@ -281,6 +288,7 @@ SPEC REPOS: - FirebaseInstallations - FirebaseRemoteConfigInterop - FirebaseSessions + - GoogleAdsOnDeviceConversion - GoogleAppMeasurement - GoogleDataTransport - GoogleMLKit @@ -304,6 +312,10 @@ SPEC REPOS: EXTERNAL SOURCES: app_links: :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: @@ -332,12 +344,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: @@ -350,21 +360,23 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 + app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8 + camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741 + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe 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 - 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 @@ -372,15 +384,16 @@ SPEC CHECKSUMS: google_mlkit_commons: 2abe6a70e1824e431d16a51085cb475b672c8aab google_mlkit_digital_ink_recognition: 17bf08581ec4c778fe1ac525302fd3a10e8799e6 google_mlkit_text_recognition: ec2122ec89bfe0d7200763336a6e4ef44810674c - GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d + GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f + GoogleAppMeasurement: 72c9a682fec6290327ea5e3c4b829b247fcb2c17 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 - GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + 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 @@ -391,16 +404,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: fc1400008a9b3525f5914ed715a5d1af0b8f4983 - sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + 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/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"> addManyToMyDictionaryList( + MyDictionaryList dictionaryList, + List dictionaryItems, + ) async { + await _database.myDictionaryListsDao + .addDictionaryItems(dictionaryList, dictionaryItems); + } + Future removeFromMyDictionaryList( MyDictionaryList dictionaryList, DictionaryItem dictionaryItem, @@ -465,6 +474,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/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/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/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/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/painters/ocr_painter.dart b/lib/ui/painters/ocr_painter.dart new file mode 100644 index 0000000..71a8a59 --- /dev/null +++ b/lib/ui/painters/ocr_painter.dart @@ -0,0 +1,49 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:sagase/datamodels/recognized_text_block.dart'; + +class OcrPainter extends CustomPainter { + final List? recognizedTextBlocks; + final Size imageSize; + + OcrPainter(this.recognizedTextBlocks, this.imageSize); + + @override + void paint(Canvas canvas, Size size) { + if (recognizedTextBlocks != null) { + final unselectedPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 4 + ..color = Colors.lightBlueAccent; + + final selectedPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 4 + ..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, + ); + } + } + } + + @override + bool shouldRepaint(OcrPainter oldDelegate) { + return oldDelegate.recognizedTextBlocks != recognizedTextBlocks; + } +} 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/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/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..258f750 100644 --- a/lib/ui/views/flashcards/flashcards_viewmodel.dart +++ b/lib/ui/views/flashcards/flashcards_viewmodel.dart @@ -344,11 +344,25 @@ 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.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(); @@ -716,7 +730,6 @@ class FlashcardsViewModel extends FutureViewModel { ); if (response != null && response.confirmed) { - // Show progress indicator dialog _dialogService.showCustomDialog( variant: DialogType.progressIndicator, title: 'Updating flashcards', @@ -724,20 +737,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()); } 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/ocr/ocr_view.dart b/lib/ui/views/ocr/ocr_view.dart index f4dd94a..1570763 100644 --- a/lib/ui/views/ocr/ocr_view.dart +++ b/lib/ui/views/ocr/ocr_view.dart @@ -1,10 +1,13 @@ 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'; -import 'painters/text_detector_painter.dart'; class OcrView extends StackedView { final bool cameraStart; @@ -15,44 +18,80 @@ 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.currentImageBytes == null + actions: viewModel.state == OcrState.waiting || + viewModel.state == OcrState.loading ? 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: Column( - children: [ - Expanded( - child: viewModel.currentImageBytes == null - ? _OcrImageLoading() - : _OcrImage(), - ), - const Divider(indent: 8, endIndent: 8), - Expanded( - child: viewModel.recognizedTextBlocks == null - ? Column( - children: [ - ListItemLoading(), - const SizedBox(height: 8), - ListItemLoading(), - ], - ) - : _SelectedText(), - ), - ], - ), + body: viewModel.state == OcrState.error + ? _Error(controller) + : 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(), + ], + ) + }, + ), + ], + ), ); } } @@ -80,97 +119,99 @@ class _OcrImageLoading extends StatelessWidget { } } -class _OcrImage extends ViewModelWidget { +class _SelectedText extends ViewModelWidget { + final TextEditingController controller; + + const _SelectedText(this.controller); + @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, + 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...', ), - child: IgnorePointer( - child: Image.memory(viewModel.currentImageBytes!), + 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), + ), + onPressed: () => viewModel.analyzeText(controller.text), + ), + ), + ), + ], ), ); } } -class _SelectedText extends ViewModelWidget { +class _Error extends ViewModelWidget { + final TextEditingController controller; + + const _Error(this.controller); + @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), - ), + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Failed to analyze image', + style: TextStyle(fontSize: 18), ), - ), - ); - } 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), - ); - }, - ); - } - - return Column( - children: [ - Expanded(child: textSection), - Container( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom, + const SizedBox(height: 8), + const Text('Please try again'), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: () { + viewModel.openCamera(); + controller.clear(); + }, + icon: const Icon(Icons.camera), + label: const Text('Open camera'), ), - width: double.infinity, - color: Colors.deepPurple, - child: TextButton.icon( - icon: const Icon(Icons.text_snippet, color: Colors.white), - label: Text( - viewModel.recognizedTextBlocks!.length == 1 - ? 'Analyze text' - : 'Analyze selected text', - style: TextStyle(color: Colors.white), - ), - onPressed: viewModel.analyzeSelectedText, + ElevatedButton.icon( + onPressed: () { + viewModel.selectImage(); + controller.clear(); + }, + icon: const Icon(Icons.photo), + label: const Text('Pick from photos'), ), - ), - ], + ], + ), ); } } diff --git a/lib/ui/views/ocr/ocr_viewmodel.dart b/lib/ui/views/ocr/ocr_viewmodel.dart index c447e6d..d899179 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; + OcrState _state = OcrState.waiting; + OcrState get state => _state; - List? _recognizedTextBlocks; - List? get recognizedTextBlocks => _recognizedTextBlocks; + XFile? _image; + XFile? get image => _image; OcrViewModel(bool cameraStart) { if (cameraStart) { @@ -49,14 +32,12 @@ class OcrViewModel extends BaseViewModel { } Future _processImage(ImageSource imageSource) async { - _currentImageBytes = null; - _recognizedTextBlocks = null; + _image = null; + _state = OcrState.waiting; 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', @@ -68,89 +49,41 @@ class OcrViewModel extends BaseViewModel { 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); + _state = OcrState.loading; - _currentImageBytes = await rotatedImage.readAsBytes(); rebuildUi(); + } - if (inputImage.metadata != null) { - _imageSize = inputImage.metadata!.size; + void handleImageProcessed(int length) { + if (length == 0) { + _state = OcrState.viewEmpty; } 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, - ), - ); + _state = OcrState.viewing; } - rebuildUi(); - - await rotatedImage.delete(); } - void reorderList(int oldIndex, int newIndex) { - if (oldIndex < newIndex) newIndex -= 1; - - _recognizedTextBlocks!.insert( - newIndex, - _recognizedTextBlocks!.removeAt(oldIndex), - ); + void handleImageError() { + _image = null; + _state = OcrState.error; rebuildUi(); } - void toggleCheckBox(int index, bool value) { - _recognizedTextBlocks![index].selected = value; + void handleTextSelected() { 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); - } + void analyzeText(String text) { + if (text.isEmpty) return; - if (lines.isEmpty) return; - - _navigationService.back(result: lines.join('\n')); + _navigationService.back(result: text); } } + +enum OcrState { + waiting, + loading, + viewing, + viewEmpty, + error, +} diff --git a/lib/ui/views/ocr/painters/text_detector_painter.dart b/lib/ui/views/ocr/painters/text_detector_painter.dart deleted file mode 100644 index d385cb6..0000000 --- a/lib/ui/views/ocr/painters/text_detector_painter.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'dart:math'; -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:sagase/datamodels/recognized_text_block.dart'; - -class TextRecognizerPainter extends CustomPainter { - final List recognizedTextBlocks; - final Size imageSize; - - TextRecognizerPainter(this.recognizedTextBlocks, this.imageSize); - - @override - void paint(Canvas canvas, Size size) { - final unselectedPaint = Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = 2 - ..color = Colors.blueGrey; - - 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, - ); - } - } - - @override - bool shouldRepaint(TextRecognizerPainter oldDelegate) { - //TODO the painting isn't that intense so probably fine? - return true; - // return oldDelegate.recognizedTextBlocks != recognizedTextBlocks; - } - - @override - bool? hitTest(Offset position) { - for (final textBlock in recognizedTextBlocks) { - if (_pointInsideRectangle(position, textBlock.offsets)) { - textBlock.selected = true; - 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/search/search_view.dart b/lib/ui/views/search/search_view.dart index 98191d7..0ded796 100644 --- a/lib/ui/views/search/search_view.dart +++ b/lib/ui/views/search/search_view.dart @@ -15,6 +15,8 @@ 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}); @@ -51,137 +53,17 @@ class _Body extends StackedHookView { viewModel.searchResult == null ? _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, - ), - ], - ), - ), - ], - ), + if (viewModel.inputMode == InputMode.handWriting) + HandWritingInput( + searchController: searchController, + handWritingController: handWritingController, + keyboardFocusNode: keyboardFocusNode, + ), + if (viewModel.inputMode == InputMode.ocr) + OcrWidget( + searchController: searchController, + handWritingController: handWritingController, + keyboardFocusNode: keyboardFocusNode, ), ], ), @@ -213,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), @@ -241,126 +134,74 @@ 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.inputMode != InputMode.text, + readOnly: viewModel.inputMode != InputMode.text, + showCursor: true, + autocorrect: false, + enableIMEPersonalizedLearning: false, + maxLines: 1, + textInputAction: viewModel.inputMode != InputMode.text + ? null + : TextInputAction.search, + focusNode: viewModel.inputMode != InputMode.text + ? 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(); + viewModel.inputMode != InputMode.text + ? handWritingFocusNode.requestFocus() + : keyboardFocusNode.requestFocus(); + }, + icon: Icon( + Icons.clear, + color: Theme.of(context).iconTheme.color, + ), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + ), + ), ), IconButton( onPressed: viewModel.setSearchFilter, @@ -393,7 +234,6 @@ class _SearchResults extends ViewModelWidget { ); } - // Show results return Expanded( child: ListView.separated( key: UniqueKey(), @@ -458,10 +298,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/search_viewmodel.dart b/lib/ui/views/search/search_viewmodel.dart index 1f8a132..aef704b 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,57 @@ 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; + _ocrError = false; + locator().setShowNavigationBar(mode == InputMode.text); + + rebuildUi(); + } + + Future handlePictureTaken(XFile image) async { + _image = image; + 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; + _ocrError = false; + 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 new file mode 100644 index 0000000..f0880bb --- /dev/null +++ b/lib/ui/views/search/widgets/hand_writing_input.dart @@ -0,0 +1,152 @@ +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.setInputMode(InputMode.ocr), + icon: const Icon(Icons.camera_alt), + 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: 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, + ), + ], + ), + ), + ], + ), + ); + } +} 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..b53e682 --- /dev/null +++ b/lib/ui/views/search/widgets/ocr_widget.dart @@ -0,0 +1,94 @@ +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.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) { + text = text.replaceAll('\n', ''); + searchController.text = text; + viewModel.handleTextSelected(text); + }, + locked: true, + singleSelection: true, + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/views/settings/settings_view.dart b/lib/ui/views/settings/settings_view.dart index 2f09a9f..0525d4c 100644 --- a/lib/ui/views/settings/settings_view.dart +++ b/lib/ui/views/settings/settings_view.dart @@ -14,194 +14,210 @@ 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.switchTile( + 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.', + ), ), - 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( + 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(), ), - 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 initial spaced repetition interval'), + onPressed: (_) => + viewModel.setInitialSpacedRepetitionInterval(), ), - ), - 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 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(), ), - ), - ], - ), - 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.navigation( + title: const Text( + 'Set correct answers required to complete a new flashcard'), + onPressed: (_) => + viewModel.setFlashcardCorrectAnswersRequired(), ), - ), - 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.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('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.', + 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.', + ), ), - 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.', + ], + ), + 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.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.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.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..39f6cf1 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'; @@ -30,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(); @@ -47,6 +50,10 @@ class SettingsViewModel extends BaseViewModel { _navigationService.navigateTo(Routes.devView); } + void handleBackButton() { + locator().handleBackButton(); + } + void setInitialCorrectInterval(int value) { _sharedPreferencesService.setInitialCorrectInterval(value); notifyListeners(); @@ -102,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/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..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; + } } } @@ -87,8 +96,34 @@ 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 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); + } + } + } + + await _dictionaryService.addManyToMyDictionaryList( + response!.data! as MyDictionaryList, + itemsToAdd, + ); + + _snackbarService.showSnackbar(message: 'Vocab added to list'); } void openAssociatedDictionaryItem(JapaneseTextToken token) { 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, ), ), ), diff --git a/lib/ui/widgets/camera_viewfinder.dart b/lib/ui/widgets/camera_viewfinder.dart new file mode 100644 index 0000000..0691b18 --- /dev/null +++ b/lib/ui/widgets/camera_viewfinder.dart @@ -0,0 +1,143 @@ +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; + + 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; + } + } + + // This is a temporary workaround for iPhone 17 family devices + final iOS = Platform.isIOS ? await DeviceInfoPlugin().iosInfo : null; + + _controller = CameraController( + cameraToUse, + iOS != null && iOS.utsname.machine.contains("iPhone18") + ? ResolutionPreset.ultraHigh + : 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, + foregroundColor: Colors.white, + backgroundColor: Colors.deepPurple, + 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..bf9feb0 --- /dev/null +++ b/lib/ui/widgets/ocr_image.dart @@ -0,0 +1,282 @@ +import 'dart:io'; +import 'dart:math'; +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(int)? onImageProcessed; + final void Function() onImageError; + final void Function(String) onTextSelected; + final bool locked; + final bool singleSelection; + + const OcrImage({ + super.key, + required this.image, + 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(() {}); + + if (widget.onImageProcessed != null) { + widget.onImageProcessed!(_recognizedTextBlocks!.length); + } + + 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: GestureDetector( + behavior: HitTestBehavior.opaque, + onTapUp: _handleTapUp, + child: CustomPaint( + foregroundPainter: OcrPainter( + recognizedTextBlocks, + imageSize, + ), + child: Image.memory(image), + ), + ), + ), + ); + }, + ); + } else { + return LayoutBuilder( + builder: (context, constraints) { + return InteractiveViewer( + constrained: false, + child: SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + child: FittedBox( + clipBehavior: Clip.hardEdge, + fit: BoxFit.contain, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTapUp: _handleTapUp, + child: CustomPaint( + foregroundPainter: OcrPainter( + recognizedTextBlocks, + imageSize, + ), + child: Image.memory(image), + ), + ), + ), + ), + ); + }, + ); + } + } + + 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; + } +} diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index cee02d3..40249e1 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; @@ -48,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.lock b/pubspec.lock index 18821eb..dbccea5 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: de9ecbb3ddafd446095f7e833c853aff2fa1682b017921fe63a833f9d6f0e422 + sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182 url: "https://pub.dev" source: hosted - version: "1.3.54" - _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: @@ -66,10 +61,10 @@ packages: dependency: "direct main" description: name: archive - sha256: a7f37ff061d7abc2fcf213554b9dcaca713c5853afa5c065c44888bc9ccaf813 + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "4.0.7" args: dependency: transitive description: @@ -82,10 +77,10 @@ packages: dependency: "direct main" description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -98,50 +93,50 @@ packages: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d url: "https://pub.dev" source: hosted - version: "2.4.2" + 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: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 + sha256: d1d57f7807debd7349b4726a19fd32ec8bc177c71ad0febf91a20f84cd2d4b46 url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.3" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" + sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30 url: "https://pub.dev" source: hosted - version: "2.4.15" + version: "2.7.1" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "9.3.1" built_collection: dependency: transitive description: @@ -154,10 +149,50 @@ packages: dependency: transitive description: name: built_value - sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" + url: "https://pub.dev" + source: hosted + version: "8.12.3" + camera: + dependency: "direct main" + description: + name: camera + sha256: eefad89f262a873f38d21e5eec853461737ea074d7c9ede39f3ceb135d201cab + url: "https://pub.dev" + source: hosted + version: "0.11.3" + camera_android_camerax: + dependency: transitive + description: + name: camera_android_camerax + sha256: "1abae0a9853401ee875d7f99e9ec5083335f0aab6edf6eb9a219756143a1b3a8" url: "https://pub.dev" source: hosted - version: "8.9.5" + version: "0.6.29" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: a600b60a7752cc5fa9de476cd0055539d7a3b9d62662f4f446bae49eba2267df + url: "https://pub.dev" + source: hosted + version: "0.9.22+9" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63" + url: "https://pub.dev" + source: hosted + version: "2.12.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7" + url: "https://pub.dev" + source: hosted + version: "0.3.5+3" characters: dependency: transitive description: @@ -170,18 +205,18 @@ packages: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" checks: dependency: transitive description: name: checks - sha256: aad431b45a8ae2fa26db8c22e385b9cdec73f72986a1d9d9f2017f4c39ecf5c9 + sha256: "016871c84732c1ac9856b8940236d5a5802ba638b3bd3e0ea7027b51a35f7aa7" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.1" clock: dependency: transitive description: @@ -190,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: @@ -218,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: @@ -242,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: @@ -254,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: @@ -266,10 +325,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25 url: "https://pub.dev" source: hosted - version: "5.8.0+1" + version: "5.9.1" dio_web_adapter: dependency: transitive description: @@ -298,42 +357,42 @@ 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: "0cadbf3b8733409a6cf61d18ba2e94e149df81df7de26f48ae0695b48fd71922" + sha256: c07120854742a0cae2f7501a0da02493addde550db6641d284983c08762e60a7 url: "https://pub.dev" source: hosted - version: "0.2.4" + 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: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: 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: @@ -346,98 +405,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: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" url: "https://pub.dev" source: hosted - version: "0.9.4+2" + 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: "2416b9d864412ab7b571dafded801bbcc7e29b5824623c055002d4d0819bea2b" + sha256: "91e2739bad690da2826c0cd5b28328fd15fb87adf54634cded703f34fd797a81" url: "https://pub.dev" source: hosted - version: "11.4.5" + version: "12.1.1" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - sha256: "3ccf5c876a8bea186016de4bcf53fc1bc6fa01236d740fb501d7ef9be356c58e" + sha256: "62fd3f27f342c898bd819fb97fa87c0b971e9fbe03357477282c0e14e1a40c3c" url: "https://pub.dev" source: hosted - version: "4.3.5" + version: "5.0.6" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - sha256: "5e4e3f001b67c2034b76cb2a42a0eed330fb3a8fb41ad13eceb04e8d9a74f662" + sha256: "8fc488bb008439fc3b850cfac892dec1ff4cd438eee44438919a14c5e61b9828" url: "https://pub.dev" source: hosted - version: "0.5.10+11" + version: "0.6.1+2" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "017d17d9915670e6117497e640b2859e0b868026ea36bf3a57feb28c3b97debe" + sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80" url: "https://pub.dev" source: hosted - version: "3.13.0" + version: "4.4.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf + sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "129a34d1e0fb62e2b488d988a1fc26cc15636357e50944ffee2862efe8929b23" + sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084" url: "https://pub.dev" source: hosted - version: "2.22.0" + version: "3.4.0" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: f3fa4a17c2f061b16b2e3ac7aaed889ae954b8952d0fd3e0009a9870cde7bbd2 + sha256: a6e6cb8b2ea1214533a54e4c1b11b19c40f6a29333f3ab0854a479fdc3237c5b url: "https://pub.dev" source: hosted - version: "4.3.5" + version: "5.0.7" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: cedfbe39927711c0e56fc38bfecbd89e17816b21698a3d88d63298c530ed375c + sha256: fc6837c4c64c48fa94cab8a872a632b9194fa9208ca76a822f424b3da945584d url: "https://pub.dev" source: hosted - version: "3.8.5" + version: "3.8.17" fixnum: dependency: transitive description: @@ -450,10 +509,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: f2e9137f261d0f53a820f6b829c80ba570ac915284c8e32789d973834796eca0 + sha256: "7ca9a40f4eb85949190e54087be8b4d6ac09dc4c54238d782a34cf1f7c011de9" url: "https://pub.dev" source: hosted - version: "0.71.0" + version: "1.1.1" flip_card: dependency: "direct main" description: @@ -481,18 +540,18 @@ 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: name: flutter_hooks - sha256: "6a126f703b89499818d73305e4ce1e3de33b4ae1c5512e3b8eab4b986f46774c" + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 url: "https://pub.dev" source: hosted - version: "0.18.6" + version: "0.20.5" flutter_keyboard_visibility: dependency: transitive description: @@ -545,51 +604,51 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_markdown: dependency: "direct main" description: name: flutter_markdown - sha256: "634622a3a826d67cb05c0e3e576d1812c430fa98404e95b60b131775c73d76ec" + sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.7+1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3" + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 url: "https://pub.dev" source: hosted - version: "2.0.27" + 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: dependency: "direct main" description: name: flutter_sticky_header - sha256: "7f76d24d119424ca0c95c146b8627a457e8de8169b0d584f766c2c545db8f8be" + sha256: fb4fda6164ef3e5fc7ab73aba34aad253c17b7c6ecf738fa26f1a905b7d2d1e2 url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.8.0" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -604,10 +663,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: @@ -620,18 +679,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: f126a3e286b7f5b578bf436d5592968706c4c1de28a228b870ce375d9f743103 + sha256: ae78de7c3f2304b8d81f2bb6e320833e5e81de942188542328f074978cc0efa9 url: "https://pub.dev" source: hosted - version: "8.0.3" + version: "8.3.0" glob: dependency: transitive description: @@ -643,26 +702,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" @@ -688,14 +750,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: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.6.0" http_multi_server: dependency: transitive description: @@ -716,74 +786,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: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9" + sha256: "518a16108529fc18657a3e6dde4a043dc465d16596d20ab2abd49a4cac2e703d" url: "https://pub.dev" source: hosted - version: "0.8.12+22" + 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: @@ -816,30 +886,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: @@ -852,10 +922,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: @@ -868,50 +938,50 @@ 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: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.8" + 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: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.0.0" logger: 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: @@ -920,14 +990,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: @@ -956,8 +1018,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" @@ -965,10 +1027,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: @@ -981,10 +1043,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: @@ -993,6 +1063,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: @@ -1029,18 +1107,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e url: "https://pub.dev" source: hosted - version: "2.2.16" + 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: @@ -1077,10 +1155,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: @@ -1101,26 +1179,26 @@ 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: name: posix - sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.0.3" provider: dependency: transitive description: name: provider - sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310" + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.1.5+1" pub_semver: dependency: transitive description: @@ -1148,10 +1226,11 @@ packages: ruby_text: dependency: "direct main" description: - name: ruby_text - sha256: c9fe49dfe703c240f2ef8274d75c33654ec893685fa7180fd0dcdaf32b850511 - url: "https://pub.dev" - source: hosted + path: "." + ref: "24c7a22" + resolved-ref: "24c7a220113efe750772c420befdce7e834e965c" + url: "https://github.com/Moseco/RubyText" + source: git version: "3.0.3" rxdart: dependency: transitive @@ -1165,8 +1244,8 @@ packages: dependency: "direct main" description: path: "." - ref: "4ea66ef" - resolved-ref: "4ea66efb580193c1c312efbeb828ffa44c33a999" + ref: "3df24c3" + resolved-ref: "3df24c3d43db9ff7adcc16d6b1a2d75bd242ba9e" url: "https://github.com/Moseco/sagase_dictionary" source: git version: "1.0.0" @@ -1198,42 +1277,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: c2c8c46297b5d6a80bed7741ec1f2759742c77d272f1a1698176ae828f8e1a18 + sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f url: "https://pub.dev" source: hosted - version: "2.4.9" + 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: @@ -1299,10 +1378,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: @@ -1311,30 +1390,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: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" + sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2" url: "https://pub.dev" source: hosted - version: "2.7.5" + version: "2.9.4" sqlite3_flutter_libs: dependency: transitive description: name: sqlite3_flutter_libs - sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" + sha256: "1e800ebe7f85a80a66adacaa6febe4d5f4d8b75f244e9838a27cb2ffc7aec08d" url: "https://pub.dev" source: hosted - version: "0.5.32" + version: "0.5.41" stack_trace: dependency: transitive description: @@ -1347,26 +1418,26 @@ 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: name: stacked_hooks - sha256: "7716851ee173988bd0808503fe96e2f1ac5410c5f71dc3c6648b7d4a9fb370c5" + sha256: ba2b0d777c69d4d38e5c08edbacc4dfe68eac94672447d067c2edc86fff9ed25 url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.2.4" stacked_services: dependency: "direct main" description: @@ -1427,18 +1498,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: @@ -1459,10 +1530,10 @@ packages: dependency: "direct main" description: name: tutorial_coach_mark - sha256: "9cdb721165d1cfb6e9b1910a1af1b3570fa6caa5059cf1506fcbd00bf7102abf" + sha256: "5a325d53bcf16ce7a969e2ab8d4dc9610f39ee3eab2b3cc57d5c98936129b891" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.3" typed_data: dependency: transitive description: @@ -1475,10 +1546,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: @@ -1492,42 +1563,42 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" url: "https://pub.dev" source: hosted - version: "6.3.15" + 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: @@ -1540,42 +1611,42 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f url: "https://pub.dev" source: hosted - version: "2.4.0" + 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: name: value_layout_builder - sha256: c02511ea91ca5c643b514a33a38fa52536f74aa939ec367d02938b5ede6807fa + sha256: ab4b7d98bac8cefeb9713154d43ee0477490183f5aa23bb4ffa5103d9bbf6275 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.5.0" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 url: "https://pub.dev" source: hosted - version: "1.1.18" + version: "1.1.19" vector_graphics_codec: dependency: transitive description: @@ -1588,34 +1659,34 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: "201e876b5d52753626af64b6359cd13ac6011b80728731428fd34bc840f71c9b" url: "https://pub.dev" source: hosted - version: "1.1.16" + 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: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.2" watcher: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.1" web: dependency: transitive description: @@ -1628,10 +1699,10 @@ packages: dependency: transitive description: name: web_socket - sha256: bfe6f435f6ec49cb6c01da1e275ae4228719e59a6b067048c51e72d9d63bcc4b + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" web_socket_channel: dependency: transitive description: @@ -1644,10 +1715,18 @@ packages: dependency: transitive description: name: win32 - sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e url: "https://pub.dev" source: hosted - version: "5.12.0" + 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: @@ -1660,10 +1739,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: @@ -1681,5 +1760,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.2 <4.0.0" - flutter: ">=3.29.3" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.9" diff --git a/pubspec.yaml b/pubspec.yaml index 4ed5c8b..2e28996 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,30 +3,36 @@ 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' - flutter: 3.29.3 + 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: '3df24c3' + # 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: 3.13.0 - firebase_analytics: 11.4.5 - firebase_crashlytics: 4.3.5 + firebase_core: 4.4.0 + firebase_analytics: 12.1.1 + firebase_crashlytics: 5.0.7 # UI google_nav_bar: ^5.0.6 @@ -35,11 +41,14 @@ dependencies: git: url: https://github.com/Moseco/flip_card keyboard_actions: ^4.2.0 - fl_chart: ^0.71.0 - flutter_svg: ^2.0.7 - flutter_sticky_header: ^0.7.0 + fl_chart: ^1.0.0 + flutter_svg: ^2.2.3 + 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 @@ -48,64 +57,67 @@ 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 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: url: https://github.com/Moseco/flutter_exif_rotation ref: '55b72de' + device_info_plus: ^12.3.0 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: sdk: flutter - flutter_lints: ^5.0.0 + 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/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/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'; diff --git a/test/ui/views/flashcards/flashcards_viewmodel_test.dart b/test/ui/views/flashcards/flashcards_viewmodel_test.dart index 4d9d23e..96c1742 100644 --- a/test/ui/views/flashcards/flashcards_viewmodel_test.dart +++ b/test/ui/views/flashcards/flashcards_viewmodel_test.dart @@ -1981,5 +1981,36 @@ 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( + 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); + }); }); }