diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index d62d376..3bdd500 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -241,12 +241,12 @@ jobs: prerelease: false generate_release_notes: true body: | - ## 🎉 TeleBook ${{ steps.version.outputs.VERSION }} + ## 🎉 tele_book ${{ steps.version.outputs.VERSION }} ### 📦 下载 - - **Android**: [TeleBook-android-${{ steps.version.outputs.VERSION }}.apk] - - **Windows**: [TeleBook-windows-${{ steps.version.outputs.VERSION }}.zip] - - **iOS**: [TeleBook-ios-${{ steps.version.outputs.VERSION }}.ipa] + - **Android**: [tele_book-android-${{ steps.version.outputs.VERSION }}.apk] + - **Windows**: [tele_book-windows-${{ steps.version.outputs.VERSION }}.zip] + - **iOS**: [tele_book-ios-${{ steps.version.outputs.VERSION }}.ipa] ### 📝 更新内容 详见下方自动生成的更新日志。 diff --git a/.gitignore b/.gitignore index 9b00b0e..9f8609c 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,4 @@ app.*.map.json keystore_base64.txt pubspec.lock - +*.sqlite diff --git a/.metadata b/.metadata index c5f4036..768732a 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6" + revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42" channel: "stable" project_type: app @@ -13,11 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 - base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + - platform: android + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + - platform: ios + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 - platform: linux - create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 - base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + - platform: macos + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + - platform: web + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + - platform: windows + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 # User provided section diff --git a/analysis_options.yaml b/analysis_options.yaml index 9403522..9a2e3cc 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -26,3 +26,7 @@ linter: # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options + +analyzer: + plugins: + - custom_lint \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore index 6f56801..be3943c 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -5,9 +5,10 @@ gradle-wrapper.jar /gradlew.bat /local.properties GeneratedPluginRegistrant.java +.cxx/ # Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +# See https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle deleted file mode 100644 index adacd45..0000000 --- a/android/app/build.gradle +++ /dev/null @@ -1,115 +0,0 @@ -plugins { - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" -} - -def localProperties = new java.util.Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def keystoreProperties = new java.util.Properties() -def keystorePropertiesFile = rootProject.file('key.properties') -if (keystorePropertiesFile.exists()) { - keystoreProperties.load(new java.io.FileInputStream(keystorePropertiesFile)) -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -def updatePubspecVersion() { - def versionPropsFile = file('../../version.properties') - if (versionPropsFile.canRead()) { - def versionProps = new java.util.Properties() - versionProps.load(new java.io.FileInputStream(versionPropsFile)) - - def versionName = versionProps['VERSION_NAME'] - def versionCode = versionProps['VERSION_CODE'] - - def pubspecFile = file('../../pubspec.yaml') - def pubspecContent = pubspecFile.text - def newContent = pubspecContent.replaceAll( - /version:\s*[\d.]+\+\d+/, - "version: ${versionName}+${versionCode}" - ) - pubspecFile.text = newContent - } -} - -preBuild.doFirst { - updatePubspecVersion() -} - -android { - namespace "com.dorkytiger.tele_book" - compileSdkVersion flutter.compileSdkVersion - ndkVersion "28.2.13676358" - - compileOptions { - sourceCompatibility JavaVersion.VERSION_21 - targetCompatibility JavaVersion.VERSION_21 - } - - kotlinOptions { - jvmTarget = '21' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.dorkytiger.tele_book" - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - signingConfigs { - release { - if (keystorePropertiesFile.exists()) { - keyAlias keystoreProperties['keyAlias'] - keyPassword keystoreProperties['keyPassword'] - storeFile file(keystoreProperties['storeFile']) - storePassword keystoreProperties['storePassword'] - } - } - } - - buildTypes { - release { - // Only assign the release signingConfig when a release keystore is present. - // This avoids Gradle trying to use an empty 'release' signingConfig when - // key.properties is not provided (which causes the missing storeFile error). - if (keystorePropertiesFile.exists()) { - signingConfig signingConfigs.release - } - } - debug { - // Use the default debug signing (do not force the release signingConfig). - // When key.properties is absent, this prevents the missing storeFile error - // during debug builds. - } - } -} - -flutter { - source '../..' -} - -dependencies {} diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..93d9c29 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.tele_book" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.tele_book" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2fa2f43..ed13ab7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + @@ -13,30 +12,18 @@ - - - - - - - - - - - - - - + + + + + + + diff --git a/android/app/src/main/kotlin/com/dorkytiger/tele_book/MainActivity.kt b/android/app/src/main/kotlin/com/dorkytiger/tele_book/MainActivity.kt deleted file mode 100644 index eb0eba8..0000000 --- a/android/app/src/main/kotlin/com/dorkytiger/tele_book/MainActivity.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.dorkytiger.tele_book - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.os.Build -import android.os.Bundle -import io.flutter.embedding.android.FlutterActivity - -class MainActivity: FlutterActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // 创建下载通知渠道 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - - // 创建下载通知渠道 - val downloadChannel = NotificationChannel( - "background_downloader", - "下载管理", - NotificationManager.IMPORTANCE_LOW - ).apply { - description = "显示文件下载进度和状态" - setShowBadge(false) - enableVibration(false) - } - notificationManager.createNotificationChannel(downloadChannel) - - // 创建前台服务通知渠道 - val foregroundChannel = NotificationChannel( - "background_downloader_foreground", - "后台下载服务", - NotificationManager.IMPORTANCE_LOW - ).apply { - description = "保持后台下载服务运行" - setShowBadge(false) - enableVibration(false) - } - notificationManager.createNotificationChannel(foregroundChannel) - } - } -} diff --git a/android/app/src/main/kotlin/com/example/tele_book/MainActivity.kt b/android/app/src/main/kotlin/com/example/tele_book/MainActivity.kt new file mode 100644 index 0000000..06da5b9 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/tele_book/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.tele_book + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 0e22fd1..db77bb4 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 0e22fd1..17987b7 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 0e22fd1..09d4391 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 0e22fd1..d5f1c8d 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 0e22fd1..4d6372e 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml index 04f2faf..399f698 100644 --- a/android/app/src/profile/AndroidManifest.xml +++ b/android/app/src/profile/AndroidManifest.xml @@ -3,10 +3,5 @@ the Flutter tool needs it to communicate with the running application to allow setting breakpoints, to provide hot reload, etc. --> - - - - - diff --git a/android/build.gradle b/android/build.gradle deleted file mode 100644 index 930a713..0000000 --- a/android/build.gradle +++ /dev/null @@ -1,22 +0,0 @@ -buildscript { - ext.kotlin_version = '2.3.0' - repositories { - google() - mavenCentral() - - } - -} - - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html deleted file mode 100644 index 6589a90..0000000 --- a/android/build/reports/problems/problems-report.html +++ /dev/null @@ -1,659 +0,0 @@ - - - - - - - - - - - - - Gradle Configuration Cache - - - -
- -
- Loading... -
- - - - - - diff --git a/android/gradle.properties b/android/gradle.properties index 598d13f..e96108c 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,6 @@ -org.gradle.jvmargs=-Xmx4G +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -android.enableJetifier=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 6388f31..2d428bf 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-9.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/android/key.properties.example b/android/key.properties.example deleted file mode 100644 index c9d3d0d..0000000 --- a/android/key.properties.example +++ /dev/null @@ -1,4 +0,0 @@ -storePassword=1234abcD -keyPassword=1234abcD -keyAlias=dorkytiger -storeFile=upload-keystore.jks \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index bd05419..0000000 --- a/android/settings.gradle +++ /dev/null @@ -1,30 +0,0 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - } - settings.ext.flutterSdkPath = flutterSdkPath() - - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } - - plugins { - id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false - } -} - -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id("com.android.application") version "8.9.1" apply false - id "org.jetbrains.kotlin.android" version "2.3.0" apply false -} - -include ":app" diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..c21f0c5 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 70d90c4..bc7cb92 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 7A07A4ADD59CDFBF626C66F9 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2313121B4CEE6A8FDF62CC94 /* Pods_RunnerTests.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -52,6 +53,7 @@ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7761669B9BB3378222FA1675 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7A02C1B51900805359BB2217 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -72,6 +74,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, B6B4894B7BD412271BA4F633 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -98,6 +101,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -198,13 +202,15 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 0FAAAE3FD367FFA7A864DA02 /* [CP] Embed Pods Frameworks */, - B0CA94AB8D604043F6F08669 /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -238,6 +244,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -318,23 +327,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - B0CA94AB8D604043F6F08669 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; CEDD2AE7CF389940B93D98E0 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -758,6 +750,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d4..c3fedb2 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - TeleBook + tele_book CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - TeleBook + tele_book CFBundlePackageType APPL CFBundleShortVersionString diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/lib/common/widget/empty_widget.dart b/lib/common/widget/empty_widget.dart new file mode 100644 index 0000000..28a393b --- /dev/null +++ b/lib/common/widget/empty_widget.dart @@ -0,0 +1,29 @@ +import 'package:flutter/cupertino.dart'; + +class CustomEmptyWidget extends StatelessWidget { + final IconData? icon; + final String? text; + + const CustomEmptyWidget({super.key, this.icon, this.text}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 40), + Icon( + icon ?? CupertinoIcons.infinite, + size: 60, + color: CupertinoColors.systemGrey, + ), + SizedBox(height: 20), + Text( + text ?? "暂无数据", + style: TextStyle(fontSize: 16, color: CupertinoColors.systemGrey), + ), + ], + ); + } +} diff --git a/lib/common/widget/error_widget.dart b/lib/common/widget/error_widget.dart new file mode 100644 index 0000000..4be84d9 --- /dev/null +++ b/lib/common/widget/error_widget.dart @@ -0,0 +1,76 @@ +import 'dart:math'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; + +class CustomErrorWidget extends StatelessWidget { + final String? errorMessage; + final StackTrace? stackTrace; + final VoidCallback? onRetry; + + const CustomErrorWidget({ + Key? key, + this.errorMessage, + this.stackTrace, + this.onRetry, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: .start, + + children: [ + Text('发生错误',style: context.theme.typography.body.xl2.copyWith( + fontWeight: .w600, + color: context.theme.colors.error, + height: 1.5 + ),), + SizedBox(height: 8), + Text('$errorMessage',style: context.theme.typography.body.sm.copyWith( + color: context.theme.colors.destructive + ),), + SizedBox(height: 8), + if (stackTrace != null) + FButton( + onPress: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('错误详情'), + content: SingleChildScrollView( + child: Text(stackTrace.toString()), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('关闭'), + ), + ], + ), + ); + }, + child: Text("查看详情"), + ), + ], + ), + ), + ), + + SizedBox(height: 8), + if (onRetry != null) + ElevatedButton(onPressed: onRetry, child: Text('重试')), + ], + ), + ); + } +} diff --git a/lib/common/widget/f_adaptive_dialog.dart b/lib/common/widget/f_adaptive_dialog.dart new file mode 100644 index 0000000..d2eb276 --- /dev/null +++ b/lib/common/widget/f_adaptive_dialog.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; + +class FAdaptiveDialog extends StatelessWidget { + final FDialogStyleDelta style; + final Animation? animation; + final Widget title; + final Widget body; + final List actions; + const FAdaptiveDialog({ + required this.title, + required this.body, + required this.actions, + this.style = const .context(), + this.animation, + super.key, + }); + @override + Widget build(BuildContext context) => FDialog.adaptive( + style: style, + animation: animation, + horizontalBuilder: (context, style) { + final touch = context.platformVariant.touch; + return Padding( + padding: touch + ? const .symmetric(horizontal: 16, vertical: 18) + : const .symmetric(horizontal: 16, vertical: 14), + child: Column( + crossAxisAlignment: .start, + mainAxisSize: .min, + children: [ + Padding( + padding: touch + ? const .only(left: 8, right: 8, bottom: 9) + : const .only(bottom: 5), + child: DefaultTextStyle.merge( + style: style.titleTextStyle, + child: title, + ), + ), + Flexible( + child: Padding( + padding: touch + ? const .only(left: 8, right: 8, bottom: 20) + : const .only(bottom: 16), + child: DefaultTextStyle.merge( + style: style.bodyTextStyle, + child: body, + ), + ), + ), + Row( + mainAxisAlignment: .end, + spacing: touch ? 10 : 8, + children: touch + ? [for (final action in actions) Expanded(child: action)] + : actions, + ), + ], + ), + ); + }, + verticalBuilder: (context, style) { + final touch = context.platformVariant.touch; + return Padding( + padding: touch + ? const .symmetric(horizontal: 16, vertical: 18) + : const .symmetric(horizontal: 16, vertical: 14), + child: Column( + crossAxisAlignment: .start, + mainAxisSize: .min, + children: [ + Padding( + padding: touch + ? const .only(left: 8, right: 8, bottom: 9) + : const .only(left: 8, right: 8, bottom: 5), + child: DefaultTextStyle.merge( + style: style.titleTextStyle, + child: title, + ), + ), + Flexible( + child: Padding( + padding: touch + ? const .only(left: 8, right: 8, bottom: 20) + : const .only(left: 8, right: 8, bottom: 16), + child: DefaultTextStyle.merge( + style: style.bodyTextStyle, + child: body, + ), + ), + ), + Column( + mainAxisSize: .min, + spacing: touch ? 10 : 8, + children: actions, + ), + ], + ), + ); + }, + ); +} \ No newline at end of file diff --git a/lib/common/widget/f_sheet_content.dart b/lib/common/widget/f_sheet_content.dart new file mode 100644 index 0000000..a980241 --- /dev/null +++ b/lib/common/widget/f_sheet_content.dart @@ -0,0 +1,67 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; + +class FSheetContent extends StatelessWidget { + final FLayout side; + final Widget child; + + const FSheetContent({super.key, required this.side, required this.child}); + + @override + Widget build(BuildContext context) { + return Container( + height: .infinity, + width: .infinity, + decoration: BoxDecoration( + color: context.theme.colors.background, + border: side.vertical + ? .symmetric( + horizontal: BorderSide(color: context.theme.colors.border), + ) + : .symmetric( + vertical: BorderSide(color: context.theme.colors.border), + ), + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const .symmetric(horizontal: 15, vertical: 8.0), + child: child, + ), + ); + } + + static Widget drag() { + return Center( + child: Container( + margin: const EdgeInsets.only(top: 8, bottom: 4), + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(2), + ), + ), + ); + } + + static Widget title(BuildContext context, String title) { + return Text( + title, + style: context.theme.typography.display.xl2.copyWith( + fontWeight: .w600, + color: context.theme.colors.foreground, + height: 1.5, + ), + ); + } + + static Widget subTitle(BuildContext context, String subTitle) { + return Text( + subTitle, + style: context.theme.typography.body.sm.copyWith( + color: context.theme.colors.mutedForeground, + ), + ); + } +} diff --git a/lib/common/widget/f_text.dart b/lib/common/widget/f_text.dart new file mode 100644 index 0000000..a984836 --- /dev/null +++ b/lib/common/widget/f_text.dart @@ -0,0 +1,24 @@ +import 'package:flutter/cupertino.dart'; +import 'package:forui/forui.dart'; + +class FText { +static Widget title(BuildContext context,String title){ + return Text( + title, + style: context.theme.typography.display.xl2.copyWith( + fontWeight: .w600, + color: context.theme.colors.foreground, + height: 1.5, + ), + ); + } + + static Widget subTitle(BuildContext context, String subTitle) { + return Text( + subTitle, + style: context.theme.typography.body.sm.copyWith( + color: context.theme.colors.mutedForeground, + ), + ); + } +} \ No newline at end of file diff --git a/lib/common/widget/task_item_widget.dart b/lib/common/widget/task_item_widget.dart index b56ed29..c3430bc 100644 --- a/lib/common/widget/task_item_widget.dart +++ b/lib/common/widget/task_item_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; import 'package:tele_book/common/widget/network_image_widget.dart'; class TaskItemWidget extends StatelessWidget { @@ -21,35 +22,12 @@ class TaskItemWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Row( - children: [ - NetworkImageWidget(imageUrl: coverUrl), - Expanded( - child: ListTile( - title: Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyLarge, - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "状态: $status", - style: Theme.of(context).textTheme.bodyMedium, - ), - SizedBox(height: 8), - LinearProgressIndicator(value: progress,borderRadius: BorderRadius.circular(4),), - ], - ), - trailing: trailing, - ), - ), - ], - ), + return FItem( + onPress: onTap, + prefix: NetworkImageWidget(imageUrl: coverUrl), + title: Text(title, maxLines: 2), + subtitle: Text("$status ${(progress * 100).toStringAsFixed(1)}%"), + suffix: trailing, ); } } diff --git a/lib/core/db/app_database.dart b/lib/core/db/app_database.dart index 857c0a2..f01bd61 100644 --- a/lib/core/db/app_database.dart +++ b/lib/core/db/app_database.dart @@ -1,25 +1,71 @@ +import 'dart:io'; + import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; -import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; - +import 'package:riverpod/riverpod.dart'; import 'package:tele_book/feature/book/datasource/local/book_local_datasource.dart'; import 'package:tele_book/feature/book/model/table/book_table.dart'; +import 'package:tele_book/feature/collection/datasource/local/collection_book_local_datasource.dart'; +import 'package:tele_book/feature/collection/datasource/local/collection_local_datasource.dart'; +import 'package:tele_book/feature/collection/model/table/collection_book_table.dart'; +import 'package:tele_book/feature/collection/model/table/collection_table.dart'; import 'converter/string_list_converter.dart'; part 'app_database.g.dart'; -@DriftDatabase(tables: [BookTable], daos: [BookLocalDatasource]) +@DriftDatabase( + tables: [BookTable, CollectionTable, CollectionBookTable], + daos: [ + BookLocalDatasource, + CollectionLocalDatasource, + CollectionBookLocalDatasource, + ], +) class AppDatabase extends _$AppDatabase { // Allow injecting a QueryExecutor for tests. If null, use the default on-disk executor. AppDatabase([QueryExecutor? executor]) : super((executor ?? _openConnection())); @override - int get schemaVersion => 1; + int get schemaVersion => 2; + + @override + MigrationStrategy get migration => MigrationStrategy( + onUpgrade: (migrator, from, to) async { + // 破坏性更新:删除所有表并重建 + await customStatement('PRAGMA foreign_keys = OFF'); + final tableNames = allTables.map((t) => t.actualTableName).toList(); + for (final name in tableNames) { + await customStatement('DROP TABLE IF EXISTS "$name"'); + } + await migrator.createAll(); + await customStatement('PRAGMA foreign_keys = ON'); + }, + ); static QueryExecutor _openConnection() { - return driftDatabase(name: 'tele_book'); + return driftDatabase( + name: 'tele_book', + native: DriftNativeOptions( + databaseDirectory: () async { + if (Platform.isIOS || Platform.isAndroid) { + final dbFolder = await getApplicationDocumentsDirectory(); + print('Database path: ${dbFolder.path}'); + return dbFolder.path; + } else { + // For desktop platforms, use the current directory + return Directory.current.path; + } + }, + ), + ); } } + +final databaseProvider = Provider((ref) { + final db = AppDatabase(); + ref.onDispose(() => db.close()); + return db; +}); diff --git a/lib/core/db/app_database.g.dart b/lib/core/db/app_database.g.dart index f08d276..2539a3c 100644 --- a/lib/core/db/app_database.g.dart +++ b/lib/core/db/app_database.g.dart @@ -36,6 +36,26 @@ class $BookTableTable extends BookTable type: DriftSqlType.string, requiredDuringInsert: true, ).withConverter>($BookTableTable.$converterlocalSubPaths); + static const VerificationMeta _coverSubPathMeta = const VerificationMeta( + 'coverSubPath', + ); + @override + late final GeneratedColumn coverSubPath = GeneratedColumn( + 'cover_sub_path', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + late final GeneratedColumnWithTypeConverter?, String> + previewSubPaths = GeneratedColumn( + 'preview_sub_paths', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($BookTableTable.$converterpreviewSubPathsn); static const VerificationMeta _readCountMeta = const VerificationMeta( 'readCount', ); @@ -77,6 +97,8 @@ class $BookTableTable extends BookTable id, name, localSubPaths, + coverSubPath, + previewSubPaths, readCount, currentPage, createdAt, @@ -104,6 +126,15 @@ class $BookTableTable extends BookTable } else if (isInserting) { context.missing(_nameMeta); } + if (data.containsKey('cover_sub_path')) { + context.handle( + _coverSubPathMeta, + coverSubPath.isAcceptableOrUnknown( + data['cover_sub_path']!, + _coverSubPathMeta, + ), + ); + } if (data.containsKey('read_count')) { context.handle( _readCountMeta, @@ -148,6 +179,16 @@ class $BookTableTable extends BookTable data['${effectivePrefix}local_sub_paths'], )!, ), + coverSubPath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cover_sub_path'], + ), + previewSubPaths: $BookTableTable.$converterpreviewSubPathsn.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}preview_sub_paths'], + ), + ), readCount: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}read_count'], @@ -170,12 +211,20 @@ class $BookTableTable extends BookTable static JsonTypeConverter2, String, List> $converterlocalSubPaths = const StringListConverter(); + static JsonTypeConverter2, String, List> + $converterpreviewSubPaths = const StringListConverter(); + static JsonTypeConverter2?, String?, List?> + $converterpreviewSubPathsn = JsonTypeConverter2.asNullable( + $converterpreviewSubPaths, + ); } class BookTableData extends DataClass implements Insertable { final int id; final String name; final List localSubPaths; + final String? coverSubPath; + final List? previewSubPaths; final int readCount; final int currentPage; final DateTime createdAt; @@ -183,6 +232,8 @@ class BookTableData extends DataClass implements Insertable { required this.id, required this.name, required this.localSubPaths, + this.coverSubPath, + this.previewSubPaths, required this.readCount, required this.currentPage, required this.createdAt, @@ -197,6 +248,14 @@ class BookTableData extends DataClass implements Insertable { $BookTableTable.$converterlocalSubPaths.toSql(localSubPaths), ); } + if (!nullToAbsent || coverSubPath != null) { + map['cover_sub_path'] = Variable(coverSubPath); + } + if (!nullToAbsent || previewSubPaths != null) { + map['preview_sub_paths'] = Variable( + $BookTableTable.$converterpreviewSubPathsn.toSql(previewSubPaths), + ); + } map['read_count'] = Variable(readCount); map['current_page'] = Variable(currentPage); map['created_at'] = Variable(createdAt); @@ -208,6 +267,12 @@ class BookTableData extends DataClass implements Insertable { id: Value(id), name: Value(name), localSubPaths: Value(localSubPaths), + coverSubPath: coverSubPath == null && nullToAbsent + ? const Value.absent() + : Value(coverSubPath), + previewSubPaths: previewSubPaths == null && nullToAbsent + ? const Value.absent() + : Value(previewSubPaths), readCount: Value(readCount), currentPage: Value(currentPage), createdAt: Value(createdAt), @@ -225,6 +290,10 @@ class BookTableData extends DataClass implements Insertable { localSubPaths: $BookTableTable.$converterlocalSubPaths.fromJson( serializer.fromJson>(json['localSubPaths']), ), + coverSubPath: serializer.fromJson(json['coverSubPath']), + previewSubPaths: $BookTableTable.$converterpreviewSubPathsn.fromJson( + serializer.fromJson?>(json['previewSubPaths']), + ), readCount: serializer.fromJson(json['readCount']), currentPage: serializer.fromJson(json['currentPage']), createdAt: serializer.fromJson(json['createdAt']), @@ -239,6 +308,10 @@ class BookTableData extends DataClass implements Insertable { 'localSubPaths': serializer.toJson>( $BookTableTable.$converterlocalSubPaths.toJson(localSubPaths), ), + 'coverSubPath': serializer.toJson(coverSubPath), + 'previewSubPaths': serializer.toJson?>( + $BookTableTable.$converterpreviewSubPathsn.toJson(previewSubPaths), + ), 'readCount': serializer.toJson(readCount), 'currentPage': serializer.toJson(currentPage), 'createdAt': serializer.toJson(createdAt), @@ -249,6 +322,8 @@ class BookTableData extends DataClass implements Insertable { int? id, String? name, List? localSubPaths, + Value coverSubPath = const Value.absent(), + Value?> previewSubPaths = const Value.absent(), int? readCount, int? currentPage, DateTime? createdAt, @@ -256,6 +331,10 @@ class BookTableData extends DataClass implements Insertable { id: id ?? this.id, name: name ?? this.name, localSubPaths: localSubPaths ?? this.localSubPaths, + coverSubPath: coverSubPath.present ? coverSubPath.value : this.coverSubPath, + previewSubPaths: previewSubPaths.present + ? previewSubPaths.value + : this.previewSubPaths, readCount: readCount ?? this.readCount, currentPage: currentPage ?? this.currentPage, createdAt: createdAt ?? this.createdAt, @@ -267,6 +346,12 @@ class BookTableData extends DataClass implements Insertable { localSubPaths: data.localSubPaths.present ? data.localSubPaths.value : this.localSubPaths, + coverSubPath: data.coverSubPath.present + ? data.coverSubPath.value + : this.coverSubPath, + previewSubPaths: data.previewSubPaths.present + ? data.previewSubPaths.value + : this.previewSubPaths, readCount: data.readCount.present ? data.readCount.value : this.readCount, currentPage: data.currentPage.present ? data.currentPage.value @@ -281,6 +366,8 @@ class BookTableData extends DataClass implements Insertable { ..write('id: $id, ') ..write('name: $name, ') ..write('localSubPaths: $localSubPaths, ') + ..write('coverSubPath: $coverSubPath, ') + ..write('previewSubPaths: $previewSubPaths, ') ..write('readCount: $readCount, ') ..write('currentPage: $currentPage, ') ..write('createdAt: $createdAt') @@ -289,8 +376,16 @@ class BookTableData extends DataClass implements Insertable { } @override - int get hashCode => - Object.hash(id, name, localSubPaths, readCount, currentPage, createdAt); + int get hashCode => Object.hash( + id, + name, + localSubPaths, + coverSubPath, + previewSubPaths, + readCount, + currentPage, + createdAt, + ); @override bool operator ==(Object other) => identical(this, other) || @@ -298,6 +393,8 @@ class BookTableData extends DataClass implements Insertable { other.id == this.id && other.name == this.name && other.localSubPaths == this.localSubPaths && + other.coverSubPath == this.coverSubPath && + other.previewSubPaths == this.previewSubPaths && other.readCount == this.readCount && other.currentPage == this.currentPage && other.createdAt == this.createdAt); @@ -307,6 +404,8 @@ class BookTableCompanion extends UpdateCompanion { final Value id; final Value name; final Value> localSubPaths; + final Value coverSubPath; + final Value?> previewSubPaths; final Value readCount; final Value currentPage; final Value createdAt; @@ -314,6 +413,8 @@ class BookTableCompanion extends UpdateCompanion { this.id = const Value.absent(), this.name = const Value.absent(), this.localSubPaths = const Value.absent(), + this.coverSubPath = const Value.absent(), + this.previewSubPaths = const Value.absent(), this.readCount = const Value.absent(), this.currentPage = const Value.absent(), this.createdAt = const Value.absent(), @@ -322,6 +423,8 @@ class BookTableCompanion extends UpdateCompanion { this.id = const Value.absent(), required String name, required List localSubPaths, + this.coverSubPath = const Value.absent(), + this.previewSubPaths = const Value.absent(), this.readCount = const Value.absent(), this.currentPage = const Value.absent(), this.createdAt = const Value.absent(), @@ -331,6 +434,8 @@ class BookTableCompanion extends UpdateCompanion { Expression? id, Expression? name, Expression? localSubPaths, + Expression? coverSubPath, + Expression? previewSubPaths, Expression? readCount, Expression? currentPage, Expression? createdAt, @@ -339,6 +444,8 @@ class BookTableCompanion extends UpdateCompanion { if (id != null) 'id': id, if (name != null) 'name': name, if (localSubPaths != null) 'local_sub_paths': localSubPaths, + if (coverSubPath != null) 'cover_sub_path': coverSubPath, + if (previewSubPaths != null) 'preview_sub_paths': previewSubPaths, if (readCount != null) 'read_count': readCount, if (currentPage != null) 'current_page': currentPage, if (createdAt != null) 'created_at': createdAt, @@ -349,6 +456,8 @@ class BookTableCompanion extends UpdateCompanion { Value? id, Value? name, Value>? localSubPaths, + Value? coverSubPath, + Value?>? previewSubPaths, Value? readCount, Value? currentPage, Value? createdAt, @@ -357,6 +466,8 @@ class BookTableCompanion extends UpdateCompanion { id: id ?? this.id, name: name ?? this.name, localSubPaths: localSubPaths ?? this.localSubPaths, + coverSubPath: coverSubPath ?? this.coverSubPath, + previewSubPaths: previewSubPaths ?? this.previewSubPaths, readCount: readCount ?? this.readCount, currentPage: currentPage ?? this.currentPage, createdAt: createdAt ?? this.createdAt, @@ -377,6 +488,14 @@ class BookTableCompanion extends UpdateCompanion { $BookTableTable.$converterlocalSubPaths.toSql(localSubPaths.value), ); } + if (coverSubPath.present) { + map['cover_sub_path'] = Variable(coverSubPath.value); + } + if (previewSubPaths.present) { + map['preview_sub_paths'] = Variable( + $BookTableTable.$converterpreviewSubPathsn.toSql(previewSubPaths.value), + ); + } if (readCount.present) { map['read_count'] = Variable(readCount.value); } @@ -395,6 +514,8 @@ class BookTableCompanion extends UpdateCompanion { ..write('id: $id, ') ..write('name: $name, ') ..write('localSubPaths: $localSubPaths, ') + ..write('coverSubPath: $coverSubPath, ') + ..write('previewSubPaths: $previewSubPaths, ') ..write('readCount: $readCount, ') ..write('currentPage: $currentPage, ') ..write('createdAt: $createdAt') @@ -403,113 +524,731 @@ class BookTableCompanion extends UpdateCompanion { } } -abstract class _$AppDatabase extends GeneratedDatabase { - _$AppDatabase(QueryExecutor e) : super(e); - $AppDatabaseManager get managers => $AppDatabaseManager(this); - late final $BookTableTable bookTable = $BookTableTable(this); - late final BookLocalDatasource bookLocalDatasource = BookLocalDatasource( - this as AppDatabase, - ); +class $CollectionTableTable extends CollectionTable + with TableInfo<$CollectionTableTable, CollectionTableData> { @override - Iterable> get allTables => - allSchemaEntities.whereType>(); + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CollectionTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); @override - List get allSchemaEntities => [bookTable]; -} - -typedef $$BookTableTableCreateCompanionBuilder = - BookTableCompanion Function({ - Value id, - required String name, - required List localSubPaths, - Value readCount, - Value currentPage, - Value createdAt, - }); -typedef $$BookTableTableUpdateCompanionBuilder = - BookTableCompanion Function({ - Value id, - Value name, - Value> localSubPaths, - Value readCount, - Value currentPage, - Value createdAt, - }); - -class $$BookTableTableFilterComposer - extends Composer<_$AppDatabase, $BookTableTable> { - $$BookTableTableFilterComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnFilters get id => $composableBuilder( - column: $table.id, - builder: (column) => ColumnFilters(column), + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), ); - - ColumnFilters get name => $composableBuilder( - column: $table.name, - builder: (column) => ColumnFilters(column), + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, ); - - ColumnWithTypeConverterFilters, List, String> - get localSubPaths => $composableBuilder( - column: $table.localSubPaths, - builder: (column) => ColumnWithTypeConverterFilters(column), + static const VerificationMeta _descriptionMeta = const VerificationMeta( + 'description', ); - - ColumnFilters get readCount => $composableBuilder( - column: $table.readCount, - builder: (column) => ColumnFilters(column), + @override + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, ); - - ColumnFilters get currentPage => $composableBuilder( - column: $table.currentPage, - builder: (column) => ColumnFilters(column), + static const VerificationMeta _coverImageSubPathMeta = const VerificationMeta( + 'coverImageSubPath', ); + @override + late final GeneratedColumn coverImageSubPath = + GeneratedColumn( + 'cover_image_sub_path', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + description, + coverImageSubPath, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'collection_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, + _descriptionMeta, + ), + ); + } + if (data.containsKey('cover_image_sub_path')) { + context.handle( + _coverImageSubPathMeta, + coverImageSubPath.isAcceptableOrUnknown( + data['cover_image_sub_path']!, + _coverImageSubPathMeta, + ), + ); + } + return context; + } - ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, - builder: (column) => ColumnFilters(column), - ); + @override + Set get $primaryKey => {id}; + @override + CollectionTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CollectionTableData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + coverImageSubPath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cover_image_sub_path'], + ), + ); + } + + @override + $CollectionTableTable createAlias(String alias) { + return $CollectionTableTable(attachedDatabase, alias); + } } -class $$BookTableTableOrderingComposer - extends Composer<_$AppDatabase, $BookTableTable> { - $$BookTableTableOrderingComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, +class CollectionTableData extends DataClass + implements Insertable { + final int id; + final String name; + final String? description; + final String? coverImageSubPath; + const CollectionTableData({ + required this.id, + required this.name, + this.description, + this.coverImageSubPath, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, - builder: (column) => ColumnOrderings(column), - ); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || coverImageSubPath != null) { + map['cover_image_sub_path'] = Variable(coverImageSubPath); + } + return map; + } - ColumnOrderings get name => $composableBuilder( - column: $table.name, - builder: (column) => ColumnOrderings(column), - ); + CollectionTableCompanion toCompanion(bool nullToAbsent) { + return CollectionTableCompanion( + id: Value(id), + name: Value(name), + description: description == null && nullToAbsent + ? const Value.absent() + : Value(description), + coverImageSubPath: coverImageSubPath == null && nullToAbsent + ? const Value.absent() + : Value(coverImageSubPath), + ); + } - ColumnOrderings get localSubPaths => $composableBuilder( - column: $table.localSubPaths, - builder: (column) => ColumnOrderings(column), - ); + factory CollectionTableData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CollectionTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + coverImageSubPath: serializer.fromJson( + json['coverImageSubPath'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'coverImageSubPath': serializer.toJson(coverImageSubPath), + }; + } - ColumnOrderings get readCount => $composableBuilder( - column: $table.readCount, - builder: (column) => ColumnOrderings(column), + CollectionTableData copyWith({ + int? id, + String? name, + Value description = const Value.absent(), + Value coverImageSubPath = const Value.absent(), + }) => CollectionTableData( + id: id ?? this.id, + name: name ?? this.name, + description: description.present ? description.value : this.description, + coverImageSubPath: coverImageSubPath.present + ? coverImageSubPath.value + : this.coverImageSubPath, ); + CollectionTableData copyWithCompanion(CollectionTableCompanion data) { + return CollectionTableData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + coverImageSubPath: data.coverImageSubPath.present + ? data.coverImageSubPath.value + : this.coverImageSubPath, + ); + } - ColumnOrderings get currentPage => $composableBuilder( - column: $table.currentPage, - builder: (column) => ColumnOrderings(column), - ); + @override + String toString() { + return (StringBuffer('CollectionTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('coverImageSubPath: $coverImageSubPath') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, description, coverImageSubPath); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CollectionTableData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.coverImageSubPath == this.coverImageSubPath); +} + +class CollectionTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value coverImageSubPath; + const CollectionTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.coverImageSubPath = const Value.absent(), + }); + CollectionTableCompanion.insert({ + this.id = const Value.absent(), + required String name, + this.description = const Value.absent(), + this.coverImageSubPath = const Value.absent(), + }) : name = Value(name); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? coverImageSubPath, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (coverImageSubPath != null) 'cover_image_sub_path': coverImageSubPath, + }); + } + + CollectionTableCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? coverImageSubPath, + }) { + return CollectionTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + coverImageSubPath: coverImageSubPath ?? this.coverImageSubPath, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (coverImageSubPath.present) { + map['cover_image_sub_path'] = Variable(coverImageSubPath.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CollectionTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('coverImageSubPath: $coverImageSubPath') + ..write(')')) + .toString(); + } +} + +class $CollectionBookTableTable extends CollectionBookTable + with TableInfo<$CollectionBookTableTable, CollectionBookTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CollectionBookTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + static const VerificationMeta _collectionIdMeta = const VerificationMeta( + 'collectionId', + ); + @override + late final GeneratedColumn collectionId = GeneratedColumn( + 'collection_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _bookIdMeta = const VerificationMeta('bookId'); + @override + late final GeneratedColumn bookId = GeneratedColumn( + 'book_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [id, collectionId, bookId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'collection_book_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('collection_id')) { + context.handle( + _collectionIdMeta, + collectionId.isAcceptableOrUnknown( + data['collection_id']!, + _collectionIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_collectionIdMeta); + } + if (data.containsKey('book_id')) { + context.handle( + _bookIdMeta, + bookId.isAcceptableOrUnknown(data['book_id']!, _bookIdMeta), + ); + } else if (isInserting) { + context.missing(_bookIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + CollectionBookTableData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CollectionBookTableData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + collectionId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}collection_id'], + )!, + bookId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}book_id'], + )!, + ); + } + + @override + $CollectionBookTableTable createAlias(String alias) { + return $CollectionBookTableTable(attachedDatabase, alias); + } +} + +class CollectionBookTableData extends DataClass + implements Insertable { + final int id; + final int collectionId; + final int bookId; + const CollectionBookTableData({ + required this.id, + required this.collectionId, + required this.bookId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['collection_id'] = Variable(collectionId); + map['book_id'] = Variable(bookId); + return map; + } + + CollectionBookTableCompanion toCompanion(bool nullToAbsent) { + return CollectionBookTableCompanion( + id: Value(id), + collectionId: Value(collectionId), + bookId: Value(bookId), + ); + } + + factory CollectionBookTableData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CollectionBookTableData( + id: serializer.fromJson(json['id']), + collectionId: serializer.fromJson(json['collectionId']), + bookId: serializer.fromJson(json['bookId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'collectionId': serializer.toJson(collectionId), + 'bookId': serializer.toJson(bookId), + }; + } + + CollectionBookTableData copyWith({int? id, int? collectionId, int? bookId}) => + CollectionBookTableData( + id: id ?? this.id, + collectionId: collectionId ?? this.collectionId, + bookId: bookId ?? this.bookId, + ); + CollectionBookTableData copyWithCompanion(CollectionBookTableCompanion data) { + return CollectionBookTableData( + id: data.id.present ? data.id.value : this.id, + collectionId: data.collectionId.present + ? data.collectionId.value + : this.collectionId, + bookId: data.bookId.present ? data.bookId.value : this.bookId, + ); + } + + @override + String toString() { + return (StringBuffer('CollectionBookTableData(') + ..write('id: $id, ') + ..write('collectionId: $collectionId, ') + ..write('bookId: $bookId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, collectionId, bookId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CollectionBookTableData && + other.id == this.id && + other.collectionId == this.collectionId && + other.bookId == this.bookId); +} + +class CollectionBookTableCompanion + extends UpdateCompanion { + final Value id; + final Value collectionId; + final Value bookId; + const CollectionBookTableCompanion({ + this.id = const Value.absent(), + this.collectionId = const Value.absent(), + this.bookId = const Value.absent(), + }); + CollectionBookTableCompanion.insert({ + this.id = const Value.absent(), + required int collectionId, + required int bookId, + }) : collectionId = Value(collectionId), + bookId = Value(bookId); + static Insertable custom({ + Expression? id, + Expression? collectionId, + Expression? bookId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (collectionId != null) 'collection_id': collectionId, + if (bookId != null) 'book_id': bookId, + }); + } + + CollectionBookTableCompanion copyWith({ + Value? id, + Value? collectionId, + Value? bookId, + }) { + return CollectionBookTableCompanion( + id: id ?? this.id, + collectionId: collectionId ?? this.collectionId, + bookId: bookId ?? this.bookId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (collectionId.present) { + map['collection_id'] = Variable(collectionId.value); + } + if (bookId.present) { + map['book_id'] = Variable(bookId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CollectionBookTableCompanion(') + ..write('id: $id, ') + ..write('collectionId: $collectionId, ') + ..write('bookId: $bookId') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + $AppDatabaseManager get managers => $AppDatabaseManager(this); + late final $BookTableTable bookTable = $BookTableTable(this); + late final $CollectionTableTable collectionTable = $CollectionTableTable( + this, + ); + late final $CollectionBookTableTable collectionBookTable = + $CollectionBookTableTable(this); + late final BookLocalDatasource bookLocalDatasource = BookLocalDatasource( + this as AppDatabase, + ); + late final CollectionLocalDatasource collectionLocalDatasource = + CollectionLocalDatasource(this as AppDatabase); + late final CollectionBookLocalDatasource collectionBookLocalDatasource = + CollectionBookLocalDatasource(this as AppDatabase); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + bookTable, + collectionTable, + collectionBookTable, + ]; +} + +typedef $$BookTableTableCreateCompanionBuilder = + BookTableCompanion Function({ + Value id, + required String name, + required List localSubPaths, + Value coverSubPath, + Value?> previewSubPaths, + Value readCount, + Value currentPage, + Value createdAt, + }); +typedef $$BookTableTableUpdateCompanionBuilder = + BookTableCompanion Function({ + Value id, + Value name, + Value> localSubPaths, + Value coverSubPath, + Value?> previewSubPaths, + Value readCount, + Value currentPage, + Value createdAt, + }); + +class $$BookTableTableFilterComposer + extends Composer<_$AppDatabase, $BookTableTable> { + $$BookTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnWithTypeConverterFilters, List, String> + get localSubPaths => $composableBuilder( + column: $table.localSubPaths, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnFilters get coverSubPath => $composableBuilder( + column: $table.coverSubPath, + builder: (column) => ColumnFilters(column), + ); + + ColumnWithTypeConverterFilters?, List, String> + get previewSubPaths => $composableBuilder( + column: $table.previewSubPaths, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnFilters get readCount => $composableBuilder( + column: $table.readCount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get currentPage => $composableBuilder( + column: $table.currentPage, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); +} + +class $$BookTableTableOrderingComposer + extends Composer<_$AppDatabase, $BookTableTable> { + $$BookTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get localSubPaths => $composableBuilder( + column: $table.localSubPaths, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get coverSubPath => $composableBuilder( + column: $table.coverSubPath, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get previewSubPaths => $composableBuilder( + column: $table.previewSubPaths, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get readCount => $composableBuilder( + column: $table.readCount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get currentPage => $composableBuilder( + column: $table.currentPage, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, @@ -538,6 +1277,17 @@ class $$BookTableTableAnnotationComposer builder: (column) => column, ); + GeneratedColumn get coverSubPath => $composableBuilder( + column: $table.coverSubPath, + builder: (column) => column, + ); + + GeneratedColumnWithTypeConverter?, String> get previewSubPaths => + $composableBuilder( + column: $table.previewSubPaths, + builder: (column) => column, + ); + GeneratedColumn get readCount => $composableBuilder(column: $table.readCount, builder: (column) => column); @@ -584,6 +1334,8 @@ class $$BookTableTableTableManager Value id = const Value.absent(), Value name = const Value.absent(), Value> localSubPaths = const Value.absent(), + Value coverSubPath = const Value.absent(), + Value?> previewSubPaths = const Value.absent(), Value readCount = const Value.absent(), Value currentPage = const Value.absent(), Value createdAt = const Value.absent(), @@ -591,6 +1343,8 @@ class $$BookTableTableTableManager id: id, name: name, localSubPaths: localSubPaths, + coverSubPath: coverSubPath, + previewSubPaths: previewSubPaths, readCount: readCount, currentPage: currentPage, createdAt: createdAt, @@ -600,6 +1354,8 @@ class $$BookTableTableTableManager Value id = const Value.absent(), required String name, required List localSubPaths, + Value coverSubPath = const Value.absent(), + Value?> previewSubPaths = const Value.absent(), Value readCount = const Value.absent(), Value currentPage = const Value.absent(), Value createdAt = const Value.absent(), @@ -607,6 +1363,8 @@ class $$BookTableTableTableManager id: id, name: name, localSubPaths: localSubPaths, + coverSubPath: coverSubPath, + previewSubPaths: previewSubPaths, readCount: readCount, currentPage: currentPage, createdAt: createdAt, @@ -636,10 +1394,377 @@ typedef $$BookTableTableProcessedTableManager = BookTableData, PrefetchHooks Function() >; +typedef $$CollectionTableTableCreateCompanionBuilder = + CollectionTableCompanion Function({ + Value id, + required String name, + Value description, + Value coverImageSubPath, + }); +typedef $$CollectionTableTableUpdateCompanionBuilder = + CollectionTableCompanion Function({ + Value id, + Value name, + Value description, + Value coverImageSubPath, + }); + +class $$CollectionTableTableFilterComposer + extends Composer<_$AppDatabase, $CollectionTableTable> { + $$CollectionTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get description => $composableBuilder( + column: $table.description, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get coverImageSubPath => $composableBuilder( + column: $table.coverImageSubPath, + builder: (column) => ColumnFilters(column), + ); +} + +class $$CollectionTableTableOrderingComposer + extends Composer<_$AppDatabase, $CollectionTableTable> { + $$CollectionTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get description => $composableBuilder( + column: $table.description, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get coverImageSubPath => $composableBuilder( + column: $table.coverImageSubPath, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$CollectionTableTableAnnotationComposer + extends Composer<_$AppDatabase, $CollectionTableTable> { + $$CollectionTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get description => $composableBuilder( + column: $table.description, + builder: (column) => column, + ); + + GeneratedColumn get coverImageSubPath => $composableBuilder( + column: $table.coverImageSubPath, + builder: (column) => column, + ); +} + +class $$CollectionTableTableTableManager + extends + RootTableManager< + _$AppDatabase, + $CollectionTableTable, + CollectionTableData, + $$CollectionTableTableFilterComposer, + $$CollectionTableTableOrderingComposer, + $$CollectionTableTableAnnotationComposer, + $$CollectionTableTableCreateCompanionBuilder, + $$CollectionTableTableUpdateCompanionBuilder, + ( + CollectionTableData, + BaseReferences< + _$AppDatabase, + $CollectionTableTable, + CollectionTableData + >, + ), + CollectionTableData, + PrefetchHooks Function() + > { + $$CollectionTableTableTableManager( + _$AppDatabase db, + $CollectionTableTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$CollectionTableTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$CollectionTableTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$CollectionTableTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value description = const Value.absent(), + Value coverImageSubPath = const Value.absent(), + }) => CollectionTableCompanion( + id: id, + name: name, + description: description, + coverImageSubPath: coverImageSubPath, + ), + createCompanionCallback: + ({ + Value id = const Value.absent(), + required String name, + Value description = const Value.absent(), + Value coverImageSubPath = const Value.absent(), + }) => CollectionTableCompanion.insert( + id: id, + name: name, + description: description, + coverImageSubPath: coverImageSubPath, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$CollectionTableTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $CollectionTableTable, + CollectionTableData, + $$CollectionTableTableFilterComposer, + $$CollectionTableTableOrderingComposer, + $$CollectionTableTableAnnotationComposer, + $$CollectionTableTableCreateCompanionBuilder, + $$CollectionTableTableUpdateCompanionBuilder, + ( + CollectionTableData, + BaseReferences< + _$AppDatabase, + $CollectionTableTable, + CollectionTableData + >, + ), + CollectionTableData, + PrefetchHooks Function() + >; +typedef $$CollectionBookTableTableCreateCompanionBuilder = + CollectionBookTableCompanion Function({ + Value id, + required int collectionId, + required int bookId, + }); +typedef $$CollectionBookTableTableUpdateCompanionBuilder = + CollectionBookTableCompanion Function({ + Value id, + Value collectionId, + Value bookId, + }); + +class $$CollectionBookTableTableFilterComposer + extends Composer<_$AppDatabase, $CollectionBookTableTable> { + $$CollectionBookTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get collectionId => $composableBuilder( + column: $table.collectionId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get bookId => $composableBuilder( + column: $table.bookId, + builder: (column) => ColumnFilters(column), + ); +} + +class $$CollectionBookTableTableOrderingComposer + extends Composer<_$AppDatabase, $CollectionBookTableTable> { + $$CollectionBookTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get collectionId => $composableBuilder( + column: $table.collectionId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get bookId => $composableBuilder( + column: $table.bookId, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$CollectionBookTableTableAnnotationComposer + extends Composer<_$AppDatabase, $CollectionBookTableTable> { + $$CollectionBookTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get collectionId => $composableBuilder( + column: $table.collectionId, + builder: (column) => column, + ); + + GeneratedColumn get bookId => + $composableBuilder(column: $table.bookId, builder: (column) => column); +} + +class $$CollectionBookTableTableTableManager + extends + RootTableManager< + _$AppDatabase, + $CollectionBookTableTable, + CollectionBookTableData, + $$CollectionBookTableTableFilterComposer, + $$CollectionBookTableTableOrderingComposer, + $$CollectionBookTableTableAnnotationComposer, + $$CollectionBookTableTableCreateCompanionBuilder, + $$CollectionBookTableTableUpdateCompanionBuilder, + ( + CollectionBookTableData, + BaseReferences< + _$AppDatabase, + $CollectionBookTableTable, + CollectionBookTableData + >, + ), + CollectionBookTableData, + PrefetchHooks Function() + > { + $$CollectionBookTableTableTableManager( + _$AppDatabase db, + $CollectionBookTableTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$CollectionBookTableTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$CollectionBookTableTableOrderingComposer( + $db: db, + $table: table, + ), + createComputedFieldComposer: () => + $$CollectionBookTableTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value collectionId = const Value.absent(), + Value bookId = const Value.absent(), + }) => CollectionBookTableCompanion( + id: id, + collectionId: collectionId, + bookId: bookId, + ), + createCompanionCallback: + ({ + Value id = const Value.absent(), + required int collectionId, + required int bookId, + }) => CollectionBookTableCompanion.insert( + id: id, + collectionId: collectionId, + bookId: bookId, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$CollectionBookTableTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $CollectionBookTableTable, + CollectionBookTableData, + $$CollectionBookTableTableFilterComposer, + $$CollectionBookTableTableOrderingComposer, + $$CollectionBookTableTableAnnotationComposer, + $$CollectionBookTableTableCreateCompanionBuilder, + $$CollectionBookTableTableUpdateCompanionBuilder, + ( + CollectionBookTableData, + BaseReferences< + _$AppDatabase, + $CollectionBookTableTable, + CollectionBookTableData + >, + ), + CollectionBookTableData, + PrefetchHooks Function() + >; class $AppDatabaseManager { final _$AppDatabase _db; $AppDatabaseManager(this._db); $$BookTableTableTableManager get bookTable => $$BookTableTableTableManager(_db, _db.bookTable); + $$CollectionTableTableTableManager get collectionTable => + $$CollectionTableTableTableManager(_db, _db.collectionTable); + $$CollectionBookTableTableTableManager get collectionBookTable => + $$CollectionBookTableTableTableManager(_db, _db.collectionBookTable); } diff --git a/lib/core/di/app_di.dart b/lib/core/di/app_di.dart deleted file mode 100644 index 6f76a02..0000000 --- a/lib/core/di/app_di.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:provider/single_child_widget.dart'; -import 'package:tele_book/core/di/core_di.dart'; -import 'package:tele_book/core/di/datasource_di.dart'; -import 'package:tele_book/core/di/repository_di.dart'; -import 'package:tele_book/core/di/service_di.dart'; -import 'package:tele_book/core/di/store_di.dart'; - - - -List createAppDI() { - return [ - ...createCoreDI(), - ...createDatasourceDI(), - ...createRepositoryDI(), - ...createServiceDI(), - ...createStoreDI(), - ]; -} diff --git a/lib/core/di/core_di.dart b/lib/core/di/core_di.dart deleted file mode 100644 index ac8ac5d..0000000 --- a/lib/core/di/core_di.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:provider/single_child_widget.dart'; -import 'package:provider/provider.dart'; -import 'package:tele_book/core/db/app_database.dart'; - -List createCoreDI() { - return [Provider(create: (_) => AppDatabase())]; -} diff --git a/lib/core/di/datasource_di.dart b/lib/core/di/datasource_di.dart deleted file mode 100644 index c346333..0000000 --- a/lib/core/di/datasource_di.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:provider/provider.dart'; -import 'package:provider/single_child_widget.dart'; -import 'package:tele_book/feature/download/datasource/runtime/download_runtime_datasource.dart'; - -List createDatasourceDI() { - return [ - - Provider(create: (context) => DownloadRuntimeDatasource()), - ]; -} diff --git a/lib/core/di/repository_di.dart b/lib/core/di/repository_di.dart deleted file mode 100644 index c7f8cc3..0000000 --- a/lib/core/di/repository_di.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:provider/provider.dart'; -import 'package:provider/single_child_widget.dart'; -import 'package:tele_book/feature/book/repository/book_repository.dart'; -import 'package:tele_book/feature/download/repository/download_repository.dart'; - -List createRepositoryDI() { - return [ - Provider(create: (context) => BookRepository(context.read())), - Provider(create: (context) => DownloadRepository(context.read())), - ]; -} diff --git a/lib/core/di/service_di.dart b/lib/core/di/service_di.dart deleted file mode 100644 index cf56ea5..0000000 --- a/lib/core/di/service_di.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:provider/provider.dart'; -import 'package:provider/single_child_widget.dart'; -import 'package:tele_book/feature/book/service/book_service.dart'; -import 'package:tele_book/feature/download/service/download_service.dart'; -import 'package:tele_book/feature/parse/service/parse_archive_service.dart'; -import 'package:tele_book/feature/parse/service/parse_pdf_service.dart'; -import 'package:tele_book/feature/parse/service/parse_web_service.dart'; - -List createServiceDI() { - return [ - Provider(create: (context) => BookService(context.read())), - Provider(create: (context) => ParseWebService()), - Provider(create: (context) => ParseArchiveService()), - Provider(create: (context) => ParsePdfService()), - Provider( - create: (context) => DownloadService(context.read(), context.read()), - ), - ]; -} diff --git a/lib/core/di/store_di.dart b/lib/core/di/store_di.dart deleted file mode 100644 index 3cf917a..0000000 --- a/lib/core/di/store_di.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:provider/provider.dart'; -import 'package:provider/single_child_widget.dart'; -import 'package:tele_book/feature/book/store/book_store.dart'; -import 'package:tele_book/feature/download/store/download_store.dart'; - -List createStoreDI() { - return [ - ChangeNotifierProvider(create: (context) => BookStore(context.read())), - ChangeNotifierProvider(create: (context) => DownloadStore(context.read())), - ]; -} diff --git a/lib/core/route/app_route.dart b/lib/core/route/app_route.dart index 593dda5..d29fc3e 100644 --- a/lib/core/route/app_route.dart +++ b/lib/core/route/app_route.dart @@ -3,10 +3,12 @@ import 'package:go_router/go_router.dart'; import 'package:tele_book/core/db/app_database.dart'; import 'package:tele_book/feature/book/ui/view/book_form_view.dart'; import 'package:tele_book/feature/book/ui/view/book_page_view.dart'; -import 'package:tele_book/feature/book/ui/view/book_view.dart'; +import 'package:tele_book/feature/book/ui/view/book_picker_view.dart'; +import 'package:tele_book/feature/collection/ui/view/collection_book_view.dart'; import 'package:tele_book/feature/download/ui/view/download_list_view.dart'; import 'package:tele_book/feature/export/ui/view/export_batch_form_view.dart'; import 'package:tele_book/feature/export/ui/view/export_single_form_view.dart'; +import 'package:tele_book/feature/main/view/main_view.dart'; import 'package:tele_book/feature/parse/ui/view/parse_archive_view.dart'; import 'package:tele_book/feature/parse/ui/view/parse_batch_archive_view.dart'; import 'package:tele_book/feature/parse/ui/view/parse_batch_image_folder_view.dart'; @@ -18,7 +20,7 @@ import 'package:tele_book/feature/parse/ui/view/parse_web_view.dart'; class AppRoute { // 主页面 - static const book = '/book'; + static const main = '/main'; // 导出 static const exportSingle = '/export/single'; @@ -27,10 +29,15 @@ class AppRoute { // 书籍相关 static const bookForm = '/book/form'; static const bookPage = '/book/page'; + static const bookPicker = '/book/picker'; // 下载 static const download = '/download'; + + static const collection = '/collection'; + static const collectionBook = '/collection/book'; + // 解析 static const parseForm = '/parse/form'; static const parseWeb = '/parse/web'; @@ -43,12 +50,12 @@ class AppRoute { static const parseArchiveBatchEdit = '/parse/archive/batch/edit'; static final GoRouter router = GoRouter( - initialLocation: book, + initialLocation: main, routes: [ GoRoute( - path: book, + path: main, pageBuilder: (context, state) { - return MaterialPage(child: BookView()); + return MaterialPage(child: MainView()); }, ), GoRoute( @@ -71,12 +78,41 @@ class AppRoute { return MaterialPage(child: BookPageView(book: book)); }, ), + GoRoute( + path: bookPicker, + pageBuilder: (context, state) { + final extra = state.extra; + Set disabledBookIds = {}; + if (extra is List) { + disabledBookIds = extra.toSet(); + } else if (extra is Set) { + disabledBookIds = extra; + } else if (extra is List) { + disabledBookIds = extra.whereType().toSet(); + } + return MaterialPage( + child: BookPickerView(disabledBookIds: disabledBookIds), + ); + }, + ), GoRoute( path: download, pageBuilder: (context, state) { return MaterialPage(child: Scaffold(body: DownloadListView())); }, ), + GoRoute( + path: collectionBook, + pageBuilder: (context, state) { + final collectionId = state.extra as int?; + if (collectionId == null) { + return MaterialPage(child: ErrorRoutePage(message: "缺少书籍收藏夹ID参数")); + } + return MaterialPage( + child: CollectionBookView(collectionId: collectionId), + ); + }, + ), GoRoute( path: exportSingle, pageBuilder: (context, state) { @@ -126,7 +162,9 @@ class AppRoute { if (extra is List) { final paths = extra.whereType().toList(); if (paths.isNotEmpty) { - return MaterialPage(child: ParseImageFolderView(imagePaths: paths)); + return MaterialPage( + child: ParseImageFolderView(imagePaths: paths), + ); } } return MaterialPage(child: ErrorRoutePage(message: "缺少图片路径参数")); @@ -200,7 +238,9 @@ class AppRoute { pageBuilder: (context, state) { final path = state.extra as String?; if (path == null) { - return MaterialPage(child: ErrorRoutePage(message: "缺少 PDF 文件路径参数")); + return MaterialPage( + child: ErrorRoutePage(message: "缺少 PDF 文件路径参数"), + ); } return MaterialPage(child: ParsePdfView(pdfPath: path)); }, diff --git a/lib/feature/book/datasource/local/book_local_datasource.dart b/lib/feature/book/datasource/local/book_local_datasource.dart index 5bfebe7..3a2fb0d 100644 --- a/lib/feature/book/datasource/local/book_local_datasource.dart +++ b/lib/feature/book/datasource/local/book_local_datasource.dart @@ -11,6 +11,10 @@ class BookLocalDatasource extends DatabaseAccessor with _$BookLocalDatasourceMixin { BookLocalDatasource(super.attachedDatabase); + Stream> watchAllBooks() { + return (select(bookTable)..orderBy([(t) => OrderingTerm(expression: t.createdAt, mode: OrderingMode.desc)])).watch(); + } + Stream> watchBooks({ int? page, int? pageSize, @@ -46,7 +50,7 @@ class BookLocalDatasource extends DatabaseAccessor return q.watch(); } - Future> getBooks({ + Future> getPagingBooks({ int? page, DateTime? lastCreatedAt, int limit = 20, diff --git a/lib/feature/book/enum/book_menu_type.dart b/lib/feature/book/enum/book_menu_type.dart new file mode 100644 index 0000000..28ae5f6 --- /dev/null +++ b/lib/feature/book/enum/book_menu_type.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +/// 顶部菜单(排序方式 + 布局切换) +enum BookTopMenuType { + asc(icon: Icons.arrow_upward, title: '升序'), + desc(icon: Icons.arrow_downward, title: '降序'), + name(icon: Icons.sort_by_alpha, title: '按书名'), + lastCreatedAt(icon: Icons.access_time, title: '按添加时间'), + list(icon: Icons.view_list, title: '列表视图'), + grid(icon: Icons.grid_view, title: '网格视图'); + + const BookTopMenuType({required this.icon, required this.title}); + + final IconData icon; + final String title; +} + +/// 单本书条目菜单 +enum BookItemMenuType { + edit(icon: Icons.edit, title: '编辑'), + export(icon: Icons.move_to_inbox, title: '导出'), + delete(icon: Icons.delete, title: '删除'); + + const BookItemMenuType({required this.icon, required this.title}); + + final IconData icon; + final String title; +} + diff --git a/lib/feature/book/model/state/book_list_state.dart b/lib/feature/book/model/state/book_list_state.dart new file mode 100644 index 0000000..031ec46 --- /dev/null +++ b/lib/feature/book/model/state/book_list_state.dart @@ -0,0 +1,65 @@ +import 'package:tele_book/core/db/app_database.dart'; +import 'package:tele_book/feature/book/enum/book_sort.dart'; + +class BookListState { + // --- 数据与查询状态 --- + final List bookVos; + final bool hasMore; + final bool isLoadingMore; + final String name; + final BookSort? sort; + + // --- 💡 塞入 UI 交互状态 --- + final bool isSelectionMode; + final Set selectedBookIds; // 使用 Set 处理查找更高效 + final BookLayout layout; // 列表或网格布局 + + BookListState({ + required this.bookVos, + required this.hasMore, + required this.name, + this.sort, + this.isLoadingMore = false, + this.isSelectionMode = false, + this.selectedBookIds = const {}, + this.layout = BookLayout.list, + }); + + // 快捷派生属性:获取当前选中的所有书籍模型 + List get selectedBooks => + bookVos.where((vo) => selectedBookIds.contains(vo.book.id)).toList(); + + BookListState copyWith({ + List? bookVos, + bool? hasMore, + bool? isLoadingMore, + String? name, + BookSort? sort, + bool? isSelectionMode, + Set? selectedBookIds, + BookLayout? layout, + }) { + return BookListState( + bookVos: bookVos ?? this.bookVos, + hasMore: hasMore ?? this.hasMore, + isLoadingMore: isLoadingMore ?? this.isLoadingMore, + name: name ?? this.name, + sort: sort ?? this.sort, + isSelectionMode: isSelectionMode ?? this.isSelectionMode, + selectedBookIds: selectedBookIds ?? this.selectedBookIds, + layout: layout ?? this.layout, + ); + } +} + +enum BookLayout { + list, + grid, +} + +class BookListItemVo { + final BookTableData book; + final String coverImagePath; + + BookListItemVo({required this.book, required this.coverImagePath}); +} diff --git a/lib/feature/book/model/table/book_table.dart b/lib/feature/book/model/table/book_table.dart index f7eb341..029cff0 100644 --- a/lib/feature/book/model/table/book_table.dart +++ b/lib/feature/book/model/table/book_table.dart @@ -8,6 +8,11 @@ class BookTable extends Table { TextColumn get localSubPaths => text().map(const StringListConverter())(); + TextColumn get coverSubPath => text().nullable()(); + + TextColumn get previewSubPaths => + text().nullable().map(const StringListConverter())(); + IntColumn get readCount => integer().withDefault(const Constant(0))(); IntColumn get currentPage => integer().withDefault(const Constant(0))(); diff --git a/lib/feature/book/model/vo/book_vo.dart b/lib/feature/book/model/vo/book_vo.dart index 4d87bb8..9a2c2ad 100644 --- a/lib/feature/book/model/vo/book_vo.dart +++ b/lib/feature/book/model/vo/book_vo.dart @@ -1,22 +1,10 @@ import 'package:tele_book/core/db/app_database.dart'; +import 'package:tele_book/feature/book/enum/book_sort.dart'; -class BookListVo { - final List bookVos; - - BookListVo({required this.bookVos}); -} - - -class BookListItemVo { - final BookTableData book; - final String coverImagePath; - - BookListItemVo({required this.book, required this.coverImagePath}); -} class BookDetailVo { final BookTableData book; final List imagePaths; BookDetailVo({required this.book, required this.imagePaths}); -} \ No newline at end of file +} diff --git a/lib/feature/book/repository/book_repository.dart b/lib/feature/book/repository/book_repository.dart index 9ee4c9f..9740b4f 100644 --- a/lib/feature/book/repository/book_repository.dart +++ b/lib/feature/book/repository/book_repository.dart @@ -1,44 +1,34 @@ import 'dart:io'; +import 'package:drift/drift.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path/path.dart' as p; +import 'package:tele_book/common/config/global_config.dart'; import 'package:tele_book/core/db/app_database.dart'; import 'package:tele_book/core/util/failure_util.dart'; import 'package:tele_book/core/util/result_util.dart'; import 'package:tele_book/feature/book/datasource/local/book_local_datasource.dart'; import 'package:tele_book/feature/book/enum/book_sort.dart'; import 'package:tele_book/feature/book/model/dto/save_as_book_dto.dart'; +import 'package:tele_book/feature/book/service/book_image_service.dart'; import 'package:uuid/uuid.dart'; -import '../../../common/config/global_config.dart'; +/// 保存步骤枚举,用于 UI 分步展示进度 +enum SaveStep { + generateCover, + generatePreview, + saveOriginal, + saveDatabase; -// ── 顶层函数(供 compute() 在 Isolate 中调用)───────────── - -class _CopyBookArgs { - final String bookId; - final String destDirPath; - final List srcPaths; - _CopyBookArgs({required this.bookId, required this.destDirPath, required this.srcPaths}); -} - -/// 在后台 Isolate 中复制文件,返回存储用的相对路径列表 -Future> _copyBookFiles(_CopyBookArgs args) async { - final dir = Directory(args.destDirPath); - await dir.create(recursive: true); - final relPaths = []; - for (var i = 0; i < args.srcPaths.length; i++) { - final src = File(args.srcPaths[i]); - if (!await src.exists()) { - throw FileSystemException('source file not found', args.srcPaths[i]); - } - final fileName = i.toString().padLeft(7, '0'); - await src.copy('${args.destDirPath}/$fileName'); - relPaths.add('${args.bookId}/$fileName'); - } - return relPaths; + String get label => switch (this) { + SaveStep.generateCover => '生成封面图', + SaveStep.generatePreview => '生成预览图', + SaveStep.saveOriginal => '保存原图', + SaveStep.saveDatabase => '保存数据', + }; } - /// 在后台 Isolate 中递归删除目录列表 Future _deleteBookDirs(List dirPaths) async { for (final path in dirPaths) { @@ -49,39 +39,33 @@ Future _deleteBookDirs(List dirPaths) async { } } +final bookRepositoryProvider = Provider((ref) { + final database = ref.watch(databaseProvider); + return BookRepository(database); +}); + class BookRepository { final AppDatabase _db; - late final BookLocalDatasource _bookLocalDatasource = _db.bookLocalDatasource; + late final BookLocalDatasource _bookLocalDatasource = + _db.bookLocalDatasource; BookRepository(this._db); - Stream> watchBooks({ - int? page, - int? pageSize, - DateTime? lastCreatedAt, - String? name, - BookSort? sort, - }) { - return _bookLocalDatasource.watchBooks( - page: page, - pageSize: pageSize, - lastCreatedAt: lastCreatedAt, - name: name, - sort: sort, - ); + Stream> watchAllBooks() { + return _bookLocalDatasource.watchAllBooks(); } - Future> fetchBooks({ + Future> getPagedBooks({ int? page, - int? pageSize, + int pageSize = 20, DateTime? lastCreatedAt, String? name, BookSort? sort, - }) async { - return _bookLocalDatasource.getBooks( + }) { + return _bookLocalDatasource.getPagingBooks( page: page, lastCreatedAt: lastCreatedAt, - limit: pageSize ?? 20, + limit: pageSize, name: name, sort: sort, ); @@ -99,26 +83,20 @@ class BookRepository { final book = await _bookLocalDatasource.getById(id); if (book == null) return Result.failure(BusinessFailure(message: '书籍不存在')); - // 先删 DB 记录 await _db.transaction(() async { await _bookLocalDatasource.deleteById(id); }); - // 计算要删的目录(把文件清理放到后台 Isolate) + // 通过 localSubPaths 推算 bookId 目录 final bookDirs = {}; for (final subPath in book.localSubPaths) { - if (p.isAbsolute(subPath)) { - bookDirs.add(p.dirname(subPath)); - } else { - final normalized = subPath.replaceAll('\\', '/'); - final segments = normalized.split('/').where((e) => e.isNotEmpty).toList(); - if (segments.isNotEmpty) { - bookDirs.add(p.join(GlobalConfig.booksDir.path, segments.first)); - } + final normalized = subPath.replaceAll('\\', '/'); + final segments = normalized.split('/').where((e) => e.isNotEmpty).toList(); + if (segments.isNotEmpty) { + bookDirs.add(p.join(GlobalConfig.booksDir.path, segments.first)); } } - // 后台 Isolate 删除目录,不阻塞主线程 if (bookDirs.isNotEmpty) { await compute(_deleteBookDirs, bookDirs.toList()); } @@ -126,76 +104,141 @@ class BookRepository { return Result.success(null); } - /// 保存单本书:文件复制在后台 Isolate,DB 写入在主线程 - Future> saveAsBook(SaveAsBookDto dto) async { + /// 保存单本书:封面 → 预览图 → 原图 → DB + Future> saveAsBook( + SaveAsBookDto dto, { + void Function(SaveStep step, int current, int total)? onStepProgress, + }) async { final bookId = const Uuid().v4(); - final destDirPath = '${GlobalConfig.booksDir.path}/$bookId'; + final bookDir = '${GlobalConfig.booksDir.path}/$bookId'; + final originalDir = '$bookDir/original'; + final previewDir = '$bookDir/preview'; + final coverPath = '$bookDir/cover.jpg'; try { - // ① 文件复制放后台 Isolate - final relPaths = await compute( - _copyBookFiles, - _CopyBookArgs(bookId: bookId, destDirPath: destDirPath, srcPaths: dto.paths), + // ① 生成封面图 + onStepProgress?.call(SaveStep.generateCover, 0, 1); + await BookImageService.generateCover(dto.paths.first, coverPath); + onStepProgress?.call(SaveStep.generateCover, 1, 1); + + // ② 生成预览图 + onStepProgress?.call(SaveStep.generatePreview, 0, dto.paths.length); + await BookImageService.generatePreviewBatch( + srcPaths: dto.paths, + destDir: previewDir, + onProgress: (current, total) { + onStepProgress?.call(SaveStep.generatePreview, current, total); + }, ); - // ② DB 写入(快,主线程即可) + // ③ 复制原图(Isolate) + onStepProgress?.call(SaveStep.saveOriginal, 0, dto.paths.length); + final relPaths = await BookImageService.copyOriginals( + dto.paths, + originalDir, + bookId, + ); + onStepProgress?.call(SaveStep.saveOriginal, dto.paths.length, dto.paths.length); + + // ④ 写入数据库 + onStepProgress?.call(SaveStep.saveDatabase, 0, 1); + final coverSubPath = '$bookId/cover.jpg'; + final previewSubPaths = List.generate( + dto.paths.length, + (i) => '$bookId/preview/${i.toString().padLeft(7, '0')}.jpg', + ); await _bookLocalDatasource.insertBook( - BookTableCompanion.insert(name: dto.title, localSubPaths: relPaths), + BookTableCompanion.insert( + name: dto.title, + localSubPaths: relPaths, + coverSubPath: Value(coverSubPath), + previewSubPaths: Value(previewSubPaths), + ), ); + onStepProgress?.call(SaveStep.saveDatabase, 1, 1); return Result.success(null); } catch (e, st) { - // 清理已创建的目录(后台 Isolate) - await compute(_deleteBookDirs, [destDirPath]); - return Result.failure(BusinessFailure(message: '保存书籍失败', details: e, stackTrace: st)); + await compute(_deleteBookDirs, [bookDir]); + return Result.failure( + BusinessFailure(message: '保存书籍失败', details: e, stackTrace: st), + ); } } - /// 批量保存: - /// ① 按书逐一复制文件(后台 Isolate,循环间让出 UI 线程) - /// ② 全部复制成功后,批量 DB 写入(单事务) - /// ③ 任意环节失败,清理已复制的目录 + /// 批量保存:逐本执行 封面→预览→原图,最后批量 DB 写入 Future> saveBatchAsBooks( List dos, Function(int count) onProgress, ) async { - // 记录已创建目录,用于失败回滚 final createdDirs = []; - // 记录每本书的(相对路径列表, 标题) - final bookData = <({String title, List relPaths})>[]; + final bookData = <({ + String title, + List relPaths, + String coverSubPath, + List previewSubPaths, + })>[]; try { - // ── 阶段一:文件复制(在后台 Isolate,不在 DB 事务内)── for (var i = 0; i < dos.length; i++) { final dto = dos[i]; final bookId = const Uuid().v4(); - final destDirPath = '${GlobalConfig.booksDir.path}/$bookId'; + final bookDir = '${GlobalConfig.booksDir.path}/$bookId'; + final originalDir = '$bookDir/original'; + final previewDir = '$bookDir/preview'; + final coverPath = '$bookDir/cover.jpg'; - // 每本书让出一次事件循环,保持 UI 刷新 await Future.delayed(Duration.zero); - final relPaths = await compute( - _copyBookFiles, - _CopyBookArgs(bookId: bookId, destDirPath: destDirPath, srcPaths: dto.paths), + // ① 封面 + await BookImageService.generateCover(dto.paths.first, coverPath); + + // ② 预览图 + await BookImageService.generatePreviewBatch( + srcPaths: dto.paths, + destDir: previewDir, + ); + + // ③ 原图 + final relPaths = await BookImageService.copyOriginals( + dto.paths, + originalDir, + bookId, + ); + + createdDirs.add(bookDir); + + final coverSubPath = '$bookId/cover.jpg'; + final previewSubPaths = List.generate( + dto.paths.length, + (j) => '$bookId/preview/${j.toString().padLeft(7, '0')}.jpg', ); + bookData.add(( + title: dto.title, + relPaths: relPaths, + coverSubPath: coverSubPath, + previewSubPaths: previewSubPaths, + )); - createdDirs.add(destDirPath); - bookData.add((title: dto.title, relPaths: relPaths)); onProgress(i + 1); } - // ── 阶段二:批量 DB 写入(单事务,仅 DB 操作,耗时极短)── + // ④ 批量 DB 写入 await _db.transaction(() async { for (final book in bookData) { await _bookLocalDatasource.insertBook( - BookTableCompanion.insert(name: book.title, localSubPaths: book.relPaths), + BookTableCompanion.insert( + name: book.title, + localSubPaths: book.relPaths, + coverSubPath: Value(book.coverSubPath), + previewSubPaths: Value(book.previewSubPaths), + ), ); } }); return Result.success(null); } catch (e, st) { - // DB 若已写入则由 Drift 事务回滚,清理已复制的文件目录 if (createdDirs.isNotEmpty) { await compute(_deleteBookDirs, createdDirs); } @@ -204,4 +247,43 @@ class BookRepository { ); } } + + /// 为已存在的书籍重新生成封面和预览图(编辑场景) + Future regenerateImages(BookTableData book) async { + final bookId = book.localSubPaths.first.split('/').first; + final bookDir = '${GlobalConfig.booksDir.path}/$bookId'; + final previewDir = '$bookDir/preview'; + final coverPath = '$bookDir/cover.jpg'; + + // 解析原图绝对路径 + final originalPaths = book.localSubPaths + .map((sub) => GlobalConfig.resolveBookPath(sub)) + .toList(); + if (originalPaths.isEmpty) return; + + // 生成封面 + await BookImageService.generateCover(originalPaths.first, coverPath); + + // 生成预览图 + // 先清空旧的预览图目录 + final previewDirectory = Directory(previewDir); + if (await previewDirectory.exists()) { + await previewDirectory.delete(recursive: true); + } + await BookImageService.generatePreviewBatch( + srcPaths: originalPaths, + destDir: previewDir, + ); + + // 更新数据库 + final previewSubPaths = List.generate( + originalPaths.length, + (i) => '$bookId/preview/${i.toString().padLeft(7, '0')}.jpg', + ); + final updatedBook = book.copyWith( + coverSubPath: Value('$bookId/cover.jpg'), + previewSubPaths: Value(previewSubPaths), + ); + await _bookLocalDatasource.updateBook(updatedBook); + } } diff --git a/lib/feature/book/service/book_image_service.dart b/lib/feature/book/service/book_image_service.dart new file mode 100644 index 0000000..5c362c1 --- /dev/null +++ b/lib/feature/book/service/book_image_service.dart @@ -0,0 +1,97 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; + +/// 书籍图片处理服务:生成封面缩略图和预览图 +class BookImageService { + /// 封面缩略图宽度 + static const int coverWidth = 300; + + /// 预览图宽度 + static const int previewWidth = 1080; + + /// JPEG 压缩质量 (0-100) + static const int jpegQuality = 80; + + + /// 从原图生成封面缩略图 + /// [srcPath] 原图绝对路径 + /// [destPath] 封面输出绝对路径 (如 books/{bookId}/cover.jpg) + static Future generateCover(String srcPath, String destPath) async { + final result = await FlutterImageCompress.compressAndGetFile( + srcPath, + destPath, + minWidth: coverWidth, + minHeight: coverWidth, + quality: jpegQuality, + ); + if (result == null) { + throw Exception('封面生成失败: $srcPath'); + } + } + + /// 从原图生成单张预览图 + /// [srcPath] 原图绝对路径 + /// [destPath] 预览图输出绝对路径 + static Future generatePreview(String srcPath, String destPath) async { + final result = await FlutterImageCompress.compressAndGetFile( + srcPath, + destPath, + minWidth: previewWidth, + minHeight: previewWidth, + quality: jpegQuality, + ); + if (result == null) { + throw Exception('预览图生成失败: $srcPath'); + } + } + + /// 批量生成预览图 + /// [srcPaths] 原图绝对路径列表 + /// [destDir] 预览图输出目录 + /// [onProgress] 逐张完成回调 (current, total) + static Future> generatePreviewBatch({ + required List srcPaths, + required String destDir, + void Function(int current, int total)? onProgress, + }) async { + await Directory(destDir).create(recursive: true); + final previewPaths = []; + for (var i = 0; i < srcPaths.length; i++) { + final fileName = '${i.toString().padLeft(7, '0')}.jpg'; + final destPath = '$destDir/$fileName'; + await generatePreview(srcPaths[i], destPath); + previewPaths.add(destPath); + onProgress?.call(i + 1, srcPaths.length); + } + return previewPaths; + } + + /// 在 Isolate 中批量复制原图 + static Future> copyOriginals( + List srcPaths, + String destDir, + String bookId, + ) async { + return compute(_copyOriginalsIsolate, (srcPaths, destDir, bookId)); + } +} + +Future> _copyOriginalsIsolate( + (List srcPaths, String destDir, String bookId) args, +) async { + final (srcPaths, destDir, bookId) = args; + await Directory(destDir).create(recursive: true); + final relPaths = []; + for (var i = 0; i < srcPaths.length; i++) { + final src = File(srcPaths[i]); + if (!await src.exists()) { + throw FileSystemException('source file not found', srcPaths[i]); + } + final fileName = i.toString().padLeft(7, '0'); + await src.copy('$destDir/$fileName'); + relPaths.add('$bookId/original/$fileName'); + } + return relPaths; +} diff --git a/lib/feature/book/service/book_service.dart b/lib/feature/book/service/book_service.dart deleted file mode 100644 index 4822000..0000000 --- a/lib/feature/book/service/book_service.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:tele_book/common/config/global_config.dart'; -import 'package:tele_book/core/db/app_database.dart'; -import 'package:tele_book/feature/book/enum/book_sort.dart'; -import 'package:tele_book/feature/book/model/vo/book_vo.dart'; -import 'package:tele_book/feature/book/repository/book_repository.dart'; - -class BookService { - final BookRepository _bookRepository; - - BookService(this._bookRepository); - - Stream watchBooks({ - int? page, - int? pageSize, - DateTime? lastCreatedAt, - String? name, - BookSort? sort, - }) { - return _bookRepository - .watchBooks( - page: page, - pageSize: pageSize, - lastCreatedAt: lastCreatedAt, - name: name, - sort: sort, - ) - .map((books) { - final bookVos = books.map((book) { - // 用 GlobalConfig 解析封面路径,避免 async/await - final coverPath = book.localSubPaths.isNotEmpty - ? GlobalConfig.resolveBookPath(book.localSubPaths.first) - : ''; - return BookListItemVo(book: book, coverImagePath: coverPath); - }).toList(); - return BookListVo(bookVos: bookVos); - }); - } - - Future fetchBooks({ - int? page, - DateTime? lastCreatedAt, - int pageSize = 20, - String? name, - BookSort? sort, - }) async { - final books = await _bookRepository.fetchBooks( - page: page, - lastCreatedAt: lastCreatedAt, - pageSize: pageSize, - name: name, - sort: sort, - ); - final bookVos = books.map((book) { - final coverPath = book.localSubPaths.isNotEmpty - ? GlobalConfig.resolveBookPath(book.localSubPaths.first) - : ''; - return BookListItemVo(book: book, coverImagePath: coverPath); - }).toList(); - return BookListVo(bookVos: bookVos); - } - - Future updateBook(BookTableData book) { - return _bookRepository.updateBook(book); - } - } diff --git a/lib/feature/book/store/book_store.dart b/lib/feature/book/store/book_store.dart deleted file mode 100644 index 7d045d2..0000000 --- a/lib/feature/book/store/book_store.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:tele_book/feature/book/model/vo/book_vo.dart'; -import 'package:tele_book/feature/book/enum/book_sort.dart'; -import 'package:tele_book/feature/book/service/book_service.dart'; - -class BookStore extends ChangeNotifier { - final BookService _bookService; - final List books = []; - BookSort sort = BookSort( - order: BookSortOrder.desc, - type: BookSortType.lastCreatedAt, - ); - final int pageSize = 20; - - int _currentPage = 1; - String? _nameFilter; - bool _isLoading = false; - bool _hasMore = true; - - StreamSubscription? _bookSubscription; - - BookStore(this._bookService) { - _loadFirstPage(); - } - - bool get isLoading => _isLoading; - - bool get hasMore => _hasMore; - - /// 加载第一页 - void _loadFirstPage() { - _currentPage = 1; - books.clear(); - _watchBooks(page: _currentPage); - } - - /// 订阅书籍列表流 - void _watchBooks({int page = 1}) { - _bookSubscription?.cancel(); - _bookSubscription = _bookService - .watchBooks( - page: page, - pageSize: pageSize, - name: _nameFilter, - sort: sort, - ) - .listen( - (bookListVo) { - if (page == 1) { - // 第一页:清空并重新设置 - books.clear(); - books.addAll(bookListVo.bookVos); - } else { - // 后续页:追加 - books.addAll(bookListVo.bookVos); - } - - // 判断是否还有更多数据 - _hasMore = bookListVo.bookVos.length >= pageSize; - _isLoading = false; - notifyListeners(); - }, - onError: (e) { - _isLoading = false; - notifyListeners(); - }, - ); - } - - /// 加载下一页 - Future loadMore() async { - if (_isLoading || !_hasMore) return; - _isLoading = true; - notifyListeners(); - - try { - // 用非 watch 的 fetch 方法直接获取下一页,避免重复订阅 - final bookListVo = await _bookService.fetchBooks( - page: _currentPage + 1, - pageSize: pageSize, - name: _nameFilter, - sort: sort, - ); - - // 追加下一页数据 - books.addAll(bookListVo.bookVos); - - if (bookListVo.bookVos.isNotEmpty) { - _currentPage += 1; - } - - // 判断是否还有更多 - _hasMore = bookListVo.bookVos.length >= pageSize; - } catch (e) { - debugPrint('加载更多失败: $e'); - } finally { - _isLoading = false; - notifyListeners(); - } - } - - /// 搜索(重置分页) - Future search(String? name) async { - _nameFilter = name; - _loadFirstPage(); // 重载第一页 - } - - Future updateReadProgress(int bookId, int progress) async { - final book = books.firstWhere((b) => b.book.id == bookId).book; - final updatedBook = book.copyWith(currentPage: progress); - await _bookService.updateBook(updatedBook); - } - - void updateSort(BookSort newSort) { - if (sort == newSort) return; - sort = newSort; - - _loadFirstPage(); // 重载第一页 - } - - @override - void dispose() { - _bookSubscription?.cancel(); - super.dispose(); - } -} diff --git a/lib/feature/book/ui/provider/book_form_provider.dart b/lib/feature/book/ui/provider/book_form_provider.dart new file mode 100644 index 0000000..b4aabc1 --- /dev/null +++ b/lib/feature/book/ui/provider/book_form_provider.dart @@ -0,0 +1,130 @@ +import 'package:flutter/cupertino.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tele_book/common/config/global_config.dart'; +import 'package:tele_book/core/db/app_database.dart'; +import 'package:tele_book/feature/book/repository/book_repository.dart'; + +part 'book_form_provider.freezed.dart'; + +part 'book_form_provider.g.dart'; + +@freezed +abstract class BookFormState with _$BookFormState { + const factory BookFormState({ + required String title, + required List imagePaths, + }) = _BookFormState; +} + +@freezed +abstract class BookFormPath with _$BookFormPath { + const factory BookFormPath({ + required String parentPath, + required String subPath, + }) = _BookFormPath; + + const BookFormPath._(); + + String get fullPath => '$parentPath/$subPath'; +} + +@riverpod +class BookForm extends _$BookForm { + late final TextEditingController titleController; + + @override + FutureOr build(int bookId) async { + ref.onDispose(() => titleController.dispose()); + final book = await ref + .read(databaseProvider) + .bookLocalDatasource + .getById(bookId); + if (book == null) throw Exception('书籍不存在'); + + titleController = TextEditingController(text: book.name); + + titleController.addListener(() { + if (state.hasValue) { + state = AsyncValue.data( + state.value!.copyWith(title: titleController.text), + ); + } + }); + final imagePaths = book.localSubPaths + .map( + (subPath) => BookFormPath( + parentPath: GlobalConfig.booksDir.path, + subPath: subPath, + ), + ) + .toList(); + return BookFormState(title: book.name, imagePaths: imagePaths); + } + + Future deleteImage(BookFormPath path) async { + if (!state.hasValue) return; + final current = state.requireValue; + + final updatePaths = current.imagePaths.where((p) => p != path).toList(); + state = AsyncValue.data(current.copyWith(imagePaths: updatePaths)); + } + + void reorderImages(int oldIndex, int newIndex) { + if (!state.hasValue) return; + final current = state.requireValue; + + if (oldIndex < 0 || oldIndex >= current.imagePaths.length) return; + if (newIndex < 0 || newIndex > current.imagePaths.length) return; + if (oldIndex == newIndex) return; + + if (oldIndex < newIndex) { + newIndex -= 1; + } + + // 创建一个可变的新副本进行操作 + final updatedPaths = List.from(current.imagePaths); + final item = updatedPaths.removeAt(oldIndex); + updatedPaths.insert(newIndex, item); + + // 重新赋给 state 触发 UI 刷新 + state = AsyncData(current.copyWith(imagePaths: updatedPaths)); + } +} + +@riverpod +class BookFormSubmit extends _$BookFormSubmit { + @override + FutureOr build() => null; + + Future submit({ + required int bookId, + required String title, + required List imagePaths, + }) async { + state = const AsyncLoading(); + + state = await AsyncValue.guard(() async { + final book = await ref + .read(databaseProvider) + .bookLocalDatasource + .getById(bookId); + if (book == null) return; + final newTitle = title; + final newSubPaths = imagePaths.map((path) => path.subPath).toList(); + final updatedBook = book.copyWith( + name: newTitle, + localSubPaths: newSubPaths, + ); + await ref + .read(databaseProvider) + .bookLocalDatasource + .updateBook(updatedBook); + + // 重新生成封面和预览图 + await ref.read(bookRepositoryProvider).regenerateImages(updatedBook); + }); + + return !state.hasError; + } +} diff --git a/lib/feature/book/ui/provider/book_form_provider.freezed.dart b/lib/feature/book/ui/provider/book_form_provider.freezed.dart new file mode 100644 index 0000000..bf19497 --- /dev/null +++ b/lib/feature/book/ui/provider/book_form_provider.freezed.dart @@ -0,0 +1,540 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'book_form_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$BookFormState { + + String get title; List get imagePaths; +/// Create a copy of BookFormState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$BookFormStateCopyWith get copyWith => _$BookFormStateCopyWithImpl(this as BookFormState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is BookFormState&&(identical(other.title, title) || other.title == title)&&const DeepCollectionEquality().equals(other.imagePaths, imagePaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,title,const DeepCollectionEquality().hash(imagePaths)); + +@override +String toString() { + return 'BookFormState(title: $title, imagePaths: $imagePaths)'; +} + + +} + +/// @nodoc +abstract mixin class $BookFormStateCopyWith<$Res> { + factory $BookFormStateCopyWith(BookFormState value, $Res Function(BookFormState) _then) = _$BookFormStateCopyWithImpl; +@useResult +$Res call({ + String title, List imagePaths +}); + + + + +} +/// @nodoc +class _$BookFormStateCopyWithImpl<$Res> + implements $BookFormStateCopyWith<$Res> { + _$BookFormStateCopyWithImpl(this._self, this._then); + + final BookFormState _self; + final $Res Function(BookFormState) _then; + +/// Create a copy of BookFormState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? title = null,Object? imagePaths = null,}) { + return _then(_self.copyWith( +title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable +as String,imagePaths: null == imagePaths ? _self.imagePaths : imagePaths // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [BookFormState]. +extension BookFormStatePatterns on BookFormState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _BookFormState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _BookFormState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _BookFormState value) $default,){ +final _that = this; +switch (_that) { +case _BookFormState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _BookFormState value)? $default,){ +final _that = this; +switch (_that) { +case _BookFormState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String title, List imagePaths)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _BookFormState() when $default != null: +return $default(_that.title,_that.imagePaths);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String title, List imagePaths) $default,) {final _that = this; +switch (_that) { +case _BookFormState(): +return $default(_that.title,_that.imagePaths);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String title, List imagePaths)? $default,) {final _that = this; +switch (_that) { +case _BookFormState() when $default != null: +return $default(_that.title,_that.imagePaths);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _BookFormState implements BookFormState { + const _BookFormState({required this.title, required final List imagePaths}): _imagePaths = imagePaths; + + +@override final String title; + final List _imagePaths; +@override List get imagePaths { + if (_imagePaths is EqualUnmodifiableListView) return _imagePaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_imagePaths); +} + + +/// Create a copy of BookFormState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$BookFormStateCopyWith<_BookFormState> get copyWith => __$BookFormStateCopyWithImpl<_BookFormState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _BookFormState&&(identical(other.title, title) || other.title == title)&&const DeepCollectionEquality().equals(other._imagePaths, _imagePaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,title,const DeepCollectionEquality().hash(_imagePaths)); + +@override +String toString() { + return 'BookFormState(title: $title, imagePaths: $imagePaths)'; +} + + +} + +/// @nodoc +abstract mixin class _$BookFormStateCopyWith<$Res> implements $BookFormStateCopyWith<$Res> { + factory _$BookFormStateCopyWith(_BookFormState value, $Res Function(_BookFormState) _then) = __$BookFormStateCopyWithImpl; +@override @useResult +$Res call({ + String title, List imagePaths +}); + + + + +} +/// @nodoc +class __$BookFormStateCopyWithImpl<$Res> + implements _$BookFormStateCopyWith<$Res> { + __$BookFormStateCopyWithImpl(this._self, this._then); + + final _BookFormState _self; + final $Res Function(_BookFormState) _then; + +/// Create a copy of BookFormState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? title = null,Object? imagePaths = null,}) { + return _then(_BookFormState( +title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable +as String,imagePaths: null == imagePaths ? _self._imagePaths : imagePaths // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc +mixin _$BookFormPath { + + String get parentPath; String get subPath; +/// Create a copy of BookFormPath +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$BookFormPathCopyWith get copyWith => _$BookFormPathCopyWithImpl(this as BookFormPath, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is BookFormPath&&(identical(other.parentPath, parentPath) || other.parentPath == parentPath)&&(identical(other.subPath, subPath) || other.subPath == subPath)); +} + + +@override +int get hashCode => Object.hash(runtimeType,parentPath,subPath); + +@override +String toString() { + return 'BookFormPath(parentPath: $parentPath, subPath: $subPath)'; +} + + +} + +/// @nodoc +abstract mixin class $BookFormPathCopyWith<$Res> { + factory $BookFormPathCopyWith(BookFormPath value, $Res Function(BookFormPath) _then) = _$BookFormPathCopyWithImpl; +@useResult +$Res call({ + String parentPath, String subPath +}); + + + + +} +/// @nodoc +class _$BookFormPathCopyWithImpl<$Res> + implements $BookFormPathCopyWith<$Res> { + _$BookFormPathCopyWithImpl(this._self, this._then); + + final BookFormPath _self; + final $Res Function(BookFormPath) _then; + +/// Create a copy of BookFormPath +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? parentPath = null,Object? subPath = null,}) { + return _then(_self.copyWith( +parentPath: null == parentPath ? _self.parentPath : parentPath // ignore: cast_nullable_to_non_nullable +as String,subPath: null == subPath ? _self.subPath : subPath // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [BookFormPath]. +extension BookFormPathPatterns on BookFormPath { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _BookFormPath value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _BookFormPath() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _BookFormPath value) $default,){ +final _that = this; +switch (_that) { +case _BookFormPath(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _BookFormPath value)? $default,){ +final _that = this; +switch (_that) { +case _BookFormPath() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String parentPath, String subPath)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _BookFormPath() when $default != null: +return $default(_that.parentPath,_that.subPath);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String parentPath, String subPath) $default,) {final _that = this; +switch (_that) { +case _BookFormPath(): +return $default(_that.parentPath,_that.subPath);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String parentPath, String subPath)? $default,) {final _that = this; +switch (_that) { +case _BookFormPath() when $default != null: +return $default(_that.parentPath,_that.subPath);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _BookFormPath extends BookFormPath { + const _BookFormPath({required this.parentPath, required this.subPath}): super._(); + + +@override final String parentPath; +@override final String subPath; + +/// Create a copy of BookFormPath +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$BookFormPathCopyWith<_BookFormPath> get copyWith => __$BookFormPathCopyWithImpl<_BookFormPath>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _BookFormPath&&(identical(other.parentPath, parentPath) || other.parentPath == parentPath)&&(identical(other.subPath, subPath) || other.subPath == subPath)); +} + + +@override +int get hashCode => Object.hash(runtimeType,parentPath,subPath); + +@override +String toString() { + return 'BookFormPath(parentPath: $parentPath, subPath: $subPath)'; +} + + +} + +/// @nodoc +abstract mixin class _$BookFormPathCopyWith<$Res> implements $BookFormPathCopyWith<$Res> { + factory _$BookFormPathCopyWith(_BookFormPath value, $Res Function(_BookFormPath) _then) = __$BookFormPathCopyWithImpl; +@override @useResult +$Res call({ + String parentPath, String subPath +}); + + + + +} +/// @nodoc +class __$BookFormPathCopyWithImpl<$Res> + implements _$BookFormPathCopyWith<$Res> { + __$BookFormPathCopyWithImpl(this._self, this._then); + + final _BookFormPath _self; + final $Res Function(_BookFormPath) _then; + +/// Create a copy of BookFormPath +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? parentPath = null,Object? subPath = null,}) { + return _then(_BookFormPath( +parentPath: null == parentPath ? _self.parentPath : parentPath // ignore: cast_nullable_to_non_nullable +as String,subPath: null == subPath ? _self.subPath : subPath // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/lib/feature/book/ui/provider/book_form_provider.g.dart b/lib/feature/book/ui/provider/book_form_provider.g.dart new file mode 100644 index 0000000..3aea42c --- /dev/null +++ b/lib/feature/book/ui/provider/book_form_provider.g.dart @@ -0,0 +1,143 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'book_form_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(BookForm) +final bookFormProvider = BookFormFamily._(); + +final class BookFormProvider + extends $AsyncNotifierProvider { + BookFormProvider._({ + required BookFormFamily super.from, + required int super.argument, + }) : super( + retry: null, + name: r'bookFormProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$bookFormHash(); + + @override + String toString() { + return r'bookFormProvider' + '' + '($argument)'; + } + + @$internal + @override + BookForm create() => BookForm(); + + @override + bool operator ==(Object other) { + return other is BookFormProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$bookFormHash() => r'2407f1247c5dcb1db39c18a8efb0dee929528e93'; + +final class BookFormFamily extends $Family + with + $ClassFamilyOverride< + BookForm, + AsyncValue, + BookFormState, + FutureOr, + int + > { + BookFormFamily._() + : super( + retry: null, + name: r'bookFormProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + BookFormProvider call(int bookId) => + BookFormProvider._(argument: bookId, from: this); + + @override + String toString() => r'bookFormProvider'; +} + +abstract class _$BookForm extends $AsyncNotifier { + late final _$args = ref.$arg as int; + int get bookId => _$args; + + FutureOr build(int bookId); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, BookFormState>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, BookFormState>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} + +@ProviderFor(BookFormSubmit) +final bookFormSubmitProvider = BookFormSubmitProvider._(); + +final class BookFormSubmitProvider + extends $AsyncNotifierProvider { + BookFormSubmitProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'bookFormSubmitProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$bookFormSubmitHash(); + + @$internal + @override + BookFormSubmit create() => BookFormSubmit(); +} + +String _$bookFormSubmitHash() => r'fd7d09a1cb4d7435ae908d34fbb02868792da325'; + +abstract class _$BookFormSubmit extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/feature/book/ui/provider/book_page_provider.dart b/lib/feature/book/ui/provider/book_page_provider.dart new file mode 100644 index 0000000..644d8ec --- /dev/null +++ b/lib/feature/book/ui/provider/book_page_provider.dart @@ -0,0 +1,79 @@ +import 'package:flutter/cupertino.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tele_book/common/config/global_config.dart'; +import 'package:tele_book/core/db/app_database.dart'; +import 'package:tele_book/feature/book/ui/provider/book_provider.dart'; + +part 'book_page_provider.g.dart'; + +class BookPageState { + final BookTableData book; + final List paths; + final int currentPage; + final bool isShowBar; + + BookPageState({ + required this.book, + required this.paths, + required this.currentPage, + required this.isShowBar, + }); + + BookPageState copyWith({ + BookTableData? book, + List? paths, + int? currentPage, + bool? isShowBar, + }) { + return BookPageState( + book: book ?? this.book, + paths: paths ?? this.paths, + currentPage: currentPage ?? this.currentPage, + isShowBar: isShowBar ?? this.isShowBar, + ); + } +} + +@riverpod +class BookPage extends _$BookPage { + late PageController pageController; + + @override + BookPageState build(int bookId) { + final book = ref + .watch(bookListProvider) + .value + ?.bookVos + .where((e) => e.book.id == bookId) + .first + .book; + if (book == null) { + throw Exception("Book not found"); + } + + final previewPaths = book.previewSubPaths; + final fullPaths = (previewPaths != null && previewPaths.isNotEmpty) + ? previewPaths.map((e) => GlobalConfig.resolveBookPath(e)).toList() + : book.localSubPaths + .map((e) => GlobalConfig.resolveBookPath(e)) + .toList(); + return BookPageState( + book: book, + paths: fullPaths, + currentPage: book.currentPage, + isShowBar: false, + ); + } + + void initController(PageController controller) { + pageController = controller; + } + + void onPageChanged(int index) { + state = state.copyWith(currentPage: index); + } + + void toggleBar() { + state = state.copyWith(isShowBar: !state.isShowBar); + } +} diff --git a/lib/feature/book/ui/provider/book_page_provider.g.dart b/lib/feature/book/ui/provider/book_page_provider.g.dart new file mode 100644 index 0000000..1c382ba --- /dev/null +++ b/lib/feature/book/ui/provider/book_page_provider.g.dart @@ -0,0 +1,107 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'book_page_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(BookPage) +final bookPageProvider = BookPageFamily._(); + +final class BookPageProvider + extends $NotifierProvider { + BookPageProvider._({ + required BookPageFamily super.from, + required int super.argument, + }) : super( + retry: null, + name: r'bookPageProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$bookPageHash(); + + @override + String toString() { + return r'bookPageProvider' + '' + '($argument)'; + } + + @$internal + @override + BookPage create() => BookPage(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(BookPageState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } + + @override + bool operator ==(Object other) { + return other is BookPageProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$bookPageHash() => r'74e8ded41f495fbfaf78d9cdbda58fa537da26a7'; + +final class BookPageFamily extends $Family + with + $ClassFamilyOverride< + BookPage, + BookPageState, + BookPageState, + BookPageState, + int + > { + BookPageFamily._() + : super( + retry: null, + name: r'bookPageProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + BookPageProvider call(int bookId) => + BookPageProvider._(argument: bookId, from: this); + + @override + String toString() => r'bookPageProvider'; +} + +abstract class _$BookPage extends $Notifier { + late final _$args = ref.$arg as int; + int get bookId => _$args; + + BookPageState build(int bookId); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + BookPageState, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} diff --git a/lib/feature/book/ui/provider/book_provider.dart b/lib/feature/book/ui/provider/book_provider.dart new file mode 100644 index 0000000..0b6a9f6 --- /dev/null +++ b/lib/feature/book/ui/provider/book_provider.dart @@ -0,0 +1,274 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tele_book/common/config/global_config.dart'; +import 'package:tele_book/core/db/app_database.dart'; +import 'package:tele_book/feature/book/enum/book_sort.dart'; +import 'package:tele_book/feature/book/model/state/book_list_state.dart'; +import 'package:tele_book/feature/book/repository/book_repository.dart'; + +part 'book_provider.g.dart'; + +final booksProvider = StreamProvider.autoDispose>((ref){ + final bookRepository = ref.watch(bookRepositoryProvider); + return bookRepository.watchAllBooks(); +}); + +class _BookListQueryState { + final String name; + final BookSort? sort; + final int page; + final int pageSize; + + const _BookListQueryState({ + this.name = '', + this.sort, + this.page = 1, + this.pageSize = 20, + }); + + _BookListQueryState copyWith({ + String? name, + BookSort? sort, + int? page, + int? pageSize, + }) { + return _BookListQueryState( + name: name ?? this.name, + sort: sort ?? this.sort, + page: page ?? this.page, + pageSize: pageSize ?? this.pageSize, + ); + } +} + +class _BookListUiState { + final bool isSelectionMode; + final Set selectedBookIds; + final BookLayout layout; + + const _BookListUiState({ + this.isSelectionMode = false, + this.selectedBookIds = const {}, + this.layout = BookLayout.list, + }); + + _BookListUiState copyWith({ + bool? isSelectionMode, + Set? selectedBookIds, + BookLayout? layout, + }) { + return _BookListUiState( + isSelectionMode: isSelectionMode ?? this.isSelectionMode, + selectedBookIds: selectedBookIds ?? this.selectedBookIds, + layout: layout ?? this.layout, + ); + } +} + +class _BookListQueryNotifier extends Notifier<_BookListQueryState> { + @override + _BookListQueryState build() => const _BookListQueryState(); + + void updateSearch(String name) { + state = state.copyWith(name: name, page: 1); + } + + void updateSort(BookSort? sort) { + state = state.copyWith(sort: sort, page: 1); + } + + void loadNextPage() { + state = state.copyWith(page: state.page + 1); + } +} + +final bookListQueryProvider = + NotifierProvider<_BookListQueryNotifier, _BookListQueryState>( + _BookListQueryNotifier.new, + ); + +class _BookListUiNotifier extends Notifier<_BookListUiState> { + @override + _BookListUiState build() => const _BookListUiState(); + + void enterSelectionMode(BookTableData firstBook) { + state = state.copyWith(isSelectionMode: true, selectedBookIds: {firstBook.id}); + } + + void exitSelectionMode() { + state = state.copyWith(isSelectionMode: false, selectedBookIds: {}); + } + + void toggleSelection(int bookId) { + final updatedIds = Set.from(state.selectedBookIds); + if (updatedIds.contains(bookId)) { + updatedIds.remove(bookId); + if (updatedIds.isEmpty) { + state = state.copyWith(isSelectionMode: false, selectedBookIds: {}); + return; + } + } else { + updatedIds.add(bookId); + } + + state = state.copyWith(selectedBookIds: updatedIds); + } + + void selectAll(Iterable ids) { + state = state.copyWith( + isSelectionMode: true, + selectedBookIds: ids.toSet(), + ); + } + + void toggleSelections(Set bookIds) { + if (bookIds.isEmpty) { + state = state.copyWith(isSelectionMode: false, selectedBookIds: {}); + return; + } + state = state.copyWith(selectedBookIds: bookIds); + } + + void toggleLayout() { + final nextLayout = + state.layout == BookLayout.list ? BookLayout.grid : BookLayout.list; + state = state.copyWith(layout: nextLayout); + } + + void clearSelectedIds(Iterable ids) { + final updated = Set.from(state.selectedBookIds) + ..removeAll(ids); + if (updated.isEmpty) { + state = state.copyWith(isSelectionMode: false, selectedBookIds: {}); + return; + } + state = state.copyWith(selectedBookIds: updated); + } +} + +final bookListUiProvider = NotifierProvider<_BookListUiNotifier, _BookListUiState>( + _BookListUiNotifier.new, +); + +@riverpod +class BookList extends _$BookList { + @override + Future build() async { + final booksAsync = ref.watch(booksProvider); + final query = ref.watch(bookListQueryProvider); + + if (booksAsync.hasError) { + throw booksAsync.error!; + } + final books = + (booksAsync.value ?? await ref.watch(booksProvider.future)) ?? + const []; + final keyword = query.name.toLowerCase(); + + final filtered = books.where((book) { + if (keyword.isEmpty) return true; + return book.name.toLowerCase().contains(keyword); + }).toList(); + + if (query.sort != null) { + filtered.sort((a, b) { + final cmp = switch (query.sort!.type) { + BookSortType.title => + a.name.toLowerCase().compareTo(b.name.toLowerCase()), + BookSortType.lastCreatedAt => a.createdAt.compareTo(b.createdAt), + }; + return query.sort!.order == BookSortOrder.asc ? cmp : -cmp; + }); + } else { + filtered.sort((a, b) => b.createdAt.compareTo(a.createdAt)); + } + + final visibleCount = query.page * query.pageSize; + final visibleBooks = filtered.take(visibleCount).toList(); + + final bookVos = visibleBooks.map((book) { + final coverPath = book.coverSubPath != null + ? GlobalConfig.resolveBookPath(book.coverSubPath!) + : book.localSubPaths.isNotEmpty + ? GlobalConfig.resolveBookPath(book.localSubPaths.first) + : ''; + return BookListItemVo(book: book, coverImagePath: coverPath); + }).toList(); + + return BookListState( + bookVos: bookVos, + hasMore: filtered.length > visibleBooks.length, + name: query.name, + sort: query.sort, + isLoadingMore: false, + ); + } + + Future loadNextPage() async { + if (!state.hasValue || state.isLoading) return; + final current = state.requireValue; + if (!current.hasMore || current.isLoadingMore) return; + ref.read(bookListQueryProvider.notifier).loadNextPage(); + } + + Future updateSearch(String name) async { + ref.read(bookListQueryProvider.notifier).updateSearch(name); + } + + Future updateSort(BookSort? sort) async { + ref.read(bookListQueryProvider.notifier).updateSort(sort); + } + + // ➡️ 进入多选模式并勾选第一本书 + void enterSelectionMode(BookTableData firstBook) { + ref.read(bookListUiProvider.notifier).enterSelectionMode(firstBook); + } + + // ➡️ 退出多选模式 + void exitSelectionMode() { + ref.read(bookListUiProvider.notifier).exitSelectionMode(); + } + + // ➡️ 切换某本书的选中状态 + void toggleSelection(int bookId) { + ref.read(bookListUiProvider.notifier).toggleSelection(bookId); + } + + // 批量选择 + void toggleSelections(Set bookIds) { + ref.read(bookListUiProvider.notifier).toggleSelections(bookIds); + + } + + // ➡️ 全选 + void selectAll() { + final ids = + state.asData?.value.bookVos.map((vo) => vo.book.id) ?? const []; + ref.read(bookListUiProvider.notifier).selectAll(ids); + } + + // ➡️ 切换布局 + void toggleLayout() { + ref.read(bookListUiProvider.notifier).toggleLayout(); + } + + // ➡️ 删除单本书并刷新列表 + Future deleteBook(int bookId) async { + final bookRepository = ref.read(bookRepositoryProvider); + await bookRepository.deleteBook(bookId); + ref.read(bookListUiProvider.notifier).clearSelectedIds([bookId]); + } + + // ➡️ 批量删除选中书籍并刷新列表 + Future deleteSelected() async { + final selectedIds = Set.from( + ref.read(bookListUiProvider).selectedBookIds, + ); + final bookRepository = ref.read(bookRepositoryProvider); + + for (final id in selectedIds) { + await bookRepository.deleteBook(id); + } + ref.read(bookListUiProvider.notifier).exitSelectionMode(); + } +} diff --git a/lib/feature/book/ui/provider/book_provider.g.dart b/lib/feature/book/ui/provider/book_provider.g.dart new file mode 100644 index 0000000..065c5e2 --- /dev/null +++ b/lib/feature/book/ui/provider/book_provider.g.dart @@ -0,0 +1,54 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'book_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(BookList) +final bookListProvider = BookListProvider._(); + +final class BookListProvider + extends $AsyncNotifierProvider { + BookListProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'bookListProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$bookListHash(); + + @$internal + @override + BookList create() => BookList(); +} + +String _$bookListHash() => r'67c643b9b8edf49a19d2f6c2f8dfc07755d1c08f'; + +abstract class _$BookList extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, BookListState>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, BookListState>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/feature/book/ui/view/book_form_view.dart b/lib/feature/book/ui/view/book_form_view.dart index 21e61da..05a9f96 100644 --- a/lib/feature/book/ui/view/book_form_view.dart +++ b/lib/feature/book/ui/view/book_form_view.dart @@ -1,84 +1,151 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tele_book/common/widget/error_widget.dart'; import 'package:tele_book/common/widget/local_image_widget.dart'; import 'package:tele_book/core/db/app_database.dart'; -import 'package:tele_book/feature/book/ui/viewmodel/book_form_viewmodel.dart'; +import 'package:tele_book/feature/book/ui/provider/book_form_provider.dart'; -class BookFormView extends StatelessWidget { +class BookFormView extends ConsumerWidget { final BookTableData book; const BookFormView({super.key, required this.book}); @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => BookFormViewmodel(book, context.read()), - child: _BookFormViewContent(), - ); - } -} + Widget build(BuildContext context, WidgetRef ref) { + final formState = ref.watch(bookFormProvider(book.id)); + final submitState = ref.watch(bookFormSubmitProvider); -class _BookFormViewContent extends StatelessWidget { - @override - Widget build(BuildContext context) { - final vm = context.watch(); - return Scaffold( - appBar: AppBar(title: Text("编辑书籍")), - body: Column( - children: [ - Padding( - padding: EdgeInsets.all(16), - child: TextField( - controller: vm.titleController, - decoration: InputDecoration(labelText: "书籍名称"), - ), + ref.listen(bookFormSubmitProvider, (previous, next) { + if (previous?.isLoading == true && next.isLoading == false) { + if (next.error != null) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text("保存失败: ${next.error}"))); + } else { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text("保存成功"))); + } + } + }); + + return FScaffold( + header: FHeader.nested( + title: Text("编辑书籍"), + prefixes: [ + FHeaderAction.back( + onPress: () { + context.pop(); + }, ), - Expanded( - child: ReorderableListView.builder( - onReorder: vm.reorderImages, - itemCount: vm.imagePaths.length, - itemBuilder: (context, index) { - final imagePath = vm.imagePaths[index]; - return Padding( - key: ObjectKey(imagePath), - padding: EdgeInsets.symmetric(vertical: 8.0), - child: ListTile( - leading: LocalImageWidget(imagePath: imagePath.fullPath), - title: Text("图片 ${index + 1}"), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(Icons.delete), - onPressed: () { - vm.deleteImage(imagePath); - }, - ), - ReorderableDragStartListener( - index: index, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: Icon(Icons.drag_handle), + ], + ), + child: formState.when( + error: (error, stack) => Center( + child: CustomErrorWidget( + errorMessage: error.toString(), + stackTrace: stack, + ), + ), + loading: () => Center(child: CircularProgressIndicator()), + data: (formState) { + final formNotifier = ref.read(bookFormProvider(book.id).notifier); + final isSubmitting = submitState.isLoading; + + return Padding( + padding: .all(16), + child: Column( + crossAxisAlignment: .start, + children: [ + FTextFormField( + control: FTextFieldControl.managed( + controller: formNotifier.titleController, + ), + label: Text("书籍名称"), + hint: "请输入书籍名称", + ), + const SizedBox(height: 16), + Text( + "图片排序", + style: context.theme.typography.body.xs.copyWith( + fontWeight: .w500, + ), + ), + const SizedBox(height: 8), + Expanded( + child: ReorderableListView.builder( + onReorderItem: formNotifier.reorderImages, + itemCount: formState.imagePaths.length, + itemBuilder: (context, index) { + final imagePath = formState.imagePaths[index]; + return Padding( + key: ObjectKey(imagePath), + padding: EdgeInsets.symmetric(vertical: 8.0), + child: FItem( + prefix: LocalImageWidget( + imagePath: imagePath.fullPath, + ), + title: Text("图片 ${index + 1}"), + suffix: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FButton.icon( + variant: .ghost, + onPress: () { + formNotifier.deleteImage(imagePath); + }, + child: Icon( + Icons.delete, + color: context.theme.colors.destructive, + ), + ), + ReorderableDragStartListener( + index: index, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Icon(Icons.drag_handle), + ), + ), + ], ), ), - ], - ), + ); + }, ), - ); - }, - ), - ), - Container( - padding: EdgeInsets.all(16), - width: double.infinity, - child: FilledButton( - onPressed: () { - vm.updateBook(context); - }, - child: Text("保存"), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 16), + + child: FButton( + onPress: isSubmitting + ? null + : () async { + final success = await ref + .read(bookFormSubmitProvider.notifier) + .submit( + bookId: book.id, + title: formState.title, + imagePaths: formState.imagePaths, + ); + if (success && context.mounted) { + Navigator.pop(context); + } + }, + child: isSubmitting + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text("保存"), + ), + ), + ], ), - ), - ], + ); + }, ), ); } diff --git a/lib/feature/book/ui/view/book_list_view.dart b/lib/feature/book/ui/view/book_list_view.dart new file mode 100644 index 0000000..454bf51 --- /dev/null +++ b/lib/feature/book/ui/view/book_list_view.dart @@ -0,0 +1,724 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:responsive_framework/responsive_framework.dart'; +import 'package:tele_book/common/widget/f_adaptive_dialog.dart'; +import 'package:tele_book/common/widget/local_image_widget.dart'; +import 'package:tele_book/core/db/app_database.dart'; +import 'package:tele_book/core/route/app_route.dart'; +import 'package:tele_book/feature/book/enum/book_menu_type.dart'; +import 'package:tele_book/feature/book/enum/book_sort.dart'; +import 'package:tele_book/feature/book/model/state/book_list_state.dart'; +import 'package:tele_book/feature/book/ui/provider/book_provider.dart'; +import 'package:tele_book/feature/export/ui/view/export_batch_form_view.dart'; +import 'package:tele_book/feature/export/ui/view/export_single_form_view.dart'; + +// ── 根页面 ───────────────────────────────────────────────────── + +class BookListView extends ConsumerStatefulWidget { + const BookListView({super.key}); + + @override + ConsumerState createState() => _BookListViewState(); +} + +class _BookListViewState extends ConsumerState { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 200) { + ref.read(bookListProvider.notifier).loadNextPage(); + } + } + + @override + Widget build(BuildContext context) { + final bookState = ref.watch(bookListProvider); + final uiState = ref.watch(bookListUiProvider); + final isSelectionMode = uiState.isSelectionMode; + final layout = uiState.layout; + + return PopScope( + canPop: !isSelectionMode, + onPopInvokedWithResult: (didPop, _) { + if (!didPop && isSelectionMode) { + ref.read(bookListProvider.notifier).exitSelectionMode(); + } + }, + child: FScaffold( + header: isSelectionMode + ? _buildSelectionAppBar(context) + : _buildNormalAppBar(context), + footer: isSelectionMode ? _buildSelectionBottomBar(context) : null, + child: bookState.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, _) => Center(child: Text('加载失败: $e')), + data: (state) { + if (state.bookVos.isEmpty) return _buildEmpty(); + return layout == BookLayout.list + ? _BookListContent( + bookVos: state.bookVos, + isLoadingMore: state.isLoadingMore, + scrollController: _scrollController, + ) + : _BookGridContent( + bookVos: state.bookVos, + isLoadingMore: state.isLoadingMore, + scrollController: _scrollController, + ); + }, + ), + ), + ); + } + + Widget _buildEmpty() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.library_books_outlined, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text('暂无书籍', style: TextStyle(fontSize: 16, color: Colors.grey[600])), + ], + ), + ); + } + + FHeader _buildNormalAppBar(BuildContext context) { + final bookVos = ref.watch( + bookListProvider.select((s) => s.value?.bookVos ?? []), + ); + final state = ref.watch(bookListProvider).value; + + return FHeader( + title: const Text('书籍'), + suffixes: [ + SearchAnchor( + builder: (context, controller) => FHeaderAction( + onPress: controller.openView, + icon: const Icon(Icons.search), + ), + suggestionsBuilder: (context, searchController) { + final query = searchController.text.toLowerCase(); + final results = bookVos.where( + (vo) => vo.book.name.toLowerCase().contains(query), + ); + return results.map( + (vo) => FItem( + prefix: LocalImageWidget(imagePath: vo.coverImagePath), + title: Text(vo.book.name), + onPress: () { + searchController.closeView(vo.book.name); + context.push(AppRoute.bookPage, extra: vo.book); + }, + ), + ); + }, + ), + FHeaderAction( + onPress: () { + context.push(AppRoute.parseForm); + }, + icon: const Icon(Icons.add), + ), + _buildTopMenuButton(context, state), + ], + ); + } + + FHeader _buildSelectionAppBar(BuildContext context) { + final selectedCount = ref.watch( + bookListUiProvider.select((s) => s.selectedBookIds.length), + ); + final notifier = ref.read(bookListProvider.notifier); + + return FHeader.nested( + prefixes: [ + FHeaderAction( + icon: const Icon(Icons.close), + onPress: notifier.exitSelectionMode, + ), + ], + title: Text('已选 $selectedCount 本'), + suffixes: [ + FHeaderAction(onPress: notifier.selectAll, icon: const Text('全选')), + ], + ); + } + + Widget _buildSelectionBottomBar(BuildContext context) { + final selectedIds = ref.watch( + bookListUiProvider.select((s) => s.selectedBookIds), + ); + + return Padding( + padding: .all(8), + child: selectedIds.isNotEmpty + ? Row( + children: [ + FButton( + variant: .outline, + onPress: () => _onExportSelected(context), + prefix: Icon(FLucideIcons.move), + child: Text("批量导出"), + ), + SizedBox(width: 8), + FButton( + variant: .destructive, + onPress: () => _deleteSelected(context), + prefix: Icon(FLucideIcons.trash), + child: Text("批量删除"), + ), + ], + ) + : const SizedBox.shrink(), + ); + } + + Widget _buildTopMenuButton(BuildContext context, BookListState? state) { + return FPopoverMenu( + autofocus: true, + menuAnchor: .topRight, + childAnchor: .bottomRight, + menu: [ + .group( + children: [ + .item( + prefix: const Icon(FLucideIcons.arrowUp), + title: const Text('升序'), + suffix: state?.sort?.order == BookSortOrder.asc + ? const Icon(FLucideIcons.check, size: 16) + : null, + onPress: () => _onTopMenuSelected(BookTopMenuType.asc), + ), + .item( + prefix: const Icon(FLucideIcons.arrowDown), + title: const Text('降序'), + suffix: state?.sort?.order == BookSortOrder.desc + ? const Icon(FLucideIcons.check, size: 16) + : null, + onPress: () => _onTopMenuSelected(BookTopMenuType.desc), + ), + ], + ), + .group( + children: [ + .item( + prefix: const Icon(FLucideIcons.type), + title: const Text('按书名'), + suffix: state?.sort?.type == BookSortType.title + ? const Icon(FLucideIcons.check, size: 16) + : null, + onPress: () => _onTopMenuSelected(BookTopMenuType.name), + ), + .item( + prefix: const Icon(FLucideIcons.clock), + title: const Text('按添加时间'), + suffix: state?.sort?.type == BookSortType.lastCreatedAt + ? const Icon(FLucideIcons.check, size: 16) + : null, + onPress: () => _onTopMenuSelected(BookTopMenuType.lastCreatedAt), + ), + ], + ), + .group( + children: [ + .item( + prefix: const Icon(FLucideIcons.list), + title: const Text('列表视图'), + suffix: ref.watch(bookListUiProvider).layout == BookLayout.list + ? const Icon(FLucideIcons.check, size: 16) + : null, + onPress: () => _onTopMenuSelected(BookTopMenuType.list), + ), + .item( + prefix: const Icon(FLucideIcons.layoutGrid), + title: const Text('网格视图'), + suffix: ref.watch(bookListUiProvider).layout == BookLayout.grid + ? const Icon(FLucideIcons.check, size: 16) + : null, + onPress: () => _onTopMenuSelected(BookTopMenuType.grid), + ), + ], + ), + ], + builder: (_, controller, _) => FHeaderAction( + icon: const Icon(FLucideIcons.ellipsis), + onPress: controller.toggle, + ), + ); + } + + void _onTopMenuSelected(BookTopMenuType type) { + final notifier = ref.read(bookListProvider.notifier); + final current = ref.read(bookListProvider).value; + final currentLayout = ref.read(bookListUiProvider).layout; + final currentSort = + current?.sort ?? + BookSort(order: BookSortOrder.desc, type: BookSortType.lastCreatedAt); + + switch (type) { + case BookTopMenuType.asc: + notifier.updateSort(currentSort.copyWith(order: BookSortOrder.asc)); + case BookTopMenuType.desc: + notifier.updateSort(currentSort.copyWith(order: BookSortOrder.desc)); + case BookTopMenuType.name: + notifier.updateSort(currentSort.copyWith(type: BookSortType.title)); + case BookTopMenuType.lastCreatedAt: + notifier.updateSort( + currentSort.copyWith(type: BookSortType.lastCreatedAt), + ); + case BookTopMenuType.list: + if (currentLayout != BookLayout.list) notifier.toggleLayout(); + case BookTopMenuType.grid: + if (currentLayout != BookLayout.grid) notifier.toggleLayout(); + } + } + + void _onExportSelected(BuildContext context) { + final selectedIds = ref.read(bookListUiProvider).selectedBookIds; + final bookVos = ref.read(bookListProvider).value?.bookVos ?? []; + final selectedBooks = bookVos + .where((v) => selectedIds.contains(v.book.id)) + .toList(); + if (selectedBooks.isEmpty) return; + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ExportBatchFormView( + books: selectedBooks.map((v) => v.book).toList(), + ), + ), + ); + } + + Future _deleteSelected(BuildContext context) async { + final count = ref.read(bookListUiProvider).selectedBookIds.length; + final confirmed = await showFDialog( + context: context, + builder: (context, style, animate) => FAdaptiveDialog( + title: const Text('确认删除'), + body: Text('将删除选中的 $count 本书籍,删除后不可恢复,是否继续?'), + actions: [ + FButton( + variant: .destructive, + size: .sm, + onPress: () => Navigator.pop(context, true), + child: const Text('删除'), + ), + FButton( + variant: .outline, + size: .sm, + onPress: () => Navigator.pop(context, false), + child: const Text('取消'), + ), + ], + ), + ); + + final rootContext = Navigator.of(context).context; + if (confirmed == true && context.mounted) { + await ref.read(bookListProvider.notifier).deleteSelected(); + showFToast(context: rootContext, title: Text("删除成功")); + } + } +} + +// ── 列表布局 ─────────────────────────────────────────────────── + +class _BookListContent extends ConsumerWidget { + final List bookVos; + final bool isLoadingMore; + final ScrollController scrollController; + + const _BookListContent({ + required this.bookVos, + required this.isLoadingMore, + required this.scrollController, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isSelectionMode = ref.watch( + bookListUiProvider.select((s) => s.isSelectionMode), + ); + return isSelectionMode ? _selectList(ref) : _listView(ref); + } + + Widget _listView(WidgetRef ref) { + final count = bookVos.length + (isLoadingMore ? 1 : 0); + final notifier = ref.read(bookListProvider.notifier); + return FItemGroup.builder( + scrollController: scrollController, + count: count, + itemBuilder: (context, index) { + final bookVo = bookVos[index]; + if (index >= bookVos.length) { + return const SizedBox( + height: 48, + child: Center(child: CircularProgressIndicator(strokeWidth: 2)), + ); + } + return FTile( + title: Text(bookVo.book.name), + prefix: LocalImageWidget(imagePath: bookVo.coverImagePath), + subtitle: Text('共 ${bookVo.book.localSubPaths.length} 页'), + suffix: FPopoverMenu.tiles( + style: .delta(), + menu: [ + .group( + children: [ + for (var item in BookItemMenuType.values) + .tile( + variant: item == BookItemMenuType.delete + ? .destructive + : .primary, + title: Text(item.title), + prefix: Icon(item.icon), + onPress: () { + _onItemSelected(context, ref, item, bookVo); + }, + ), + ], + ), + ], + builder: (context, controller, child) { + return FButton.icon( + variant: .ghost, + child: Icon(FLucideIcons.moreHorizontal), + onPress: () { + controller.show(); + }, + ); + }, + ), + onPress: () { + context.push(AppRoute.bookPage, extra: bookVo.book); + }, + onLongPress: () { + notifier.enterSelectionMode(bookVo.book); + }, + ); + }, + ); + } + + Widget _selectList(WidgetRef ref) { + final count = bookVos.length + (isLoadingMore ? 1 : 0); + final notifier = ref.read(bookListProvider.notifier); + final selectedIds = ref.watch( + bookListUiProvider.select((s) => s.selectedBookIds), + ); + return FSelectTileGroup.builder( + control: FMultiValueControl.managed( + initial: selectedIds, + onChange: (value) { + notifier.toggleSelections(value); + }, + ), + scrollController: scrollController, + count: count, + tileBuilder: (context, index) { + final bookVo = bookVos[index]; + if (index >= bookVos.length) { + return .tile(title: Text("加载中"), value: -1); + } + + return .suffix( + title: Text(bookVo.book.name), + prefix: LocalImageWidget(imagePath: bookVo.coverImagePath), + value: bookVo.book.id, + ); + }, + ); + } + + void _onItemSelected( + BuildContext context, + WidgetRef ref, + BookItemMenuType type, + BookListItemVo bookVo, + ) { + switch (type) { + case BookItemMenuType.edit: + context.push(AppRoute.bookForm, extra: bookVo.book); + case BookItemMenuType.export: + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ExportSingleFormView(book: bookVo.book), + ), + ); + case BookItemMenuType.delete: + _confirmDelete(context, ref, bookVo); + } + } + + Future _confirmDelete( + BuildContext context, + WidgetRef ref, + BookListItemVo bookVo, + ) async { + final ok = await showFDialog( + context: context, + builder: (context, style, animate) => FAdaptiveDialog( + title: Text('确认删除'), + body: Text('删除《${bookVo.book.name}》后不可恢复,是否继续?'), + actions: [ + FButton( + size: .sm, + variant: .destructive, + child: const Text('确定'), + onPress: () => Navigator.of(context).pop(true), + ), + FButton( + variant: .outline, + size: .sm, + child: const Text('取消'), + onPress: () => Navigator.of(context).pop(false), + ), + ], + ), + ); + final rootContext = Navigator.of(context).context; + + if (ok == true) { + await ref.read(bookListProvider.notifier).deleteBook(bookVo.book.id); + showFToast(context: rootContext, title: Text("删除成功")); + } + } +} + +// ── 网格布局 ─────────────────────────────────────────────────── + +class _BookGridContent extends ConsumerWidget { + final List bookVos; + final bool isLoadingMore; + final ScrollController scrollController; + + const _BookGridContent({ + required this.bookVos, + required this.isLoadingMore, + required this.scrollController, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final itemCount = bookVos.length + (isLoadingMore ? 1 : 0); + return GridView.builder( + controller: scrollController, + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 180, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + mainAxisExtent: 240, + ), + itemCount: itemCount, + itemBuilder: (context, index) { + if (index == bookVos.length) { + return const Center(child: CircularProgressIndicator(strokeWidth: 2)); + } + return _BookGridTile(bookVo: bookVos[index]); + }, + ); + } +} + +class _BookGridTile extends ConsumerWidget { + final BookListItemVo bookVo; + + const _BookGridTile({required this.bookVo}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isSelectionMode = ref.watch( + bookListUiProvider.select((s) => s.isSelectionMode), + ); + final isSelected = ref.watch( + bookListUiProvider.select( + (s) => s.selectedBookIds.contains(bookVo.book.id), + ), + ); + final notifier = ref.read(bookListProvider.notifier); + + return GestureDetector( + onTap: () => isSelectionMode + ? notifier.toggleSelection(bookVo.book.id) + : context.push(AppRoute.bookPage, extra: bookVo.book), + onLongPress: () => isSelectionMode + ? _showItemMenu(context, ref) + : notifier.enterSelectionMode(bookVo.book), + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: bookVo.coverImagePath.isNotEmpty + ? Image.file( + File(bookVo.coverImagePath), + width: double.infinity, + fit: BoxFit.cover, + cacheWidth: 300, + errorBuilder: (_, __, ___) => _placeholder(), + ) + : _placeholder(), + ), + ), + const SizedBox(height: 4), + FItem( + title: Text(bookVo.book.name, maxLines: 2), + subtitle: Text('${bookVo.book.localSubPaths.length} 页'), + suffix: FPopoverMenu.tiles( + style: .delta(), + menu: [ + .group( + children: [ + for (var item in BookItemMenuType.values) + .tile( + variant: item == BookItemMenuType.delete + ? .destructive + : .primary, + title: Text(item.title), + prefix: Icon(item.icon), + onPress: () { + _onItemSelected(context, ref, item); + }, + ), + ], + ), + ], + builder: (context, controller, child) { + return FButton.icon( + variant: .ghost, + child: Icon(FLucideIcons.moreHorizontal), + onPress: () { + controller.show(); + }, + ); + }, + ), + ), + ], + ), + if (isSelectionMode) + Positioned( + top: 4, + right: 4, + child: Container( + decoration: BoxDecoration( + color: isSelected + ? Theme.of(context).colorScheme.primary + : Colors.white.withValues(alpha: 0.8), + shape: BoxShape.circle, + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: 2, + ), + ), + child: isSelected + ? const Icon(Icons.check, color: Colors.white, size: 18) + : const SizedBox(width: 18, height: 18), + ), + ), + ], + ), + ); + } + + Widget _placeholder() => Container( + width: double.infinity, + height: double.infinity, + color: Colors.grey[200], + child: Icon(Icons.book, color: Colors.grey[400], size: 40), + ); + + void _showItemMenu(BuildContext context, WidgetRef ref) { + showModalBottomSheet( + context: context, + builder: (_) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: BookItemMenuType.values + .map( + (type) => ListTile( + leading: Icon(type.icon), + title: Text(type.title), + onTap: () { + Navigator.pop(context); + _onItemSelected(context, ref, type); + }, + ), + ) + .toList(), + ), + ), + ); + } + + void _onItemSelected( + BuildContext context, + WidgetRef ref, + BookItemMenuType type, + ) { + switch (type) { + case BookItemMenuType.edit: + context.push(AppRoute.bookForm, extra: bookVo.book); + case BookItemMenuType.export: + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ExportSingleFormView(book: bookVo.book), + ), + ); + case BookItemMenuType.delete: + _confirmDelete(context, ref); + } + } + + Future _confirmDelete(BuildContext context, WidgetRef ref) async { + final ok = await showFDialog( + context: context, + builder: (context, style, animate) => FAdaptiveDialog( + title: const Text('确认删除'), + body: Text('删除《${bookVo.book.name}》后不可恢复,是否继续?'), + actions: [ + FButton( + variant: .destructive, + size: .sm, + onPress: () => Navigator.pop(context, true), + child: const Text('删除'), + ), + FButton( + size: .sm, + variant: .outline, + onPress: () => Navigator.pop(context, false), + child: const Text('取消'), + ), + ], + ), + ); + + final rootContext = Navigator.of(context).context; + if (ok == true && context.mounted) { + await ref.read(bookListProvider.notifier).deleteBook(bookVo.book.id); + showFToast(context: rootContext, title: Text("删除成功")); + } + } +} diff --git a/lib/feature/book/ui/view/book_page_view.dart b/lib/feature/book/ui/view/book_page_view.dart index 556c23b..5412906 100644 --- a/lib/feature/book/ui/view/book_page_view.dart +++ b/lib/feature/book/ui/view/book_page_view.dart @@ -1,94 +1,105 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; import 'package:tele_book/core/db/app_database.dart'; -import 'package:tele_book/feature/book/ui/viewmodel/book_page_viewmodel.dart'; +import 'package:tele_book/feature/book/ui/provider/book_page_provider.dart'; -class BookPageView extends StatelessWidget { +class BookPageView extends ConsumerStatefulWidget { final BookTableData book; const BookPageView({super.key, required this.book}); @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => - BookPageViewmodel(book: book, bookService: context.read()), - child: _BookPageContent(), + ConsumerState createState() => _BookPageViewState(); +} + +class _BookPageViewState extends ConsumerState { + late final PageController _pageController; + late final FPaginationController _paginationController; + + @override + void initState() { + super.initState(); + final notifier = ref.read(bookPageProvider(widget.book.id).notifier); + final state = ref.read(bookPageProvider(widget.book.id)); + final initialPage = state.currentPage; + _pageController = PageController(initialPage: initialPage); + _paginationController = FPaginationController( + pages: state.paths.length - 1, + siblings: 0, ); + _paginationController.value = initialPage; + notifier.initController(_pageController); + } + + @override + void dispose() { + _paginationController.dispose(); + _pageController.dispose(); + super.dispose(); + } + + void _handlePageChange(int page) { + final old = _pageController.page?.round(); + if (old == null || old == page) return; + if (page == old + 1 || page == old - 1) { + _pageController.animateToPage( + page, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + + } else { + _pageController.jumpToPage(page); + } } -} -class _BookPageContent extends StatelessWidget { - const _BookPageContent({super.key}); @override Widget build(BuildContext context) { - final fullWidth = MediaQuery.sizeOf(context).width; - final boxWidth = fullWidth / 3; - final viewmodel = context.watch(); - return Scaffold( - appBar: AppBar(title: Text(viewmodel.book.name)), - body: Stack( - children: [ - PageView.builder( - controller: viewmodel.pageController, - onPageChanged: viewmodel.onPageChanged, - itemBuilder: (context, index) { - final page = viewmodel.paths[index]; - return Image.file(File(page), fit: BoxFit.contain); - }, - itemCount: viewmodel.paths.length, - ), - Positioned.fill( - child: Align( - alignment: Alignment.center, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: viewmodel.toggleBar, - child: SizedBox(width: boxWidth, height: double.infinity), - ), + final state = ref.watch(bookPageProvider(widget.book.id)); + final notifier = ref.watch(bookPageProvider(widget.book.id).notifier); + + return FScaffold( + header: FHeader.nested( + title: Text(state.book.name), + prefixes: [FHeaderAction.back(onPress: () => context.pop())], + ), + footer: Padding( + padding: const EdgeInsets.all(16), + child: FPagination( + style: .delta( + itemConstraints: const BoxConstraints.tightFor( + width: 32, + height: 32, ), ), - Positioned.fill( - - child: Align( - alignment: Alignment.bottomCenter, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: viewmodel.isShowBar && viewmodel.paths.isNotEmpty - ? Container( - key: const ValueKey('progress_slider'), - height: 80, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - child: Row( - children: [ - Text('${viewmodel.currentPage + 1}'), - Expanded( - child: Slider( - value: viewmodel.currentPage.toDouble(), - min: 0, - max: (viewmodel.paths.length - 1).toDouble(), - divisions: viewmodel.paths.length > 1 - ? viewmodel.paths.length - 1 - : 1, - onChanged: (value) { - viewmodel.jumpToPage(value.toInt()); - }, - ), - ), - Text('${viewmodel.paths.length}'), - ], - ), - ) - : const SizedBox.shrink(), - ),), + control: FPaginationControl.managed( + controller: _paginationController, + onChange: _handlePageChange, ), - ], + ), + ), + child: NotificationListener( + onNotification: (notification) { + if (_pageController.hasClients) { + _paginationController.value = _pageController.page!.round(); + notifier.onPageChanged(_pageController.page!.round()); + return true; + } + return false; + }, + child: PageView.builder( + controller: _pageController, + itemCount: state.paths.length, + itemBuilder: (context, index) { + final page = state.paths[index]; + return Image.file(File(page), fit: BoxFit.contain); + }, + ), ), ); } diff --git a/lib/feature/book/ui/view/book_picker_view.dart b/lib/feature/book/ui/view/book_picker_view.dart new file mode 100644 index 0000000..042e0f2 --- /dev/null +++ b/lib/feature/book/ui/view/book_picker_view.dart @@ -0,0 +1,186 @@ +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tele_book/common/config/global_config.dart'; +import 'package:tele_book/common/widget/error_widget.dart'; +import 'package:tele_book/common/widget/local_image_widget.dart'; +import 'package:tele_book/core/db/app_database.dart'; +import 'package:tele_book/feature/book/ui/provider/book_provider.dart'; + +class BookPickerView extends ConsumerStatefulWidget { + final Set disabledBookIds; + + const BookPickerView({super.key, this.disabledBookIds = const {}}); + + @override + ConsumerState createState() => _BookPickerViewState(); +} + +class _BookPickerViewState extends ConsumerState { + Set _selectedBookIds = {}; + final TextEditingController _searchController = TextEditingController(); + String _keyword = ''; + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + void _toggleBook(int bookId) { + if (widget.disabledBookIds.contains(bookId)) return; + setState(() { + if (_selectedBookIds.contains(bookId)) { + _selectedBookIds.remove(bookId); + } else { + _selectedBookIds.add(bookId); + } + }); + } + + void _selectBooks(Set bookIds) { + setState(() { + _selectedBookIds = bookIds; + }); + } + + @override + Widget build(BuildContext context) { + final asyncState = ref.watch(booksProvider); + + return FScaffold( + header: FHeader.nested(title: Text("选择书籍")), + child: asyncState.when( + data: (books) { + final filteredBooks = books.where((book) { + if (_keyword.isEmpty) return true; + return book.name.toLowerCase().contains(_keyword.toLowerCase()); + }).toList(); + + final selectedBooks = books + .where((book) => _selectedBookIds.contains(book.id)) + .toList(); + + if (filteredBooks.isEmpty) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), + child: TextField( + controller: _searchController, + decoration: const InputDecoration( + hintText: '搜索书名', + prefixIcon: Icon(Icons.search), + border: OutlineInputBorder(), + ), + onChanged: (value) { + setState(() { + _keyword = value.trim(); + }); + }, + ), + ), + const Expanded(child: Center(child: Text('无匹配书籍'))), + ], + ); + } + + return Column( + crossAxisAlignment: .start, + children: [ + FTextField( + control: FTextFieldControl.managed( + controller: _searchController, + onChange: (value) { + setState(() { + _keyword = value.text.trim(); + }); + }, + ), + label: Text('搜索书名'), + hint: "请输入要搜索的书名", + prefixBuilder: (context, style, variant) { + return FButton.icon( + style: style.obscureButtonStyle, + child: Icon(Icons.search), + onPress: () {}, + ); + }, + ), + SizedBox(height: 8), + Text( + "书籍列表", + style: context.theme.typography.body.xs.copyWith( + fontWeight: .w500, + ), + ), + SizedBox(height: 8), + Expanded( + child: FSelectTileGroup.builder( + count: filteredBooks.length, + control: FMultiValueControl.managed( + initial: _selectedBookIds, + onChange: (value) { + _selectBooks(value); + }, + ), + tileBuilder: (context, index) { + final book = filteredBooks[index]; + final isDisabled = widget.disabledBookIds.contains(book.id); + final coverPath = book.coverSubPath != null + ? GlobalConfig.resolveBookPath(book.coverSubPath!) + : book.localSubPaths.isNotEmpty + ? GlobalConfig.resolveBookPath(book.localSubPaths.first) + : ''; + + return .suffix( + value: book.id, + enabled: !isDisabled, + prefix: coverPath.isEmpty + ? Container( + width: 64, + height: 64, + color: Colors.grey.shade300, + child: Icon( + Icons.menu_book_outlined, + color: Colors.grey.shade600, + ), + ) + : LocalImageWidget(imagePath: coverPath), + title: Text( + book.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + isDisabled + ? '已在当前收藏夹中' + : '共 ${book.localSubPaths.length} 页', + ), + ); + }, + ), + ), + Padding( + padding: .symmetric(vertical: 16), + child: FButton( + onPress: _selectedBookIds.isEmpty + ? null + : () { + context.pop>(selectedBooks); + }, + child: Text('确认选择 (${selectedBooks.length})'), + ), + ), + ], + ); + }, + error: (e, st) => Center( + child: CustomErrorWidget(errorMessage: e.toString(), stackTrace: st), + ), + loading: () => Center(child: CircularProgressIndicator()), + ), + ); + } +} diff --git a/lib/feature/book/ui/view/book_view.dart b/lib/feature/book/ui/view/book_view.dart deleted file mode 100644 index a29def9..0000000 --- a/lib/feature/book/ui/view/book_view.dart +++ /dev/null @@ -1,416 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:provider/provider.dart'; -import 'package:tele_book/common/widget/local_image_widget.dart'; -import 'package:tele_book/core/db/app_database.dart'; -import 'package:tele_book/core/route/app_route.dart'; -import 'package:tele_book/feature/book/enum/book_sort.dart'; -import 'package:tele_book/feature/book/model/vo/book_vo.dart'; -import 'package:tele_book/feature/book/store/book_store.dart'; -import 'package:tele_book/feature/book/ui/viewmodel/book_viewmodel.dart'; - -import 'package:tele_book/feature/download/store/download_store.dart'; - -class BookView extends StatelessWidget { - const BookView({super.key}); - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => BookViewmodel(context.read(), context.read()), - child: _BookViewContent(), - ); - } -} - -class _BookViewContent extends StatelessWidget { - @override - Widget build(BuildContext context) { - final bookStore = context.watch(); - final downloadStore = context.watch(); - final vm = context.watch(); - - return PopScope( - canPop: !vm.isSelectionMode, - onPopInvokedWithResult: (didPop, _) { - if (!didPop && vm.isSelectionMode) vm.exitSelectionMode(); - }, - child: Scaffold( - appBar: vm.isSelectionMode - ? _buildSelectionAppBar(context, vm) - : _buildNormalAppBar(context, vm, bookStore, downloadStore), - body: bookStore.books.isEmpty && !bookStore.isLoading - ? _buildEmpty() - : vm.layout == BookLayout.list - ? _BookListView(vm: vm) - : _BookGridView(vm: vm), - bottomNavigationBar: vm.isSelectionMode - ? _buildSelectionBottomBar(context, vm) - : null, - ), - ); - } - - Widget _buildEmpty() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.library_books_outlined, size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text('暂无书籍', style: TextStyle(fontSize: 16, color: Colors.grey[600])), - ], - ), - ); - } - - AppBar _buildNormalAppBar( - BuildContext context, - BookViewmodel vm, - BookStore bookStore, - DownloadStore downloadStore, - ) { - return AppBar( - title: const Text('书籍'), - elevation: 0, - actions: [ - downloadStore.tasks.isNotEmpty - ? IconButton( - onPressed: () => context.push(AppRoute.download), - icon: Badge( - label: Text('${downloadStore.tasks.length}'), - child: const Icon(Icons.download), - ), - ) - : const SizedBox.shrink(), - _buildTopMenuButton(context, vm, bookStore), - ], - ); - } - - AppBar _buildSelectionAppBar(BuildContext context, BookViewmodel vm) { - return AppBar( - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: vm.exitSelectionMode, - ), - title: Text('已选 ${vm.selectedBookIds.length} 本'), - actions: [TextButton(onPressed: vm.selectAll, child: const Text('全选'))], - ); - } - - Widget _buildSelectionBottomBar(BuildContext context, BookViewmodel vm) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: FilledButton.icon( - onPressed: vm.selectedBookIds.isEmpty - ? null - : () => vm.onExportSelected(context), - icon: const Icon(Icons.upload_file), - label: Text( - vm.selectedBookIds.isEmpty - ? '请选择书籍' - : '导出 ${vm.selectedBookIds.length} 本', - ), - ), - ), - ); - } - - Widget _buildTopMenuButton( - BuildContext context, - BookViewmodel vm, - BookStore bookStore, - ) { - return PopupMenuButton( - onSelected: (value) => vm.onTopMenuSelected(context, value), - itemBuilder: (context) => [ - ...BookTopMenuType.values.map((type) { - return PopupMenuItem( - value: type, - child: Row( - children: [ - Icon(type.icon, size: 16, color: Colors.grey[600]), - SizedBox(width: 8), - Text(type.title), - const SizedBox(width: 8), - if (type == BookTopMenuType.asc && - bookStore.sort.order == BookSortOrder.asc) - const Icon(Icons.check, size: 16), - if (type == BookTopMenuType.desc && - bookStore.sort.order == BookSortOrder.desc) - const Icon(Icons.check, size: 16), - if (type == BookTopMenuType.name && - bookStore.sort.type == BookSortType.title) - const Icon(Icons.check, size: 16), - if (type == BookTopMenuType.lastCreatedAt && - bookStore.sort.type == BookSortType.lastCreatedAt) - const Icon(Icons.check, size: 16), - if (type == BookTopMenuType.list && - vm.layout == BookLayout.list) - const Icon(Icons.check, size: 16), - if (type == BookTopMenuType.grid && - vm.layout == BookLayout.grid) - const Icon(Icons.check, size: 16), - ], - ), - ); - }), - ], - ); - } -} - -// ── 列表布局 ────────────────────────────────────────────── -class _BookListView extends StatelessWidget { - final BookViewmodel vm; - - const _BookListView({required this.vm}); - - @override - Widget build(BuildContext context) { - final bookStore = context.watch(); - final itemCount = bookStore.books.length + (bookStore.isLoading ? 1 : 0); - - return ListView.separated( - controller: vm.scrollController, - padding: const EdgeInsets.symmetric(vertical: 8), - separatorBuilder: (_, __) => const SizedBox(height: 4), - itemCount: itemCount, - itemBuilder: (context, index) { - if (index == bookStore.books.length) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Center(child: CircularProgressIndicator(strokeWidth: 2)), - ); - } - final book = bookStore.books[index]; - return _BookListTile(book: book, vm: vm); - }, - ); - } -} - -class _BookListTile extends StatelessWidget { - final BookListItemVo book; - final BookViewmodel vm; - - const _BookListTile({required this.book, required this.vm}); - - @override - Widget build(BuildContext context) { - final isSelected = vm.selectedBookIds.contains(book.book.id); - - return GestureDetector( - onTap: () { - if (vm.isSelectionMode) { - vm.toggleSelection(book.book.id); - } else { - context.push(AppRoute.bookPage, extra: book.book); - } - }, - onLongPress: () { - if (!vm.isSelectionMode) vm.enterSelectionMode(book.book); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - children: [ - if (vm.isSelectionMode) - Padding( - padding: const EdgeInsets.only(right: 8), - child: Checkbox( - value: isSelected, - onChanged: (_) => vm.toggleSelection(book.book.id), - ), - ), - LocalImageWidget(imagePath: book.coverImagePath), - Expanded( - child: ListTile( - title: Text( - book.book.name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text('共 ${book.book.localSubPaths.length} 页'), - ), - ), - if (!vm.isSelectionMode) - _buildItemMenuButton(context, vm, book.book), - ], - ), - ), - ); - } - - Widget _buildItemMenuButton( - BuildContext context, - BookViewmodel vm, - BookTableData book, - ) { - return PopupMenuButton( - onSelected: (value) => vm.onItemMenuSelected(context, value, book), - itemBuilder: (context) => BookItemMenuType.values - .map( - (type) => PopupMenuItem( - value: type, - child: Row( - children: [ - Icon(type.icon, size: 16, color: Colors.grey[600]), - SizedBox(width: 8), - Text(type.title), - ], - ), - ), - ) - .toList(), - ); - } -} - -// ── 网格布局 ────────────────────────────────────────────── -class _BookGridView extends StatelessWidget { - final BookViewmodel vm; - - const _BookGridView({required this.vm}); - - @override - Widget build(BuildContext context) { - final bookStore = context.watch(); - - return GridView.builder( - controller: vm.scrollController, - padding: const EdgeInsets.all(12), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - childAspectRatio: 0.65, - ), - itemCount: bookStore.books.length + (bookStore.isLoading ? 1 : 0), - itemBuilder: (context, index) { - if (index == bookStore.books.length) { - return const Center(child: CircularProgressIndicator(strokeWidth: 2)); - } - final book = bookStore.books[index]; - return _BookGridTile(book: book, vm: vm); - }, - ); - } -} - -class _BookGridTile extends StatelessWidget { - final BookListItemVo book; - final BookViewmodel vm; - - const _BookGridTile({required this.book, required this.vm}); - - @override - Widget build(BuildContext context) { - final isSelected = vm.selectedBookIds.contains(book.book.id); - - return GestureDetector( - onTap: () { - if (vm.isSelectionMode) { - vm.toggleSelection(book.book.id); - } else { - context.push(AppRoute.bookPage, extra: book.book); - } - }, - onLongPress: () { - if (vm.isSelectionMode) { - _showItemMenu(context); - } else { - vm.enterSelectionMode(book.book); - } - }, - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: book.coverImagePath.isNotEmpty - ? Image.file( - File(book.coverImagePath), - width: double.infinity, - fit: BoxFit.cover, - cacheWidth: 300, - errorBuilder: (_, __, ___) => _placeholder(), - ) - : _placeholder(), - ), - ), - const SizedBox(height: 4), - Text( - book.book.name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 12), - ), - Text( - '${book.book.localSubPaths.length} 页', - style: TextStyle(fontSize: 11, color: Colors.grey[600]), - ), - ], - ), - // 选择模式时显示选中指示器 - if (vm.isSelectionMode) - Positioned( - top: 4, - right: 4, - child: Container( - decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).colorScheme.primary - : Colors.white.withValues(alpha: 0.8), - shape: BoxShape.circle, - border: Border.all( - color: Theme.of(context).colorScheme.primary, - width: 2, - ), - ), - child: isSelected - ? const Icon(Icons.check, color: Colors.white, size: 18) - : const SizedBox(width: 18, height: 18), - ), - ), - ], - ), - ); - } - - Widget _placeholder() => Container( - width: double.infinity, - height: double.infinity, - color: Colors.grey[200], - child: Icon(Icons.book, color: Colors.grey[400], size: 40), - ); - - void _showItemMenu(BuildContext context) { - showModalBottomSheet( - context: context, - builder: (_) => SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: BookItemMenuType.values - .map( - (type) => ListTile( - title: Text(type.title), - onTap: () { - Navigator.pop(context); - vm.onItemMenuSelected(context, type, book.book); - }, - ), - ) - .toList(), - ), - ), - ); - } -} diff --git a/lib/feature/book/ui/viewmodel/book_form_viewmodel.dart b/lib/feature/book/ui/viewmodel/book_form_viewmodel.dart deleted file mode 100644 index 472dae3..0000000 --- a/lib/feature/book/ui/viewmodel/book_form_viewmodel.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:go_router/go_router.dart'; -import 'package:tele_book/common/config/global_config.dart'; -import 'package:tele_book/core/db/app_database.dart'; -import 'package:tele_book/core/route/app_route.dart'; -import 'package:tele_book/feature/book/repository/book_repository.dart'; - -class BookFormViewmodel extends ChangeNotifier { - final BookTableData book; - final BookRepository _bookRepository; - final List imagePaths = []; - final TextEditingController titleController = TextEditingController(); - - BookFormViewmodel(this.book, this._bookRepository) { - titleController.text = book.name; - for (final subPath in book.localSubPaths) { - imagePaths.add(BookFormPath(GlobalConfig.booksDir.path, subPath)); - } - } - - Future deleteImage(BookFormPath path) async { - imagePaths.remove(path); - notifyListeners(); - } - - void reorderImages(int oldIndex, int newIndex) { - if (oldIndex < 0 || oldIndex >= imagePaths.length) return; - if (newIndex < 0 || newIndex > imagePaths.length) return; - if (oldIndex == newIndex) return; - - if (oldIndex < newIndex) { - newIndex -= 1; - } - - final item = imagePaths.removeAt(oldIndex); - imagePaths.insert(newIndex, item); - notifyListeners(); - } - - Future updateBook(BuildContext context) async { - final updatedBook = book.copyWith( - name: titleController.text, - localSubPaths: imagePaths.map((p) => p.subPath).toList(), - ); - await _bookRepository.updateBook(updatedBook); - context.go(AppRoute.book); - } -} - -class BookFormPath { - final String parentPath; - final String subPath; - - BookFormPath(this.parentPath, this.subPath); - - String get fullPath => '$parentPath/$subPath'; -} diff --git a/lib/feature/book/ui/viewmodel/book_page_viewmodel.dart b/lib/feature/book/ui/viewmodel/book_page_viewmodel.dart deleted file mode 100644 index 96ea4bc..0000000 --- a/lib/feature/book/ui/viewmodel/book_page_viewmodel.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:tele_book/common/config/global_config.dart'; -import 'package:tele_book/core/db/app_database.dart'; -import 'package:tele_book/feature/book/service/book_service.dart'; - -class BookPageViewmodel extends ChangeNotifier { - final BookTableData book; - final BookService bookService; - List paths = []; - String title = ""; - bool isShowBar = false; - int currentPage = 0; - late final PageController pageController; - - BookPageViewmodel({required this.book, required this.bookService}) { - paths = book.localSubPaths - .map((rel) => GlobalConfig.resolveBookPath(rel)) - .toList(); - title = book.name; - currentPage = book.currentPage.clamp( - 0, - paths.isEmpty ? 0 : paths.length - 1, - ); - pageController = PageController(initialPage: currentPage); - } - - double get progress => paths.isEmpty ? 0.0 : (currentPage + 1) / paths.length; - - void onPageChanged(int index) { - currentPage = index; - notifyListeners(); - } - - void jumpToPage(int index) { - if (paths.isEmpty) return; - final target = index.clamp(0, paths.length - 1); - pageController.jumpToPage(target); - } - - void toggleBar() { - isShowBar = !isShowBar; - notifyListeners(); - } - - void nextPage() { - if (currentPage < paths.length - 1) { - currentPage++; - notifyListeners(); - } - } - - void prevPage() { - if (currentPage > 0) { - currentPage--; - notifyListeners(); - } - } - - @override - void dispose() { - pageController.dispose(); - super.dispose(); - } -} diff --git a/lib/feature/book/ui/viewmodel/book_viewmodel.dart b/lib/feature/book/ui/viewmodel/book_viewmodel.dart deleted file mode 100644 index 001dcf5..0000000 --- a/lib/feature/book/ui/viewmodel/book_viewmodel.dart +++ /dev/null @@ -1,199 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:tele_book/core/db/app_database.dart'; -import 'package:tele_book/core/route/app_route.dart'; -import 'package:tele_book/feature/book/enum/book_sort.dart'; -import 'package:tele_book/feature/book/model/vo/book_vo.dart'; -import 'package:tele_book/feature/book/repository/book_repository.dart'; -import 'package:tele_book/feature/book/store/book_store.dart'; - -enum BookLayout { list, grid } - -class BookViewmodel extends ChangeNotifier { - final BookRepository _bookRepository; - final BookStore bookStore; - final ScrollController scrollController = ScrollController(); - bool _isViewportCheckScheduled = false; - - BookLayout layout = BookLayout.list; - - // ── 批量选择模式 ────────────────────────────────────────── - bool isSelectionMode = false; - final Set selectedBookIds = {}; - - List get selectedBooks => - bookStore.books.where((b) => selectedBookIds.contains(b.book.id)).toList(); - - void enterSelectionMode(BookTableData book) { - isSelectionMode = true; - selectedBookIds.add(book.id); - notifyListeners(); - } - - void exitSelectionMode() { - isSelectionMode = false; - selectedBookIds.clear(); - notifyListeners(); - } - - void toggleSelection(int bookId) { - if (selectedBookIds.contains(bookId)) { - selectedBookIds.remove(bookId); - } else { - selectedBookIds.add(bookId); - } - notifyListeners(); - } - - void selectAll() { - selectedBookIds.addAll(bookStore.books.map((b) => b.book.id)); - notifyListeners(); - } - - void onExportSelected(BuildContext context) { - final books = selectedBooks.map((v) => v.book).toList(); - exitSelectionMode(); - context.push(AppRoute.exportBatch, extra: books); - } - - // ── 滚动 ───────────────────────────────────────────────── - - BookViewmodel(this._bookRepository, this.bookStore) { - scrollController.addListener(_onScroll); - bookStore.addListener(_onBookStoreChanged); - _scheduleViewportCheck(); - } - - void _onScroll() { - if (!scrollController.hasClients || bookStore.isLoading || !bookStore.hasMore) { - return; - } - final pos = scrollController.position; - if (pos.maxScrollExtent <= 0 || pos.pixels >= pos.maxScrollExtent - 200) { - bookStore.loadMore(); - } - } - - void _onBookStoreChanged() { - _scheduleViewportCheck(); - } - - void _scheduleViewportCheck() { - if (_isViewportCheckScheduled) return; - _isViewportCheckScheduled = true; - WidgetsBinding.instance.addPostFrameCallback((_) { - _isViewportCheckScheduled = false; - _onScroll(); - }); - } - - void toggleLayout() { - layout = layout == BookLayout.list ? BookLayout.grid : BookLayout.list; - notifyListeners(); - } - - @override - void dispose() { - scrollController.removeListener(_onScroll); - bookStore.removeListener(_onBookStoreChanged); - scrollController.dispose(); - super.dispose(); - } - - void onTopMenuSelected(BuildContext context, BookTopMenuType type) { - switch (type) { - case BookTopMenuType.add: - context.push(AppRoute.parseForm); - break; - case BookTopMenuType.desc: - bookStore.updateSort( - bookStore.sort.copyWith(order: BookSortOrder.desc), - ); - break; - case BookTopMenuType.asc: - bookStore.updateSort(bookStore.sort.copyWith(order: BookSortOrder.asc)); - break; - case BookTopMenuType.name: - bookStore.updateSort(bookStore.sort.copyWith(type: BookSortType.title)); - break; - case BookTopMenuType.lastCreatedAt: - bookStore.updateSort( - bookStore.sort.copyWith(type: BookSortType.lastCreatedAt), - ); - break; - case BookTopMenuType.list: - layout = BookLayout.list; - notifyListeners(); - break; - case BookTopMenuType.grid: - layout = BookLayout.grid; - notifyListeners(); - break; - } - } - - void onItemMenuSelected( - BuildContext context, - BookItemMenuType type, - BookTableData book, - ) { - switch (type) { - case BookItemMenuType.edit: - context.push(AppRoute.bookForm, extra: book); - break; - case BookItemMenuType.export: - context.push(AppRoute.exportSingle, extra: book); - break; - case BookItemMenuType.delete: - showGeneralDialog( - context: context, - pageBuilder: (_, __, ___) { - return AlertDialog( - title: const Text('删除书籍'), - content: const Text('确定要删除这本书吗?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('取消'), - ), - TextButton( - onPressed: () { - _bookRepository.deleteBook(book.id); - Navigator.of(context).pop(); - }, - child: const Text('删除'), - ), - ], - ); - }, - ); - break; - } - } -} - -enum BookTopMenuType { - add('添加', Icons.add), - desc('降序',Icons.arrow_downward), - asc('升序',Icons.arrow_upward), - name('按标题',Icons.sort_by_alpha), - lastCreatedAt('按时间',Icons.access_time), - list('列表',Icons.view_list), - grid('网格',Icons.grid_view); - - final String title; - final IconData icon; - - const BookTopMenuType(this.title,this.icon); -} - -enum BookItemMenuType { - edit('编辑',Icons.edit), - export('导出',Icons.file_download), - delete('删除',Icons.delete); - - final String title; - final IconData icon; - - const BookItemMenuType(this.title,this.icon); -} diff --git a/lib/feature/collection/datasource/local/collection_book_local_datasource.dart b/lib/feature/collection/datasource/local/collection_book_local_datasource.dart new file mode 100644 index 0000000..4c2c1af --- /dev/null +++ b/lib/feature/collection/datasource/local/collection_book_local_datasource.dart @@ -0,0 +1,48 @@ +import 'package:drift/drift.dart'; +import 'package:tele_book/core/db/app_database.dart'; +import 'package:tele_book/feature/collection/model/table/collection_book_table.dart'; + +part 'collection_book_local_datasource.g.dart'; + +@DriftAccessor(tables: [CollectionBookTable]) +class CollectionBookLocalDatasource extends DatabaseAccessor + with _$CollectionBookLocalDatasourceMixin { + CollectionBookLocalDatasource(super.db); + + Stream> watchAllCollectionBooks() { + return select(collectionBookTable).watch(); + } + + Future> getAllCollectionBooks() { + return select(collectionBookTable).get(); + } + + Future insertCollectionBook(CollectionBookTableCompanion entry) { + return into(collectionBookTable).insert(entry); + } + + Future updateCollectionBook(CollectionBookTableData entry) { + return update(collectionBookTable).replace(entry); + } + + Future insertCollectionBooks(List entries) { + return batch((batch) { + batch.insertAll(collectionBookTable, entries); + }); + } + + Future removeBookFromCollection(int collection, int bookId) { + return (delete( + collectionBookTable, + ) + ..where((tbl) => tbl.collectionId.equals(collection) & tbl.bookId.equals(bookId))) + .go(); + } + + + Future deleteCollectionBook(int id) { + return (delete( + collectionBookTable, + )..where((tbl) => tbl.id.equals(id))).go(); + } +} diff --git a/lib/feature/collection/datasource/local/collection_book_local_datasource.g.dart b/lib/feature/collection/datasource/local/collection_book_local_datasource.g.dart new file mode 100644 index 0000000..86013aa --- /dev/null +++ b/lib/feature/collection/datasource/local/collection_book_local_datasource.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'collection_book_local_datasource.dart'; + +// ignore_for_file: type=lint +mixin _$CollectionBookLocalDatasourceMixin on DatabaseAccessor { + $CollectionBookTableTable get collectionBookTable => + attachedDatabase.collectionBookTable; + CollectionBookLocalDatasourceManager get managers => + CollectionBookLocalDatasourceManager(this); +} + +class CollectionBookLocalDatasourceManager { + final _$CollectionBookLocalDatasourceMixin _db; + CollectionBookLocalDatasourceManager(this._db); + $$CollectionBookTableTableTableManager get collectionBookTable => + $$CollectionBookTableTableTableManager( + _db.attachedDatabase, + _db.collectionBookTable, + ); +} diff --git a/lib/feature/collection/datasource/local/collection_local_datasource.dart b/lib/feature/collection/datasource/local/collection_local_datasource.dart new file mode 100644 index 0000000..8166530 --- /dev/null +++ b/lib/feature/collection/datasource/local/collection_local_datasource.dart @@ -0,0 +1,31 @@ +import 'package:drift/drift.dart'; +import 'package:tele_book/core/db/app_database.dart'; +import 'package:tele_book/feature/collection/model/table/collection_table.dart'; + +part 'collection_local_datasource.g.dart'; + +@DriftAccessor(tables: [CollectionTable]) +class CollectionLocalDatasource extends DatabaseAccessor + with _$CollectionLocalDatasourceMixin { + CollectionLocalDatasource(super.db); + + Stream> watchCollections() { + return (select(collectionTable).watch()); + } + + Future> getCollectionsById(int id) { + return (select(collectionTable)..where((tbl) => tbl.id.equals(id))).get(); + } + + Future insertCollection(CollectionTableCompanion collection) { + return into(collectionTable).insert(collection); + } + + Future updateCollection(CollectionTableData collection) { + return update(collectionTable).replace(collection); + } + + Future deleteCollectionById(int id) { + return (delete(collectionTable)..where((tbl) => tbl.id.equals(id))).go(); + } +} diff --git a/lib/feature/collection/datasource/local/collection_local_datasource.g.dart b/lib/feature/collection/datasource/local/collection_local_datasource.g.dart new file mode 100644 index 0000000..0159b20 --- /dev/null +++ b/lib/feature/collection/datasource/local/collection_local_datasource.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'collection_local_datasource.dart'; + +// ignore_for_file: type=lint +mixin _$CollectionLocalDatasourceMixin on DatabaseAccessor { + $CollectionTableTable get collectionTable => attachedDatabase.collectionTable; + CollectionLocalDatasourceManager get managers => + CollectionLocalDatasourceManager(this); +} + +class CollectionLocalDatasourceManager { + final _$CollectionLocalDatasourceMixin _db; + CollectionLocalDatasourceManager(this._db); + $$CollectionTableTableTableManager get collectionTable => + $$CollectionTableTableTableManager( + _db.attachedDatabase, + _db.collectionTable, + ); +} diff --git a/lib/feature/collection/model/table/collection_book_table.dart b/lib/feature/collection/model/table/collection_book_table.dart new file mode 100644 index 0000000..9703658 --- /dev/null +++ b/lib/feature/collection/model/table/collection_book_table.dart @@ -0,0 +1,9 @@ +import 'package:drift/drift.dart'; + +class CollectionBookTable extends Table { + IntColumn get id => integer().autoIncrement()(); + + IntColumn get collectionId => integer()(); + + IntColumn get bookId => integer()(); +} diff --git a/lib/feature/collection/model/table/collection_table.dart b/lib/feature/collection/model/table/collection_table.dart new file mode 100644 index 0000000..faf83fc --- /dev/null +++ b/lib/feature/collection/model/table/collection_table.dart @@ -0,0 +1,11 @@ +import 'package:drift/drift.dart'; + +class CollectionTable extends Table { + IntColumn get id => integer().autoIncrement()(); + + TextColumn get name => text()(); + + TextColumn get description => text().nullable()(); + + TextColumn get coverImageSubPath => text().nullable()(); +} diff --git a/lib/feature/collection/model/vo/collection_list_item_vo.dart b/lib/feature/collection/model/vo/collection_list_item_vo.dart new file mode 100644 index 0000000..a45ad3d --- /dev/null +++ b/lib/feature/collection/model/vo/collection_list_item_vo.dart @@ -0,0 +1,13 @@ +import 'package:tele_book/core/db/app_database.dart'; + +class CollectionListItemVo { + final CollectionTableData collection; + final int count; + final List coverImages; + + CollectionListItemVo({ + required this.collection, + required this.count, + required this.coverImages, + }); +} \ No newline at end of file diff --git a/lib/feature/collection/repository/collection_repository.dart b/lib/feature/collection/repository/collection_repository.dart new file mode 100644 index 0000000..9dabe02 --- /dev/null +++ b/lib/feature/collection/repository/collection_repository.dart @@ -0,0 +1,61 @@ +import 'package:drift/drift.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:tele_book/core/db/app_database.dart'; + +final collectionRepositoryProvider = Provider((ref) { + final database = ref.watch(databaseProvider); + return CollectionRepository(database); +}); + +class CollectionRepository { + final AppDatabase _db; + + CollectionRepository(this._db); + + Stream> watchCollections() => + _db.collectionLocalDatasource.watchCollections(); + + Stream> watchAllCollectionBooks() => + _db.collectionBookLocalDatasource.watchAllCollectionBooks(); + + Future createCollection({ + required String name, + String? description, + }) async { + await _db.collectionLocalDatasource.insertCollection( + CollectionTableCompanion.insert( + name: name, + description: Value(description), + ), + ); + } + + Future addBooksToCollection({ + required int collectionId, + required List bookIds, + }) async { + final entries = bookIds.map((bookId) => CollectionBookTableCompanion.insert( + collectionId: collectionId, + bookId: bookId, + )).toList(); + await _db.collectionBookLocalDatasource.insertCollectionBooks(entries); + } + + Future removeBookFromCollection({ + required int collectionId, + required int bookId, + }) async { + await _db.collectionBookLocalDatasource.removeBookFromCollection( + collectionId, + bookId, + ); + } + + Future updateCollection(CollectionTableData collection) async { + await _db.collectionLocalDatasource.updateCollection(collection); + } + + Future deleteCollection(int id) async { + await _db.collectionLocalDatasource.deleteCollectionById(id); + } +} diff --git a/lib/feature/collection/ui/provider/collection_book_provider.dart b/lib/feature/collection/ui/provider/collection_book_provider.dart new file mode 100644 index 0000000..0f5532d --- /dev/null +++ b/lib/feature/collection/ui/provider/collection_book_provider.dart @@ -0,0 +1,51 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tele_book/common/config/global_config.dart'; +import 'package:tele_book/feature/book/model/state/book_list_state.dart'; +import 'package:tele_book/feature/book/ui/provider/book_provider.dart'; +import 'package:tele_book/feature/collection/ui/provider/collection_provider.dart'; + +part 'collection_book_provider.freezed.dart'; + +part 'collection_book_provider.g.dart'; + +@freezed +abstract class CollectionBookState with _$CollectionBookState { + const factory CollectionBookState({@Default([]) List books}) = + _CollectionBookState; +} + +@riverpod +AsyncValue collectionBookView(Ref ref,int collectionId) { + + final collectionsBooksAsync = ref.watch(collectionBooksProvider); + final booksAsync = ref.watch(booksProvider); + + if (collectionsBooksAsync.hasError) { + return AsyncValue.error(collectionsBooksAsync.error!, StackTrace.current); + } + if (booksAsync.hasError) { + return AsyncValue.error(booksAsync.error!, StackTrace.current); + } + + if (collectionsBooksAsync.isLoading || booksAsync.isLoading) { + return const AsyncValue.loading(); + } + + final collectionsBooks = collectionsBooksAsync.value!.where((e) => e.collectionId == collectionId).toList(); + final books = booksAsync.value!; + + final collectionBookIds = collectionsBooks.map((e) => e.bookId).toSet(); + final collectionBooks = books + .where((book) => collectionBookIds.contains(book.id)) + .toList(); + + final items = collectionBooks.map((book) { + final coverImagePath = book.coverSubPath != null + ? GlobalConfig.resolveBookPath(book.coverSubPath!) + : GlobalConfig.resolveBookPath(book.localSubPaths.first); + return BookListItemVo(book: book, coverImagePath: coverImagePath); + }).toList(); + + return AsyncValue.data(CollectionBookState(books: items)); +} diff --git a/lib/feature/collection/ui/provider/collection_book_provider.freezed.dart b/lib/feature/collection/ui/provider/collection_book_provider.freezed.dart new file mode 100644 index 0000000..0963fd9 --- /dev/null +++ b/lib/feature/collection/ui/provider/collection_book_provider.freezed.dart @@ -0,0 +1,277 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'collection_book_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$CollectionBookState { + + List get books; +/// Create a copy of CollectionBookState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$CollectionBookStateCopyWith get copyWith => _$CollectionBookStateCopyWithImpl(this as CollectionBookState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is CollectionBookState&&const DeepCollectionEquality().equals(other.books, books)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(books)); + +@override +String toString() { + return 'CollectionBookState(books: $books)'; +} + + +} + +/// @nodoc +abstract mixin class $CollectionBookStateCopyWith<$Res> { + factory $CollectionBookStateCopyWith(CollectionBookState value, $Res Function(CollectionBookState) _then) = _$CollectionBookStateCopyWithImpl; +@useResult +$Res call({ + List books +}); + + + + +} +/// @nodoc +class _$CollectionBookStateCopyWithImpl<$Res> + implements $CollectionBookStateCopyWith<$Res> { + _$CollectionBookStateCopyWithImpl(this._self, this._then); + + final CollectionBookState _self; + final $Res Function(CollectionBookState) _then; + +/// Create a copy of CollectionBookState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? books = null,}) { + return _then(_self.copyWith( +books: null == books ? _self.books : books // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [CollectionBookState]. +extension CollectionBookStatePatterns on CollectionBookState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _CollectionBookState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _CollectionBookState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _CollectionBookState value) $default,){ +final _that = this; +switch (_that) { +case _CollectionBookState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _CollectionBookState value)? $default,){ +final _that = this; +switch (_that) { +case _CollectionBookState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List books)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _CollectionBookState() when $default != null: +return $default(_that.books);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List books) $default,) {final _that = this; +switch (_that) { +case _CollectionBookState(): +return $default(_that.books);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List books)? $default,) {final _that = this; +switch (_that) { +case _CollectionBookState() when $default != null: +return $default(_that.books);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _CollectionBookState implements CollectionBookState { + const _CollectionBookState({final List books = const []}): _books = books; + + + final List _books; +@override@JsonKey() List get books { + if (_books is EqualUnmodifiableListView) return _books; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_books); +} + + +/// Create a copy of CollectionBookState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$CollectionBookStateCopyWith<_CollectionBookState> get copyWith => __$CollectionBookStateCopyWithImpl<_CollectionBookState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _CollectionBookState&&const DeepCollectionEquality().equals(other._books, _books)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_books)); + +@override +String toString() { + return 'CollectionBookState(books: $books)'; +} + + +} + +/// @nodoc +abstract mixin class _$CollectionBookStateCopyWith<$Res> implements $CollectionBookStateCopyWith<$Res> { + factory _$CollectionBookStateCopyWith(_CollectionBookState value, $Res Function(_CollectionBookState) _then) = __$CollectionBookStateCopyWithImpl; +@override @useResult +$Res call({ + List books +}); + + + + +} +/// @nodoc +class __$CollectionBookStateCopyWithImpl<$Res> + implements _$CollectionBookStateCopyWith<$Res> { + __$CollectionBookStateCopyWithImpl(this._self, this._then); + + final _CollectionBookState _self; + final $Res Function(_CollectionBookState) _then; + +/// Create a copy of CollectionBookState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? books = null,}) { + return _then(_CollectionBookState( +books: null == books ? _self._books : books // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +// dart format on diff --git a/lib/feature/collection/ui/provider/collection_book_provider.g.dart b/lib/feature/collection/ui/provider/collection_book_provider.g.dart new file mode 100644 index 0000000..3d8fbd3 --- /dev/null +++ b/lib/feature/collection/ui/provider/collection_book_provider.g.dart @@ -0,0 +1,96 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'collection_book_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(collectionBookView) +final collectionBookViewProvider = CollectionBookViewFamily._(); + +final class CollectionBookViewProvider + extends + $FunctionalProvider< + AsyncValue, + AsyncValue, + AsyncValue + > + with $Provider> { + CollectionBookViewProvider._({ + required CollectionBookViewFamily super.from, + required int super.argument, + }) : super( + retry: null, + name: r'collectionBookViewProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$collectionBookViewHash(); + + @override + String toString() { + return r'collectionBookViewProvider' + '' + '($argument)'; + } + + @$internal + @override + $ProviderElement> $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + AsyncValue create(Ref ref) { + final argument = this.argument as int; + return collectionBookView(ref, argument); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(AsyncValue value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>( + value, + ), + ); + } + + @override + bool operator ==(Object other) { + return other is CollectionBookViewProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$collectionBookViewHash() => + r'c03f3388df2a89f51a6a06a98d302c2dd9f4402b'; + +final class CollectionBookViewFamily extends $Family + with $FunctionalFamilyOverride, int> { + CollectionBookViewFamily._() + : super( + retry: null, + name: r'collectionBookViewProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + CollectionBookViewProvider call(int collectionId) => + CollectionBookViewProvider._(argument: collectionId, from: this); + + @override + String toString() => r'collectionBookViewProvider'; +} diff --git a/lib/feature/collection/ui/provider/collection_provider.dart b/lib/feature/collection/ui/provider/collection_provider.dart new file mode 100644 index 0000000..35c8d16 --- /dev/null +++ b/lib/feature/collection/ui/provider/collection_provider.dart @@ -0,0 +1,158 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tele_book/common/config/global_config.dart'; +import 'package:tele_book/core/db/app_database.dart'; +import 'package:tele_book/feature/book/ui/provider/book_provider.dart'; +import 'package:tele_book/feature/collection/model/vo/collection_list_item_vo.dart'; +import 'package:tele_book/feature/collection/repository/collection_repository.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'collection_provider.g.dart'; + +final collectionsProvider = + StreamProvider.autoDispose>((ref) { + final repo = ref.watch(collectionRepositoryProvider); + return repo.watchCollections(); + }); + +final collectionBooksProvider = + StreamProvider.autoDispose>((ref) { + final repo = ref.watch(collectionRepositoryProvider); + return repo.watchAllCollectionBooks(); + }); + +@riverpod +AsyncValue> collectionList(Ref ref) { + final collectionsAsync = ref.watch(collectionsProvider); + final collectionBooksAsync = ref.watch(collectionBooksProvider); + final booksAsync = ref.watch(booksProvider); + + if (collectionsAsync.hasError) { + return AsyncError( + collectionsAsync.error!, + collectionsAsync.stackTrace ?? StackTrace.current, + ); + } + if (collectionBooksAsync.hasError) { + return AsyncError( + collectionBooksAsync.error!, + collectionBooksAsync.stackTrace ?? StackTrace.current, + ); + } + if (booksAsync.hasError) { + return AsyncError( + booksAsync.error!, + booksAsync.stackTrace ?? StackTrace.current, + ); + } + if (collectionsAsync.isLoading || + collectionBooksAsync.isLoading || + booksAsync.isLoading) { + return const AsyncLoading(); + } + + final collections = collectionsAsync.value ?? const []; + final collectionBooks = + collectionBooksAsync.value ?? const []; + final books = booksAsync.value ?? const []; + + final bookCountMap = {}; + final bookIdsByCollectionId = >{}; + for (final cb in collectionBooks) { + bookCountMap[cb.collectionId] = (bookCountMap[cb.collectionId] ?? 0) + 1; + bookIdsByCollectionId + .putIfAbsent(cb.collectionId, () => []) + .add(cb.bookId); + } + + final bookById = {for (final b in books) b.id: b}; + + final list = collections.map((c) { + final count = bookCountMap[c.id] ?? 0; + final coverImages = (bookIdsByCollectionId[c.id] ?? const []) + .map((bookId) => bookById[bookId]) + .whereType() + .where((book) => book.localSubPaths.isNotEmpty) + .map((book) => book.coverSubPath != null + ? GlobalConfig.resolveBookPath(book.coverSubPath!) + : GlobalConfig.resolveBookPath(book.localSubPaths.first)) + .take(4) + .toList(); + + return CollectionListItemVo( + collection: c, + count: count, + coverImages: coverImages, + ); + }).toList(); + + return AsyncData(list); +} + +@riverpod +class CreateCollectionController extends _$CreateCollectionController { + @override + FutureOr build() { + // 可以在这里进行一些初始化操作 + } + + Future createCollection({ + required String name, + String? description, + }) async { + final trimmedName = name.trim(); + if (trimmedName.isEmpty) { + state = AsyncError('Collection name cannot be empty', StackTrace.current); + return; + } + + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + final repo = ref.read(collectionRepositoryProvider); + await repo.createCollection(name: name, description: description); + }); + } +} + +@riverpod +class UpdateCollectionController extends _$UpdateCollectionController { + @override + FutureOr build() => null; + + Future updateCollection({ + required int collectionId, + required String name, + String? description, + }) async { + final trimmedName = name.trim(); + if (trimmedName.isEmpty) { + state = AsyncError('Collection name cannot be empty', StackTrace.current); + return; + } + + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + final repo = ref.read(collectionRepositoryProvider); + await repo.updateCollection( + CollectionTableData( + id: collectionId, + name: name, + description: description, + ), + ); + }); + } +} + +@riverpod +class DeleteCollectionController extends _$DeleteCollectionController { + @override + FutureOr build() => null; + + Future deleteCollection({required int collectionId}) async { + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + final repo = ref.read(collectionRepositoryProvider); + await repo.deleteCollection(collectionId); + }); + } +} diff --git a/lib/feature/collection/ui/provider/collection_provider.g.dart b/lib/feature/collection/ui/provider/collection_provider.g.dart new file mode 100644 index 0000000..f005379 --- /dev/null +++ b/lib/feature/collection/ui/provider/collection_provider.g.dart @@ -0,0 +1,196 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'collection_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(collectionList) +final collectionListProvider = CollectionListProvider._(); + +final class CollectionListProvider + extends + $FunctionalProvider< + AsyncValue>, + AsyncValue>, + AsyncValue> + > + with $Provider>> { + CollectionListProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'collectionListProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$collectionListHash(); + + @$internal + @override + $ProviderElement>> $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + AsyncValue> create(Ref ref) { + return collectionList(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(AsyncValue> value) { + return $ProviderOverride( + origin: this, + providerOverride: + $SyncValueProvider>>(value), + ); + } +} + +String _$collectionListHash() => r'b121eda013ea7f8b0fd9013e6ea4694967e302a1'; + +@ProviderFor(CreateCollectionController) +final createCollectionControllerProvider = + CreateCollectionControllerProvider._(); + +final class CreateCollectionControllerProvider + extends $AsyncNotifierProvider { + CreateCollectionControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'createCollectionControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$createCollectionControllerHash(); + + @$internal + @override + CreateCollectionController create() => CreateCollectionController(); +} + +String _$createCollectionControllerHash() => + r'4a614af941a2c48759c089990e1658f64b044933'; + +abstract class _$CreateCollectionController extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} + +@ProviderFor(UpdateCollectionController) +final updateCollectionControllerProvider = + UpdateCollectionControllerProvider._(); + +final class UpdateCollectionControllerProvider + extends $AsyncNotifierProvider { + UpdateCollectionControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'updateCollectionControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$updateCollectionControllerHash(); + + @$internal + @override + UpdateCollectionController create() => UpdateCollectionController(); +} + +String _$updateCollectionControllerHash() => + r'13fda0c5619b384b3d7f1d9181b11f99002db90a'; + +abstract class _$UpdateCollectionController extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} + +@ProviderFor(DeleteCollectionController) +final deleteCollectionControllerProvider = + DeleteCollectionControllerProvider._(); + +final class DeleteCollectionControllerProvider + extends $AsyncNotifierProvider { + DeleteCollectionControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'deleteCollectionControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$deleteCollectionControllerHash(); + + @$internal + @override + DeleteCollectionController create() => DeleteCollectionController(); +} + +String _$deleteCollectionControllerHash() => + r'ec60e3546bdc4526f1c3e5ab5540655728761406'; + +abstract class _$DeleteCollectionController extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/feature/collection/ui/view/collection_book_view.dart b/lib/feature/collection/ui/view/collection_book_view.dart new file mode 100644 index 0000000..3c60d7b --- /dev/null +++ b/lib/feature/collection/ui/view/collection_book_view.dart @@ -0,0 +1,183 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tele_book/common/widget/empty_widget.dart'; +import 'package:tele_book/common/widget/error_widget.dart'; +import 'package:tele_book/common/widget/local_image_widget.dart'; +import 'package:tele_book/core/db/app_database.dart'; +import 'package:tele_book/core/route/app_route.dart'; +import 'package:tele_book/feature/collection/repository/collection_repository.dart'; +import 'package:tele_book/feature/collection/ui/provider/collection_book_provider.dart'; + +class CollectionBookView extends ConsumerWidget { + final int collectionId; + + const CollectionBookView({super.key, required this.collectionId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncState = ref.watch(collectionBookViewProvider(collectionId)); + + return FScaffold( + header: FHeader.nested( + title: Text("书籍列表"), + prefixes: [ + FHeaderAction.back( + onPress: () { + context.pop(); + }, + ), + ], + suffixes: [ + FHeaderAction( + icon: Icon(Icons.add), + onPress: () async { + final disabledBookIds = + asyncState.value?.books + .map((item) => item.book.id) + .toList() ?? + []; + + final result = await context.push>( + AppRoute.bookPicker, + extra: disabledBookIds, + ); + if (result != null && result.isNotEmpty) { + await ref + .read(collectionRepositoryProvider) + .addBooksToCollection( + collectionId: collectionId, + bookIds: result.map((e) => e.id).toList(), + ); + showFToast(context: context, title: Text("添加书籍到收藏成功")); + } + }, + ), + ], + ), + child: asyncState.when( + data: (data) { + if (data.books.isEmpty) { + return Center( + child: CustomEmptyWidget(icon: CupertinoIcons.book, text: "暂无书籍"), + ); + } + return FItemGroup.builder( + count: data.books.length, + itemBuilder: (context, index) { + final item = data.books[index]; + return FItem( + title: Text( + item.book.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + prefix: LocalImageWidget(imagePath: item.coverImagePath), + subtitle: Text('共 ${item.book.localSubPaths.length} 页'), + suffix: FButton.icon( + variant: .ghost, + onPress: () async { + final confirm = await showFDialog( + context: context, + builder: (context, style, animate) => FDialog.adaptive( + verticalBuilder: (context, style) { + return Padding( + padding: .all(16), + child: Column( + crossAxisAlignment: .start, + mainAxisSize: .min, + children: [ + Text("确认删除吗?", style: style.titleTextStyle), + SizedBox(width: 8), + Text( + "将从书架中移除该书籍,但不会删除本地文件", + style: style.bodyTextStyle, + ), + SizedBox(width: 8), + Row( + mainAxisAlignment: .end, + children: [ + FButton( + variant: .ghost, + onPress: () => + Navigator.pop(context, false), + child: Text("取消"), + ), + SizedBox(width: 8), + FButton( + variant: .destructive, + onPress: () => + Navigator.pop(context, true), + child: Text("确认"), + ), + ], + ), + ], + ), + ); + }, + horizontalBuilder: (context, style) { + return Padding( + padding: .all(16), + child: Column( + crossAxisAlignment: .start, + mainAxisSize: .min, + children: [ + Text("确认删除吗?", style: style.titleTextStyle), + SizedBox(width: 8), + Text( + "将从书架中移除该书籍,但不会删除本地文件", + style: style.bodyTextStyle, + ), + SizedBox(width: 8), + Row( + mainAxisAlignment: .end, + children: [ + FButton( + variant: .ghost, + onPress: () => + Navigator.pop(context, false), + child: Text("取消"), + ), + SizedBox(width: 8), + FButton( + variant: .destructive, + onPress: () { + Navigator.pop(context, true); + }, + child: Text("确认"), + ), + ], + ), + ], + ), + ); + }, + ), + ); + if (confirm == true) { + await ref + .read(collectionRepositoryProvider) + .removeBookFromCollection( + collectionId: collectionId, + bookId: item.book.id, + ); + showFToast(context: context, title: Text("删除收藏书籍成功")); + } + }, + child: Icon(FLucideIcons.trash), + ), + ); + }, + ); + }, + loading: () => Center(child: CircularProgressIndicator()), + error: (e, st) => Center( + child: CustomErrorWidget(errorMessage: e.toString(), stackTrace: st), + ), + ), + ); + } +} diff --git a/lib/feature/collection/ui/view/collection_view.dart b/lib/feature/collection/ui/view/collection_view.dart new file mode 100644 index 0000000..f141353 --- /dev/null +++ b/lib/feature/collection/ui/view/collection_view.dart @@ -0,0 +1,548 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tele_book/common/widget/empty_widget.dart'; +import 'package:tele_book/common/widget/error_widget.dart'; +import 'package:tele_book/core/route/app_route.dart'; +import 'package:tele_book/feature/collection/ui/provider/collection_provider.dart'; + +class CollectionView extends ConsumerStatefulWidget { + const CollectionView({super.key}); + + @override + ConsumerState createState() => _CollectionViewState(); +} + +class _CollectionViewState extends ConsumerState + with TickerProviderStateMixin { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final listAsync = ref.watch(collectionListProvider); + + ref.listen>(createCollectionControllerProvider, ( + prev, + next, + ) { + next.whenOrNull( + data: (_) { + if (prev?.isLoading == true) { + showFToast(context: context, title: Text("创建收藏夹成功")); + } + }, + error: (e, _) { + showFToast( + context: context, + title: Text("创建收藏夹失败"), + description: Text(e.toString()), + ); + }, + ); + }); + + ref.listen>(updateCollectionControllerProvider, ( + prev, + next, + ) { + next.whenOrNull( + data: (_) { + if (prev?.isLoading == true) { + showFToast(context: context, title: Text("修改收藏夹成功")); + } + }, + error: (e, _) { + showFToast( + context: context, + title: Text("修改收藏夹失败"), + description: Text(e.toString()), + ); + }, + ); + }); + + ref.listen>(deleteCollectionControllerProvider, ( + prev, + next, + ) { + next.whenOrNull( + data: (_) { + if (prev?.isLoading == true) { + showFToast(context: context, title: Text("删除收藏夹成功")); + } + }, + error: (e, _) { + showFToast( + context: context, + title: Text("删除收藏夹失败"), + description: Text(e.toString()), + ); + }, + ); + }); + + return FScaffold( + header: FHeader( + title: Text("收藏夹"), + suffixes: [ + FHeaderAction( + onPress: () => _showCreateBottomSheet(context, ref), + icon: const Icon(Icons.add), + ), + ], + ), + child: listAsync.when( + data: (list) { + if (list.isEmpty) { + return const Center( + child: CustomEmptyWidget( + icon: Icons.collections_bookmark_outlined, + ), + ); + } + + return GridView.builder( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + mainAxisExtent: 230, + ), + itemCount: list.length, + itemBuilder: (listContext, index) { + final item = list[index]; + final controller = FPopoverController(vsync: this); + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => context.push( + AppRoute.collectionBook, + extra: item.collection.id, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (item.coverImages.isEmpty) + AspectRatio( + aspectRatio: 1, + child: SizedBox( + child: Icon( + Icons.collections_bookmark_outlined, + size: 48, + color: Colors.grey[400], + ), + ), + ) + else + AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GridView.count( + crossAxisCount: 2, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: 2, + crossAxisSpacing: 2, + children: item.coverImages.take(4).map((url) { + return Image.file( + File(url), + fit: BoxFit.cover, + cacheWidth: 300, + ); + }).toList(), + ), + ), + ), + const SizedBox(height: 4), + + FItem( + title: Text( + item.collection.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text("${item.count} 本书"), + suffix: FPopoverMenu( + control: FPopoverControl.managed( + controller: controller, + ), + autofocus: true, + menu: [ + .group( + children: [ + .item( + title: Text('编辑'), + prefix: Icon(FLucideIcons.edit), + onPress: () { + controller.hide(); + _showUpdateBottomSheet( + context, + ref, + collectionId: item.collection.id, + initialName: item.collection.name, + initialDescription: + item.collection.description ?? '', + ); + }, + ), + .item( + variant: .destructive, + title: Text('删除'), + prefix: Icon(FLucideIcons.delete), + onPress: () { + controller.hide(); + _showDeleteConfirmDialog( + context, + item.collection.id, + ref, + ); + }, + ), + ], + ), + ], + builder: (context, controller, child) { + return FButton.icon( + onPress: () { + controller.show(); + }, + variant: .ghost, + child: Icon(FLucideIcons.moreHorizontal), + ); + }, + ), + ), + ], + ), + ); + }, + ); + }, + loading: () => const CircularProgressIndicator(), + error: (e, st) { + return Center( + child: CustomErrorWidget( + stackTrace: st, + errorMessage: e.toString(), + ), + ); + }, + ), + ); + } + + Future _showCreateBottomSheet( + BuildContext context, + WidgetRef ref, + ) async { + final formKey = GlobalKey(); + final nameController = TextEditingController(); + final descController = TextEditingController(); + final formData = await showFSheet<_CollectionFormData>( + context: context, + side: .btt, + mainAxisMaxRatio: null, + builder: (sheetContext) { + return Form( + key: formKey, + child: Container( + decoration: BoxDecoration( + color: context.theme.colors.background, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + border: .symmetric( + horizontal: BorderSide(color: context.theme.colors.border), + ), + ), + child: Padding( + padding: .all(16), + child: Column( + mainAxisAlignment: .center, + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Text( + "创建收藏夹", + style: context.theme.typography.display.xl2.copyWith( + fontWeight: .w600, + color: context.theme.colors.foreground, + height: 1.5, + ), + ), + const SizedBox(height: 8), + Text( + "按自己的习惯创建收藏夹,方便管理书籍", + style: context.theme.typography.body.sm.copyWith( + color: context.theme.colors.mutedForeground, + ), + ), + const SizedBox(height: 16), + FTextFormField( + control: FTextFieldControl.managed( + controller: nameController, + ), + label: Text("收藏夹名称"), + hint: "请输入收藏夹名称", + prefixBuilder: (context, style, _) { + return FButton.icon( + onPress: () {}, + style: style.obscureButtonStyle, + child: Icon(Icons.collections_bookmark_outlined), + ); + }, + validator: (v) => (v?.isEmpty ?? true) ? "请输入收藏夹名称" : null, + ), + SizedBox(height: 16), + FTextFormField( + control: FTextFieldControl.managed( + controller: descController, + ), + label: Text("描述"), + hint: "请输入收藏夹描述(可选)", + prefixBuilder: (context, style, _) { + return FButton.icon( + onPress: () {}, + style: style.obscureButtonStyle, + child: Icon(Icons.description_outlined), + ); + }, + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: FButton( + onPress: () { + if (formKey.currentState!.validate()) { + context.pop( + _CollectionFormData( + name: nameController.text, + description: descController.text, + ), + ); + } + }, + prefix: Icon(FLucideIcons.plus), + child: const Text("创建"), + ), + ), + ], + ), + ), + ), + ); + }, + ); + + if (formData != null) { + await ref + .read(createCollectionControllerProvider.notifier) + .createCollection( + name: formData.name, + description: formData.description, + ); + } + } + + Future _showUpdateBottomSheet( + BuildContext context, + WidgetRef ref, { + required int collectionId, + required String initialName, + required String initialDescription, + }) async { + final formKey = GlobalKey(); + final nameController = TextEditingController(text: initialName); + final descController = TextEditingController(text: initialDescription); + final formData = await showFSheet<_CollectionFormData>( + context: context, + side: .btt, + mainAxisMaxRatio: null, + builder: (sheetContext) { + return Form( + key: formKey, + child: Container( + decoration: BoxDecoration( + color: context.theme.colors.background, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + border: .symmetric( + horizontal: BorderSide(color: context.theme.colors.border), + ), + ), + child: Padding( + padding: .all(16), + child: Column( + mainAxisAlignment: .center, + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Text( + "编辑收藏夹", + style: context.theme.typography.display.xl2.copyWith( + fontWeight: .w600, + color: context.theme.colors.foreground, + height: 1.5, + ), + ), + const SizedBox(height: 8), + Text( + "按自己的习惯创建收藏夹,方便管理书籍", + style: context.theme.typography.body.sm.copyWith( + color: context.theme.colors.mutedForeground, + ), + ), + const SizedBox(height: 16), + FTextFormField( + control: FTextFieldControl.managed( + controller: nameController, + ), + label: Text("收藏夹名称"), + hint: "请输入收藏夹名称", + prefixBuilder: (context, style, _) { + return FButton.icon( + onPress: () {}, + style: style.obscureButtonStyle, + child: Icon(Icons.collections_bookmark_outlined), + ); + }, + validator: (v) => (v?.isEmpty ?? true) ? "请输入收藏夹名称" : null, + ), + SizedBox(height: 16), + FTextFormField( + control: FTextFieldControl.managed( + controller: descController, + ), + label: Text("描述"), + hint: "请输入收藏夹描述(可选)", + prefixBuilder: (context, style, _) { + return FButton.icon( + onPress: () {}, + style: style.obscureButtonStyle, + child: Icon(Icons.description_outlined), + ); + }, + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: FButton( + onPress: () { + if (formKey.currentState!.validate()) { + context.pop( + _CollectionFormData( + name: nameController.text, + description: descController.text, + ), + ); + } + }, + prefix: Icon(FLucideIcons.edit), + child: const Text("修改"), + ), + ), + ], + ), + ), + ), + ); + }, + ); + + if (formData != null) { + ref + .read(updateCollectionControllerProvider.notifier) + .updateCollection( + collectionId: collectionId, + name: nameController.text, + description: descController.text, + ); + } + } + + Future _showDeleteConfirmDialog( + BuildContext context, + int collectionId, + WidgetRef ref, + ) async { + final confirmed = await showFDialog( + context: context, + builder: (dialogContext, style, animate) => FDialog.adaptive( + horizontalBuilder: (context, style) { + return Padding( + padding: .all(16), + child: Column( + mainAxisSize: .min, + children: [ + const Text("删除收藏夹"), + const Text("确定要删除这个收藏夹吗?"), + Row( + children: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(false), + child: const Text("取消"), + ), + ElevatedButton( + onPressed: () => Navigator.of(dialogContext).pop(true), + child: const Text("删除"), + ), + ], + ), + ], + ), + ); + }, + + verticalBuilder: (context, style) { + return Padding( + padding: .all(16), + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Text("删除收藏夹", style: style.titleTextStyle), + SizedBox(height: 8), + Text("确定要删除这个收藏夹吗?", style: style.bodyTextStyle), + SizedBox(height: 8), + Row( + mainAxisAlignment: .end, + children: [ + FButton( + variant: .ghost, + onPress: () => Navigator.of(dialogContext).pop(false), + child: const Text("取消"), + ), + SizedBox(width: 8), + FButton( + variant: .destructive, + onPress: () => Navigator.of(dialogContext).pop(true), + child: const Text("删除"), + ), + ], + ), + ], + ), + ); + }, + ), + ); + if (confirmed == true) { + ref + .read(deleteCollectionControllerProvider.notifier) + .deleteCollection(collectionId: collectionId); + } + } +} + +class _CollectionFormData { + final String name; + final String description; + + const _CollectionFormData({required this.name, required this.description}); +} diff --git a/lib/feature/download/datasource/runtime/download_runtime_datasource.dart b/lib/feature/download/datasource/runtime/download_runtime_datasource.dart index c31fad7..73b41cb 100644 --- a/lib/feature/download/datasource/runtime/download_runtime_datasource.dart +++ b/lib/feature/download/datasource/runtime/download_runtime_datasource.dart @@ -1,9 +1,18 @@ import 'dart:async'; import 'dart:collection'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tele_book/feature/download/model/bo/download_bo.dart'; import 'package:tele_book/feature/download/model/vo/download_vo.dart'; +final downloadRuntimeDatasourceProvider = Provider(( + ref, +) { + final ds = DownloadRuntimeDatasource(); + ref.onDispose(ds.dispose); + return ds; +}); + class DownloadRuntimeDatasource { final LinkedHashMap _groupMap = LinkedHashMap(); final LinkedHashMap _itemMap = LinkedHashMap(); @@ -28,6 +37,8 @@ class DownloadRuntimeDatasource { DownloadGroupBo? getGroup(String groupId) => _groupMap[groupId]; + List getGroups() => _groupMap.values.toList(); + List getItemsByGroup(String groupId) { return _itemMap.values.where((item) => item.groupId == groupId).toList(); } diff --git a/lib/feature/download/repository/download_repository.dart b/lib/feature/download/repository/download_repository.dart index 9de8d2e..3b190b0 100644 --- a/lib/feature/download/repository/download_repository.dart +++ b/lib/feature/download/repository/download_repository.dart @@ -1,7 +1,13 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tele_book/feature/download/datasource/runtime/download_runtime_datasource.dart'; import 'package:tele_book/feature/download/model/bo/download_bo.dart'; import 'package:tele_book/feature/download/model/vo/download_vo.dart'; +final downloadRepositoryProvider = Provider((ref) { + return DownloadRepository(ref.watch(downloadRuntimeDatasourceProvider)); +}); + + class DownloadRepository { final DownloadRuntimeDatasource _downloadRuntimeDatasource; @@ -19,6 +25,10 @@ class DownloadRepository { return _downloadRuntimeDatasource.getGroup(groupId); } + List getDownloadGroups() { + return _downloadRuntimeDatasource.getGroups(); + } + List getDownloadItemsByGroup(String groupId) { return _downloadRuntimeDatasource.getItemsByGroup(groupId); } diff --git a/lib/feature/download/service/download_service.dart b/lib/feature/download/service/download_service.dart index 6703d87..f417231 100644 --- a/lib/feature/download/service/download_service.dart +++ b/lib/feature/download/service/download_service.dart @@ -2,10 +2,9 @@ import 'dart:async'; import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; import 'package:synchronized/synchronized.dart'; -import 'package:tele_book/common/config/global_config.dart'; -import 'package:tele_book/core/db/app_database.dart'; import 'package:tele_book/feature/book/model/dto/save_as_book_dto.dart'; import 'package:tele_book/feature/book/repository/book_repository.dart'; import 'package:tele_book/feature/download/enum/download_status.dart'; @@ -14,6 +13,13 @@ import 'package:tele_book/feature/download/model/vo/download_vo.dart'; import 'package:tele_book/feature/download/repository/download_repository.dart'; import 'package:uuid/uuid.dart'; +final downloadServiceProvider = Provider((ref) { + return DownloadService( + ref.watch(downloadRepositoryProvider), + ref.watch(bookRepositoryProvider), + ); +}); + class DownloadService { final FileDownloader _downloader = FileDownloader(); final DownloadRepository _downloadRepository; @@ -197,7 +203,6 @@ class DownloadService { }); }, onStatus: (status) async { - String? groupIdToUpdate; await _stateLock.synchronized(() { switch (status) { case TaskStatus.failed: @@ -206,8 +211,6 @@ class DownloadService { ); currentItem = failedItem; _downloadRepository.upsertItem(failedItem); - - groupIdToUpdate = item.groupId; break; case TaskStatus.complete: final completedItem = currentItem.copyWith( @@ -216,7 +219,6 @@ class DownloadService { ); currentItem = completedItem; _downloadRepository.upsertItem(completedItem); - groupIdToUpdate = item.groupId; break; case TaskStatus.running || TaskStatus.enqueued: final downloadingItem = currentItem.copyWith( @@ -228,23 +230,72 @@ class DownloadService { default: } }); - // 在 lock 外更新组状态和检查自动保存条件 - if (groupIdToUpdate != null) { - await _updateGroupStatus(groupIdToUpdate!); - } - await _checkAndAutoSave(item.groupId); + // 在 lock 外统一刷新组状态并检查自动保存条件 + await _refreshGroupState(item.groupId); }, ); } + /// 删除单个下载项:会同步清理临时文件、重算组状态,并在满足条件时自动保存为书籍 + Future deleteTaskItem(String itemId) async { + final item = _downloadRepository.getDownloadTask(itemId); + if (item == null) return; + + // 删除进行中的任务会和后台回调打架,先不支持,避免状态回写冲突 + if (item.status == DownloadStatus.pending || + item.status == DownloadStatus.downloading || + item.status == DownloadStatus.paused) { + throw StateError('下载中的任务暂不支持删除,请稍后再试'); + } + + final group = _downloadRepository.getDownloadGroup(item.groupId); + if (group == null) return; + + final file = File('${group.saveParentPath}/${item.saveSubPath}'); + if (await file.exists()) { + await file.delete(); + } + + _downloadRepository.deleteItem(itemId); + await _refreshGroupState(group.id); + } + + /// 清空所有已完成的下载组 + Future clearCompletedTasks() async { + final completedGroups = []; + + await _stateLock.synchronized(() async { + final groups = _downloadRepository.getDownloadGroups(); + for (final group in groups) { + if (group.status != DownloadStatus.completed) continue; + completedGroups.add(group); + _autoSavedGroups.remove(group.id); + _downloadRepository.deleteGroup(group.id); + } + }); + + for (final group in completedGroups) { + final dir = Directory(group.saveParentPath); + if (await dir.exists()) { + await dir.delete(recursive: true); + } + } + + return completedGroups.length; + } + /// 根据组内所有项的状态更新组状态 - Future _updateGroupStatus(String groupId) async { + Future _refreshGroupState(String groupId) async { final group = _downloadRepository.getDownloadGroup(groupId); if (group == null) return; // 直接读取仓库中的当前快照,避免 stream.first 时序带来的状态回读延迟 final items = _downloadRepository.getDownloadItemsByGroup(groupId); - if (items.isEmpty) return; + if (items.isEmpty) { + _autoSavedGroups.remove(groupId); + _downloadRepository.deleteGroup(groupId); + return; + } // 统计各状态的任务数 final completedCount = items @@ -281,6 +332,8 @@ class DownloadService { status: newStatus, ), ); + + await _checkAndAutoSave(groupId); } /// 检查组是否已完成且无失败,若满足则自动保存为书籍 diff --git a/lib/feature/download/ui/provider/download_provider.dart b/lib/feature/download/ui/provider/download_provider.dart new file mode 100644 index 0000000..8a7a182 --- /dev/null +++ b/lib/feature/download/ui/provider/download_provider.dart @@ -0,0 +1,13 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tele_book/feature/book/repository/book_repository.dart'; +import 'package:tele_book/feature/download/datasource/runtime/download_runtime_datasource.dart'; +import 'package:tele_book/feature/download/model/vo/download_vo.dart'; +import 'package:tele_book/feature/download/repository/download_repository.dart'; +import 'package:tele_book/feature/download/service/download_service.dart'; + + +/// 监听下载任务列表(响应式 Stream) +final downloadTasksProvider = StreamProvider>((ref) { + return ref.watch(downloadServiceProvider).watchDownloadTasks(); +}); + diff --git a/lib/feature/download/ui/view/download_list_view.dart b/lib/feature/download/ui/view/download_list_view.dart index cf11d82..4fb8e5b 100644 --- a/lib/feature/download/ui/view/download_list_view.dart +++ b/lib/feature/download/ui/view/download_list_view.dart @@ -1,29 +1,20 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; import 'package:go_router/go_router.dart'; -import 'package:provider/provider.dart'; +import 'package:tele_book/common/widget/error_widget.dart'; import 'package:tele_book/common/widget/task_item_widget.dart'; import 'package:tele_book/core/route/app_route.dart'; import 'package:tele_book/feature/download/enum/download_status.dart'; import 'package:tele_book/feature/download/model/bo/download_bo.dart'; -import 'package:tele_book/feature/download/store/download_store.dart'; +import 'package:tele_book/feature/download/service/download_service.dart'; +import 'package:tele_book/feature/download/ui/provider/download_provider.dart'; +import 'package:tele_book/feature/download/ui/widget/download_task_sheet_widget.dart'; -class DownloadListView extends StatelessWidget { +class DownloadListView extends ConsumerWidget { const DownloadListView({super.key}); - @override - Widget build(BuildContext context) { - return const _DownloadListContent(); - } -} - -class _DownloadListContent extends StatefulWidget { - const _DownloadListContent(); - - @override - State<_DownloadListContent> createState() => _DownloadListContentState(); -} - -class _DownloadListContentState extends State<_DownloadListContent> { double _groupProgressPercent(List items) { if (items.isEmpty) return 0; final total = items.fold(0, (sum, item) => sum + item.progress); @@ -31,116 +22,202 @@ class _DownloadListContentState extends State<_DownloadListContent> { } @override - Widget build(BuildContext context) { - final store = context.watch(); - return Scaffold( - appBar: AppBar( + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(downloadTasksProvider); + return FScaffold( + header: FHeader( title: Text("下载任务"), - leading: BackButton( - onPressed: () { - context.go(AppRoute.book); - }, - ), + suffixes: [ + FHeaderAction( + onPress: () async { + await _clearCompletedTasks(context, ref); + }, + icon: Icon(FLucideIcons.trash), + ), + + FHeaderAction( + onPress: () async { + await _showDownloadForm(context); + }, + icon: Icon(FLucideIcons.plus), + ), + ], ), - body: store.tasks.isEmpty - ? Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.download, size: 64, color: Colors.grey), - SizedBox(height: 16), - Text( - "暂无下载任务", - style: TextStyle(fontSize: 16, color: Colors.grey), + child: state.when( + loading: () => Center(child: FProgress()), + error: (e, st) => Center( + child: CustomErrorWidget(errorMessage: "加载下载任务失败", stackTrace: st), + ), + data: (tasks) { + return tasks.isEmpty + ? Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.download, size: 64, color: Colors.grey), + SizedBox(height: 16), + Text( + "暂无下载任务", + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + ], ), - ], - ), - ) - : ListView.separated( - padding: EdgeInsets.all(16), - separatorBuilder: (context, index) => SizedBox(height: 16), - itemBuilder: (context, index) { - final item = store.tasks[index]; - final progressPercent = _groupProgressPercent( - item.downloadItemBoList, - ); - return TaskItemWidget( - title: item.downloadGroupBo.name, - coverUrl: item.downloadItemBoList.first.url, - status: item.downloadGroupBo.status.description, - progress: progressPercent, - onTap: () => - _showDownloadTaskList(context, item.downloadGroupBo.id), + ) + : FItemGroup.builder( + itemBuilder: (context, index) { + final item = tasks[index]; + final progressPercent = _groupProgressPercent( + item.downloadItemBoList, + ); + return TaskItemWidget( + title: item.downloadGroupBo.name, + coverUrl: item.downloadItemBoList.first.url, + status: item.downloadGroupBo.status.description, + progress: progressPercent, + onTap: () => _showDownloadTaskList( + context, + item.downloadGroupBo.id, + item.downloadGroupBo.name, + ref, + ), + trailing: Icon(FLucideIcons.chevronRight), + ); + }, + count: tasks.length, ); - }, - itemCount: store.tasks.length, - ), + }, + ), ); } - void _showDownloadTaskList(BuildContext context, String groupId) { - showModalBottomSheet( + Future _clearCompletedTasks(BuildContext context, WidgetRef ref) async { + final tasks = await ref.read(downloadTasksProvider.future); + final hasCompleted = tasks.any( + (group) => group.downloadGroupBo.status == DownloadStatus.completed, + ); + if (!hasCompleted) { + if (!context.mounted) return; + showFToast(context: context, title: Text('没有可清空的已完成任务')); + return; + } + + final confirmed = await showFDialog( context: context, - builder: (context) { - return Consumer( - builder: (context, store, _) { - final tasks = store.tasks - .where((task) => task.downloadGroupBo.id == groupId) - .expand((task) => task.downloadItemBoList) - .toList(); + builder: (dialogContext, style, animate) => FDialog.adaptive( + style: style, + animation: animate, + horizontalBuilder: (context, style) => + Text('清空已完成任务', style: style.titleTextStyle), + verticalBuilder: (context, style) => + Text('将移除所有已完成的下载任务组及其临时文件,是否继续?', style: style.bodyTextStyle), + ), + ); + + if (confirmed != true || !context.mounted) return; - if (tasks.isEmpty) { - return Container( - padding: const EdgeInsets.all(16), - child: Center( - child: Text( - "处理中", - style: TextStyle(fontSize: 16, color: Colors.grey), + try { + final clearedCount = await ref + .read(downloadServiceProvider) + .clearCompletedTasks(); + if (!context.mounted) return; + showFToast(context: context, title: Text('已清空 $clearedCount 个已完成任务')); + } catch (e) { + if (!context.mounted) return; + showFToast( + context: context, + title: Text("清楚失败"), + description: Text(e.toString()), + ); + } + } + + void _showDownloadTaskList( + BuildContext context, + String groupId, + String name, + WidgetRef ref, + ) { + showFSheet( + context: context, + side: .btt, + builder: (_) => DownloadTaskSheetWidget(groupId: groupId,name: name,), + ); + } + + Future _showDownloadForm(BuildContext context) async { + final urlController = TextEditingController(); + final url = await showFSheet( + context: context, + side: .btt, + style: const .delta(flingVelocity: 700), + builder: (context) { + return Container( + decoration: BoxDecoration( + color: context.theme.colors.background, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + border: .symmetric( + horizontal: BorderSide(color: context.theme.colors.border), + ), + ), + child: Padding( + padding: .all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "下载链接", + style: context.theme.typography.display.xl2.copyWith( + fontWeight: .w600, + color: context.theme.colors.foreground, + height: 1.5, ), ), - ); - } - - return Container( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - const Text( - "下载任务详情", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + SizedBox(height: 8), + Text( + '要下载的网页图片链接,如https://www.google.com', + style: context.theme.typography.body.sm.copyWith( + color: context.theme.colors.mutedForeground, ), - const SizedBox(height: 16), - Expanded( - child: ListView.separated( - separatorBuilder: (context, index) => - const SizedBox(height: 16), - itemBuilder: (context, index) { - final task = tasks[index]; - - return TaskItemWidget( - title: task.url, - coverUrl: task.url, - status: task.status.description, - progress: task.progress, - trailing: task.status == DownloadStatus.failed - ? IconButton( - onPressed: () { - store.retryDownload(task.id); - }, - icon: Icon(Icons.refresh), - ) - : null, + ), + SizedBox(height: 16), + FTextFormField( + control: FTextFieldControl.managed(controller: urlController), + label: Text("URL"), + hint: "请输入网址URL", + suffixBuilder: (context, style, _) { + return FButton.icon( + onPress: () async { + final clipData = await Clipboard.getData( + Clipboard.kTextPlain, ); + if (clipData?.text != null) { + urlController.text = clipData!.text!; + } }, - itemCount: tasks.length, - ), + child: Icon(FLucideIcons.clipboardPaste), + style: style.obscureButtonStyle, + ); + }, + ), + Padding( + padding: .symmetric(vertical: 16), + child: FButton( + onPress: () { + context.pop(urlController.text); + }, + child: Text("解析"), ), - ], - ), - ); - }, + ), + ], + ), + ), ); }, ); + + if (url != null && url.isNotEmpty) { + await context.push(AppRoute.parseWeb, extra: urlController.text); + } } } diff --git a/lib/feature/download/ui/widget/download_task_sheet_widget.dart b/lib/feature/download/ui/widget/download_task_sheet_widget.dart new file mode 100644 index 0000000..76c14a8 --- /dev/null +++ b/lib/feature/download/ui/widget/download_task_sheet_widget.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; +import 'package:tele_book/common/widget/f_sheet_content.dart'; +import 'package:tele_book/common/widget/task_item_widget.dart'; +import 'package:tele_book/feature/download/enum/download_status.dart'; +import 'package:tele_book/feature/download/service/download_service.dart'; +import 'package:tele_book/feature/download/ui/provider/download_provider.dart'; + +class DownloadTaskSheetWidget extends ConsumerWidget { + final String groupId; + final String name; + + const DownloadTaskSheetWidget({required this.groupId, required this.name}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncTasks = ref.watch(downloadTasksProvider); + + return asyncTasks.when( + loading: () => const Padding( + padding: EdgeInsets.all(16), + child: Center(child: FCircularProgress()), + ), + error: (e, st) => Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Text( + "加载任务失败", + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + ), + ), + data: (groups) { + final tasks = groups + .where((g) => g.downloadGroupBo.id == groupId) + .expand((g) => g.downloadItemBoList) + .toList(); + + if (tasks.isEmpty) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center( + child: Text( + "处理中", + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + ), + ); + } + + return FSheetContent( + side: .btt, + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + FSheetContent.title(context, "下载任务详情"), + FSheetContent.subTitle(context,name), + const SizedBox(height: 12), + Expanded( + child: FItemGroup.builder( + count: tasks.length, + itemBuilder: (context, index) { + final task = tasks[index]; + return TaskItemWidget( + title: task.url, + coverUrl: task.url, + status: task.status.description, + progress: task.progress, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (task.status == DownloadStatus.failed) + IconButton( + tooltip: '重试', + onPressed: () { + ref + .read(downloadServiceProvider) + .retryTask(task.id); + }, + icon: const Icon(Icons.refresh), + ), + IconButton( + tooltip: + task.status == DownloadStatus.pending || + task.status == DownloadStatus.downloading || + task.status == DownloadStatus.paused + ? '下载中暂不支持删除' + : '删除任务项', + onPressed: + task.status == DownloadStatus.pending || + task.status == DownloadStatus.downloading || + task.status == DownloadStatus.paused + ? null + : () async { + final confirmed = await showDialog( + context: context, + builder: (dialogContext) { + return AlertDialog( + title: const Text('删除任务项'), + content: const Text( + '删除后会重新检查剩余任务,若已经全部完成则会自动保存书籍,是否继续?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of( + dialogContext, + ).pop(false), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () => Navigator.of( + dialogContext, + ).pop(true), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of( + context, + ).colorScheme.error, + ), + child: const Text( + '删除', + style: TextStyle( + color: Colors.white, + ), + ), + ), + ], + ); + }, + ); + + if (confirmed != true || !context.mounted) { + return; + } + + try { + await ref + .read(downloadServiceProvider) + .deleteTaskItem(task.id); + } catch (e) { + if (!context.mounted) return; + showFToast( + context: context, + title: Text("删除失败"), + description: Text(e.toString()), + ); + } + }, + icon: const Icon(FLucideIcons.trash), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/feature/export/ui/provider/export_batch_provider.dart b/lib/feature/export/ui/provider/export_batch_provider.dart new file mode 100644 index 0000000..1a7d8ca --- /dev/null +++ b/lib/feature/export/ui/provider/export_batch_provider.dart @@ -0,0 +1,113 @@ +import 'package:dk_util/dk_util.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tele_book/common/config/global_config.dart'; +import 'package:tele_book/feature/book/ui/provider/book_provider.dart'; +import 'package:tele_book/feature/export/enum/export_format.dart'; +import 'package:tele_book/feature/export/model/export_item.dart'; +import 'package:tele_book/feature/export/service/export_service.dart'; + +part 'export_batch_provider.freezed.dart'; + +part 'export_batch_provider.g.dart'; + +@freezed +abstract class ExportBatchState with _$ExportBatchState { + const factory ExportBatchState({ + required ExportFormat format, + required bool isExporting, + required bool isDone, + required int progress, + required List items, + required TextEditingController outputPathController, + String? errorMessage, + }) = _ExportBatchState; +} + +@riverpod +class ExportBatch extends _$ExportBatch { + final ExportService _exportService = ExportService(); + + @override + ExportBatchState build(List bookIds) { + final books = ref + .read(bookListProvider) + .value + ?.bookVos + .where((e) => bookIds.contains(e.book.id)) + .map((e) => e.book) + .toList(); + if (books == null || books.isEmpty) { + throw Exception('Books not found'); + } + + final items = books + .map( + (b) => ExportItem( + book: b, + coverPath: b.coverSubPath != null + ? '${GlobalConfig.booksDir.path}/${b.coverSubPath}' + : '${GlobalConfig.booksDir.path}/${b.localSubPaths.first}', + ), + ) + .toList(); + + ref.onDispose(() { + for (final item in items) { + item.dispose(); + } + }); + + return ExportBatchState( + format: ExportFormat.folder, + isExporting: false, + isDone: false, + progress: 0, + items: items, + outputPathController: TextEditingController() + ); + } + + void setFormat(ExportFormat fmt) { + state = state.copyWith(format: fmt); + } + + void setError(String message) { + state = state.copyWith(errorMessage: message); + } + + Future pickOutputDir() async { + final result = await FilePicker.platform.getDirectoryPath(); + if (result != null) { + state.outputPathController.text=result; + } + } + + Future doExport() async { + final path = state.outputPathController.text; + + state = state.copyWith(isExporting: true, progress: 0, errorMessage: null); + try { + final exportList = state.items + .map((i) => (book: i.book, fileName: i.nameController.text.trim())) + .toList(); + + await _exportService.exportBatch( + items: exportList, + outputDirPath: path, + format: state.format, + onProgress: (current, total) { + if (!ref.mounted) return; + state = state.copyWith(progress: current); + }, + ); + if (!ref.mounted) return; + state = state.copyWith(isDone: true, isExporting: false); + } catch (e) { + if (!ref.mounted) return; + state = state.copyWith(isExporting: false, errorMessage: '导出失败:$e'); + } + } +} diff --git a/lib/feature/export/ui/provider/export_batch_provider.freezed.dart b/lib/feature/export/ui/provider/export_batch_provider.freezed.dart new file mode 100644 index 0000000..fc4de6e --- /dev/null +++ b/lib/feature/export/ui/provider/export_batch_provider.freezed.dart @@ -0,0 +1,295 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'export_batch_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ExportBatchState { + + ExportFormat get format; bool get isExporting; bool get isDone; int get progress; List get items; TextEditingController get outputPathController; String? get errorMessage; +/// Create a copy of ExportBatchState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ExportBatchStateCopyWith get copyWith => _$ExportBatchStateCopyWithImpl(this as ExportBatchState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ExportBatchState&&(identical(other.format, format) || other.format == format)&&(identical(other.isExporting, isExporting) || other.isExporting == isExporting)&&(identical(other.isDone, isDone) || other.isDone == isDone)&&(identical(other.progress, progress) || other.progress == progress)&&const DeepCollectionEquality().equals(other.items, items)&&(identical(other.outputPathController, outputPathController) || other.outputPathController == outputPathController)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); +} + + +@override +int get hashCode => Object.hash(runtimeType,format,isExporting,isDone,progress,const DeepCollectionEquality().hash(items),outputPathController,errorMessage); + +@override +String toString() { + return 'ExportBatchState(format: $format, isExporting: $isExporting, isDone: $isDone, progress: $progress, items: $items, outputPathController: $outputPathController, errorMessage: $errorMessage)'; +} + + +} + +/// @nodoc +abstract mixin class $ExportBatchStateCopyWith<$Res> { + factory $ExportBatchStateCopyWith(ExportBatchState value, $Res Function(ExportBatchState) _then) = _$ExportBatchStateCopyWithImpl; +@useResult +$Res call({ + ExportFormat format, bool isExporting, bool isDone, int progress, List items, TextEditingController outputPathController, String? errorMessage +}); + + + + +} +/// @nodoc +class _$ExportBatchStateCopyWithImpl<$Res> + implements $ExportBatchStateCopyWith<$Res> { + _$ExportBatchStateCopyWithImpl(this._self, this._then); + + final ExportBatchState _self; + final $Res Function(ExportBatchState) _then; + +/// Create a copy of ExportBatchState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? format = null,Object? isExporting = null,Object? isDone = null,Object? progress = null,Object? items = null,Object? outputPathController = null,Object? errorMessage = freezed,}) { + return _then(_self.copyWith( +format: null == format ? _self.format : format // ignore: cast_nullable_to_non_nullable +as ExportFormat,isExporting: null == isExporting ? _self.isExporting : isExporting // ignore: cast_nullable_to_non_nullable +as bool,isDone: null == isDone ? _self.isDone : isDone // ignore: cast_nullable_to_non_nullable +as bool,progress: null == progress ? _self.progress : progress // ignore: cast_nullable_to_non_nullable +as int,items: null == items ? _self.items : items // ignore: cast_nullable_to_non_nullable +as List,outputPathController: null == outputPathController ? _self.outputPathController : outputPathController // ignore: cast_nullable_to_non_nullable +as TextEditingController,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ExportBatchState]. +extension ExportBatchStatePatterns on ExportBatchState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ExportBatchState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ExportBatchState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ExportBatchState value) $default,){ +final _that = this; +switch (_that) { +case _ExportBatchState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ExportBatchState value)? $default,){ +final _that = this; +switch (_that) { +case _ExportBatchState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( ExportFormat format, bool isExporting, bool isDone, int progress, List items, TextEditingController outputPathController, String? errorMessage)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ExportBatchState() when $default != null: +return $default(_that.format,_that.isExporting,_that.isDone,_that.progress,_that.items,_that.outputPathController,_that.errorMessage);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( ExportFormat format, bool isExporting, bool isDone, int progress, List items, TextEditingController outputPathController, String? errorMessage) $default,) {final _that = this; +switch (_that) { +case _ExportBatchState(): +return $default(_that.format,_that.isExporting,_that.isDone,_that.progress,_that.items,_that.outputPathController,_that.errorMessage);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( ExportFormat format, bool isExporting, bool isDone, int progress, List items, TextEditingController outputPathController, String? errorMessage)? $default,) {final _that = this; +switch (_that) { +case _ExportBatchState() when $default != null: +return $default(_that.format,_that.isExporting,_that.isDone,_that.progress,_that.items,_that.outputPathController,_that.errorMessage);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ExportBatchState implements ExportBatchState { + const _ExportBatchState({required this.format, required this.isExporting, required this.isDone, required this.progress, required final List items, required this.outputPathController, this.errorMessage}): _items = items; + + +@override final ExportFormat format; +@override final bool isExporting; +@override final bool isDone; +@override final int progress; + final List _items; +@override List get items { + if (_items is EqualUnmodifiableListView) return _items; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_items); +} + +@override final TextEditingController outputPathController; +@override final String? errorMessage; + +/// Create a copy of ExportBatchState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ExportBatchStateCopyWith<_ExportBatchState> get copyWith => __$ExportBatchStateCopyWithImpl<_ExportBatchState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ExportBatchState&&(identical(other.format, format) || other.format == format)&&(identical(other.isExporting, isExporting) || other.isExporting == isExporting)&&(identical(other.isDone, isDone) || other.isDone == isDone)&&(identical(other.progress, progress) || other.progress == progress)&&const DeepCollectionEquality().equals(other._items, _items)&&(identical(other.outputPathController, outputPathController) || other.outputPathController == outputPathController)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); +} + + +@override +int get hashCode => Object.hash(runtimeType,format,isExporting,isDone,progress,const DeepCollectionEquality().hash(_items),outputPathController,errorMessage); + +@override +String toString() { + return 'ExportBatchState(format: $format, isExporting: $isExporting, isDone: $isDone, progress: $progress, items: $items, outputPathController: $outputPathController, errorMessage: $errorMessage)'; +} + + +} + +/// @nodoc +abstract mixin class _$ExportBatchStateCopyWith<$Res> implements $ExportBatchStateCopyWith<$Res> { + factory _$ExportBatchStateCopyWith(_ExportBatchState value, $Res Function(_ExportBatchState) _then) = __$ExportBatchStateCopyWithImpl; +@override @useResult +$Res call({ + ExportFormat format, bool isExporting, bool isDone, int progress, List items, TextEditingController outputPathController, String? errorMessage +}); + + + + +} +/// @nodoc +class __$ExportBatchStateCopyWithImpl<$Res> + implements _$ExportBatchStateCopyWith<$Res> { + __$ExportBatchStateCopyWithImpl(this._self, this._then); + + final _ExportBatchState _self; + final $Res Function(_ExportBatchState) _then; + +/// Create a copy of ExportBatchState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? format = null,Object? isExporting = null,Object? isDone = null,Object? progress = null,Object? items = null,Object? outputPathController = null,Object? errorMessage = freezed,}) { + return _then(_ExportBatchState( +format: null == format ? _self.format : format // ignore: cast_nullable_to_non_nullable +as ExportFormat,isExporting: null == isExporting ? _self.isExporting : isExporting // ignore: cast_nullable_to_non_nullable +as bool,isDone: null == isDone ? _self.isDone : isDone // ignore: cast_nullable_to_non_nullable +as bool,progress: null == progress ? _self.progress : progress // ignore: cast_nullable_to_non_nullable +as int,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable +as List,outputPathController: null == outputPathController ? _self.outputPathController : outputPathController // ignore: cast_nullable_to_non_nullable +as TextEditingController,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/lib/feature/export/ui/provider/export_batch_provider.g.dart b/lib/feature/export/ui/provider/export_batch_provider.g.dart new file mode 100644 index 0000000..8d3300e --- /dev/null +++ b/lib/feature/export/ui/provider/export_batch_provider.g.dart @@ -0,0 +1,107 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'export_batch_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ExportBatch) +final exportBatchProvider = ExportBatchFamily._(); + +final class ExportBatchProvider + extends $NotifierProvider { + ExportBatchProvider._({ + required ExportBatchFamily super.from, + required List super.argument, + }) : super( + retry: null, + name: r'exportBatchProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$exportBatchHash(); + + @override + String toString() { + return r'exportBatchProvider' + '' + '($argument)'; + } + + @$internal + @override + ExportBatch create() => ExportBatch(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ExportBatchState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } + + @override + bool operator ==(Object other) { + return other is ExportBatchProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$exportBatchHash() => r'2db90e6ebc7e97ab5eb9f60e13c388f69815b9a3'; + +final class ExportBatchFamily extends $Family + with + $ClassFamilyOverride< + ExportBatch, + ExportBatchState, + ExportBatchState, + ExportBatchState, + List + > { + ExportBatchFamily._() + : super( + retry: null, + name: r'exportBatchProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + ExportBatchProvider call(List bookIds) => + ExportBatchProvider._(argument: bookIds, from: this); + + @override + String toString() => r'exportBatchProvider'; +} + +abstract class _$ExportBatch extends $Notifier { + late final _$args = ref.$arg as List; + List get bookIds => _$args; + + ExportBatchState build(List bookIds); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + ExportBatchState, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} diff --git a/lib/feature/export/ui/provider/export_single_provider.dart b/lib/feature/export/ui/provider/export_single_provider.dart new file mode 100644 index 0000000..dd4fb0b --- /dev/null +++ b/lib/feature/export/ui/provider/export_single_provider.dart @@ -0,0 +1,82 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/widgets.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tele_book/core/db/app_database.dart'; +import 'package:tele_book/feature/book/ui/provider/book_provider.dart'; +import 'package:tele_book/feature/export/enum/export_format.dart'; +import 'package:tele_book/feature/export/service/export_service.dart'; + +part 'export_single_provider.freezed.dart'; + +part 'export_single_provider.g.dart'; + +@freezed +abstract class ExportSingleState with _$ExportSingleState { + const factory ExportSingleState({ + required BookTableData book, + required ExportFormat format, + required TextEditingController fileNameCrl, + required bool isExporting, + required bool isDone, + required TextEditingController outputPathCrl, + String? errorMsg + }) = _ExportSingleState; +} + +@riverpod +class ExportSingle extends _$ExportSingle { + final ExportService _exportService = ExportService(); + + @override + ExportSingleState build(int bookId) { + final book = ref + .watch(bookListProvider) + .value + ?.bookVos + .where((e) => e.book.id == bookId) + .first + .book; + if (book == null) { + throw Exception('Book not found'); + } + return ExportSingleState( + book: book, + format: ExportFormat.folder, + fileNameCrl: TextEditingController(text: book.name), + isExporting: false, + isDone: false, + outputPathCrl: TextEditingController(), + ); + } + + void setFormat(ExportFormat fmt) { + state = state.copyWith(format: fmt); + } + + + Future pickOutputDir() async { + final result = await FilePicker.platform.getDirectoryPath(); + if (result != null) { + state.outputPathCrl.text = result; + } + } + + Future doExport() async { + final path = state.outputPathCrl.text; + final name = state.fileNameCrl.text.trim(); + + state = state.copyWith(isExporting: true); + try { + await _exportService.exportSingle( + book: state.book, + outputDirPath: path, + fileName: name, + format: state.format, + ); + state = state.copyWith(isDone: true, isExporting: false); + } catch (e) { + state = state.copyWith(isExporting: false, errorMsg: '导出失败:$e'); + } + } +} diff --git a/lib/feature/export/ui/provider/export_single_provider.freezed.dart b/lib/feature/export/ui/provider/export_single_provider.freezed.dart new file mode 100644 index 0000000..ad253de --- /dev/null +++ b/lib/feature/export/ui/provider/export_single_provider.freezed.dart @@ -0,0 +1,289 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'export_single_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ExportSingleState { + + BookTableData get book; ExportFormat get format; TextEditingController get fileNameCrl; bool get isExporting; bool get isDone; TextEditingController get outputPathCrl; String? get errorMsg; +/// Create a copy of ExportSingleState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ExportSingleStateCopyWith get copyWith => _$ExportSingleStateCopyWithImpl(this as ExportSingleState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ExportSingleState&&(identical(other.book, book) || other.book == book)&&(identical(other.format, format) || other.format == format)&&(identical(other.fileNameCrl, fileNameCrl) || other.fileNameCrl == fileNameCrl)&&(identical(other.isExporting, isExporting) || other.isExporting == isExporting)&&(identical(other.isDone, isDone) || other.isDone == isDone)&&(identical(other.outputPathCrl, outputPathCrl) || other.outputPathCrl == outputPathCrl)&&(identical(other.errorMsg, errorMsg) || other.errorMsg == errorMsg)); +} + + +@override +int get hashCode => Object.hash(runtimeType,book,format,fileNameCrl,isExporting,isDone,outputPathCrl,errorMsg); + +@override +String toString() { + return 'ExportSingleState(book: $book, format: $format, fileNameCrl: $fileNameCrl, isExporting: $isExporting, isDone: $isDone, outputPathCrl: $outputPathCrl, errorMsg: $errorMsg)'; +} + + +} + +/// @nodoc +abstract mixin class $ExportSingleStateCopyWith<$Res> { + factory $ExportSingleStateCopyWith(ExportSingleState value, $Res Function(ExportSingleState) _then) = _$ExportSingleStateCopyWithImpl; +@useResult +$Res call({ + BookTableData book, ExportFormat format, TextEditingController fileNameCrl, bool isExporting, bool isDone, TextEditingController outputPathCrl, String? errorMsg +}); + + + + +} +/// @nodoc +class _$ExportSingleStateCopyWithImpl<$Res> + implements $ExportSingleStateCopyWith<$Res> { + _$ExportSingleStateCopyWithImpl(this._self, this._then); + + final ExportSingleState _self; + final $Res Function(ExportSingleState) _then; + +/// Create a copy of ExportSingleState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? book = null,Object? format = null,Object? fileNameCrl = null,Object? isExporting = null,Object? isDone = null,Object? outputPathCrl = null,Object? errorMsg = freezed,}) { + return _then(_self.copyWith( +book: null == book ? _self.book : book // ignore: cast_nullable_to_non_nullable +as BookTableData,format: null == format ? _self.format : format // ignore: cast_nullable_to_non_nullable +as ExportFormat,fileNameCrl: null == fileNameCrl ? _self.fileNameCrl : fileNameCrl // ignore: cast_nullable_to_non_nullable +as TextEditingController,isExporting: null == isExporting ? _self.isExporting : isExporting // ignore: cast_nullable_to_non_nullable +as bool,isDone: null == isDone ? _self.isDone : isDone // ignore: cast_nullable_to_non_nullable +as bool,outputPathCrl: null == outputPathCrl ? _self.outputPathCrl : outputPathCrl // ignore: cast_nullable_to_non_nullable +as TextEditingController,errorMsg: freezed == errorMsg ? _self.errorMsg : errorMsg // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ExportSingleState]. +extension ExportSingleStatePatterns on ExportSingleState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ExportSingleState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ExportSingleState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ExportSingleState value) $default,){ +final _that = this; +switch (_that) { +case _ExportSingleState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ExportSingleState value)? $default,){ +final _that = this; +switch (_that) { +case _ExportSingleState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( BookTableData book, ExportFormat format, TextEditingController fileNameCrl, bool isExporting, bool isDone, TextEditingController outputPathCrl, String? errorMsg)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ExportSingleState() when $default != null: +return $default(_that.book,_that.format,_that.fileNameCrl,_that.isExporting,_that.isDone,_that.outputPathCrl,_that.errorMsg);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( BookTableData book, ExportFormat format, TextEditingController fileNameCrl, bool isExporting, bool isDone, TextEditingController outputPathCrl, String? errorMsg) $default,) {final _that = this; +switch (_that) { +case _ExportSingleState(): +return $default(_that.book,_that.format,_that.fileNameCrl,_that.isExporting,_that.isDone,_that.outputPathCrl,_that.errorMsg);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( BookTableData book, ExportFormat format, TextEditingController fileNameCrl, bool isExporting, bool isDone, TextEditingController outputPathCrl, String? errorMsg)? $default,) {final _that = this; +switch (_that) { +case _ExportSingleState() when $default != null: +return $default(_that.book,_that.format,_that.fileNameCrl,_that.isExporting,_that.isDone,_that.outputPathCrl,_that.errorMsg);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ExportSingleState implements ExportSingleState { + const _ExportSingleState({required this.book, required this.format, required this.fileNameCrl, required this.isExporting, required this.isDone, required this.outputPathCrl, this.errorMsg}); + + +@override final BookTableData book; +@override final ExportFormat format; +@override final TextEditingController fileNameCrl; +@override final bool isExporting; +@override final bool isDone; +@override final TextEditingController outputPathCrl; +@override final String? errorMsg; + +/// Create a copy of ExportSingleState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ExportSingleStateCopyWith<_ExportSingleState> get copyWith => __$ExportSingleStateCopyWithImpl<_ExportSingleState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ExportSingleState&&(identical(other.book, book) || other.book == book)&&(identical(other.format, format) || other.format == format)&&(identical(other.fileNameCrl, fileNameCrl) || other.fileNameCrl == fileNameCrl)&&(identical(other.isExporting, isExporting) || other.isExporting == isExporting)&&(identical(other.isDone, isDone) || other.isDone == isDone)&&(identical(other.outputPathCrl, outputPathCrl) || other.outputPathCrl == outputPathCrl)&&(identical(other.errorMsg, errorMsg) || other.errorMsg == errorMsg)); +} + + +@override +int get hashCode => Object.hash(runtimeType,book,format,fileNameCrl,isExporting,isDone,outputPathCrl,errorMsg); + +@override +String toString() { + return 'ExportSingleState(book: $book, format: $format, fileNameCrl: $fileNameCrl, isExporting: $isExporting, isDone: $isDone, outputPathCrl: $outputPathCrl, errorMsg: $errorMsg)'; +} + + +} + +/// @nodoc +abstract mixin class _$ExportSingleStateCopyWith<$Res> implements $ExportSingleStateCopyWith<$Res> { + factory _$ExportSingleStateCopyWith(_ExportSingleState value, $Res Function(_ExportSingleState) _then) = __$ExportSingleStateCopyWithImpl; +@override @useResult +$Res call({ + BookTableData book, ExportFormat format, TextEditingController fileNameCrl, bool isExporting, bool isDone, TextEditingController outputPathCrl, String? errorMsg +}); + + + + +} +/// @nodoc +class __$ExportSingleStateCopyWithImpl<$Res> + implements _$ExportSingleStateCopyWith<$Res> { + __$ExportSingleStateCopyWithImpl(this._self, this._then); + + final _ExportSingleState _self; + final $Res Function(_ExportSingleState) _then; + +/// Create a copy of ExportSingleState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? book = null,Object? format = null,Object? fileNameCrl = null,Object? isExporting = null,Object? isDone = null,Object? outputPathCrl = null,Object? errorMsg = freezed,}) { + return _then(_ExportSingleState( +book: null == book ? _self.book : book // ignore: cast_nullable_to_non_nullable +as BookTableData,format: null == format ? _self.format : format // ignore: cast_nullable_to_non_nullable +as ExportFormat,fileNameCrl: null == fileNameCrl ? _self.fileNameCrl : fileNameCrl // ignore: cast_nullable_to_non_nullable +as TextEditingController,isExporting: null == isExporting ? _self.isExporting : isExporting // ignore: cast_nullable_to_non_nullable +as bool,isDone: null == isDone ? _self.isDone : isDone // ignore: cast_nullable_to_non_nullable +as bool,outputPathCrl: null == outputPathCrl ? _self.outputPathCrl : outputPathCrl // ignore: cast_nullable_to_non_nullable +as TextEditingController,errorMsg: freezed == errorMsg ? _self.errorMsg : errorMsg // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/lib/feature/export/ui/provider/export_single_provider.g.dart b/lib/feature/export/ui/provider/export_single_provider.g.dart new file mode 100644 index 0000000..a6c438c --- /dev/null +++ b/lib/feature/export/ui/provider/export_single_provider.g.dart @@ -0,0 +1,107 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'export_single_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ExportSingle) +final exportSingleProvider = ExportSingleFamily._(); + +final class ExportSingleProvider + extends $NotifierProvider { + ExportSingleProvider._({ + required ExportSingleFamily super.from, + required int super.argument, + }) : super( + retry: null, + name: r'exportSingleProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$exportSingleHash(); + + @override + String toString() { + return r'exportSingleProvider' + '' + '($argument)'; + } + + @$internal + @override + ExportSingle create() => ExportSingle(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ExportSingleState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } + + @override + bool operator ==(Object other) { + return other is ExportSingleProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$exportSingleHash() => r'e33ec12c57f13a8edef79fefd40746dd6e2aef00'; + +final class ExportSingleFamily extends $Family + with + $ClassFamilyOverride< + ExportSingle, + ExportSingleState, + ExportSingleState, + ExportSingleState, + int + > { + ExportSingleFamily._() + : super( + retry: null, + name: r'exportSingleProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + ExportSingleProvider call(int bookId) => + ExportSingleProvider._(argument: bookId, from: this); + + @override + String toString() => r'exportSingleProvider'; +} + +abstract class _$ExportSingle extends $Notifier { + late final _$args = ref.$arg as int; + int get bookId => _$args; + + ExportSingleState build(int bookId); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + ExportSingleState, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} diff --git a/lib/feature/export/ui/view/export_batch_form_view.dart b/lib/feature/export/ui/view/export_batch_form_view.dart index e030aca..f164089 100644 --- a/lib/feature/export/ui/view/export_batch_form_view.dart +++ b/lib/feature/export/ui/view/export_batch_form_view.dart @@ -1,199 +1,260 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; import 'package:tele_book/common/widget/local_image_widget.dart'; import 'package:tele_book/core/db/app_database.dart'; import 'package:tele_book/feature/export/enum/export_format.dart'; -import 'package:tele_book/feature/export/ui/viewmodel/export_batch_viewmodel.dart'; +import 'package:tele_book/feature/export/model/export_item.dart'; +import 'package:tele_book/feature/export/ui/provider/export_batch_provider.dart'; -class ExportBatchFormView extends StatelessWidget { +class ExportBatchFormView extends ConsumerStatefulWidget { final List books; + const ExportBatchFormView({super.key, required this.books}); @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (_) => ExportBatchViewmodel(books: books), - child: const _ExportBatchFormContent(), - ); - } + ConsumerState createState() => + _ExportBatchFormViewState(); } -class _ExportBatchFormContent extends StatelessWidget { - const _ExportBatchFormContent(); +class _ExportBatchFormViewState extends ConsumerState { + final _formKey = GlobalKey(); + late final List _bookIds; + + @override + void initState() { + super.initState(); + _bookIds = widget.books.map((b) => b.id).toList(); + } @override Widget build(BuildContext context) { - final vm = context.watch(); + final state = ref.watch( + exportBatchProvider(_bookIds), + ); + final notifier = ref.read( + exportBatchProvider(_bookIds).notifier, + ); - if (vm.isDone) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('全部导出成功!'), - backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, - ), + ref.listen(exportBatchProvider(_bookIds), ( + prev, + next, + ) { + if (prev?.isDone == false && next.isDone) { + showFToast( + context: context, + icon: const Icon(FLucideIcons.check), + title: const Text('导出成功'), + description: Text('全部 ${next.items.length} 本书已导出'), + swipeToDismiss: const [.right], + duration: const Duration(seconds: 3), ); Navigator.of(context).pop(); - }); + } + }); - } - - return Scaffold( - appBar: AppBar(title: Text('批量导出(${vm.items.length} 本)')), - body: Column( - children: [ - // 顶部设置区 - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 导出格式 - LayoutBuilder( - builder: (context, constraints) => DropdownMenu( - width: constraints.maxWidth, - initialSelection: vm.format, - label: const Text('导出格式'), - leadingIcon: const Icon(Icons.file_present), - onSelected: (v) { - if (v != null) vm.setFormat(v); - }, - dropdownMenuEntries: ExportFormat.values - .map((f) => DropdownMenuEntry(value: f, label: f.label)) - .toList(), - ), - ), - const SizedBox(height: 12), - - // 导出路径 - TextField( - readOnly: true, - controller: TextEditingController(text: vm.outputPath ?? ''), - decoration: InputDecoration( - labelText: '导出路径', - hintText: '请选择导出目录', - prefixIcon: const Icon(Icons.folder_open), - border: const OutlineInputBorder(), - suffixIcon: IconButton( - icon: const Icon(Icons.drive_folder_upload), - onPressed: vm.isExporting ? null : vm.pickOutputDir, + return FScaffold( + header: FHeader.nested( + title: Text('批量导出(${state.items.length} 本)'), + prefixes: [FHeaderAction.back(onPress: () => context.pop())], + ), + child: Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + children: [ + // 顶部设置区 + Padding( + padding: .all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 导出格式 + FSelect.rich( + control: FSelectControl.managed( + onChange: (value) { + if (value != null) notifier.setFormat(value); + }, + initial: state.format, ), + label: Text("导出格式"), + hint: "请选择导出格式", + format: (s) => s.label, + children: [ + for (final format in ExportFormat.values) + .item(title: Text(format.label), value: format), + ], + validator: (v) => (v == null) ? '请选择导出格式' : null, ), - ), + const SizedBox(height: 12), - if (vm.errorMessage != null) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - vm.errorMessage!, - style: TextStyle( - color: Theme.of(context).colorScheme.error, - ), + // 导出路径 + FTextFormField( + readOnly: true, + control: FTextFieldControl.managed( + controller: state.outputPathController, ), + label: Text('导出路径'), + hint: '请选择导出目录', + suffixBuilder: (context, style, variants) { + return FButton.icon( + style: style.obscureButtonStyle, + onPress: state.isExporting + ? null + : () => notifier.pickOutputDir(), + child: Icon(FLucideIcons.folderOpen), + ); + }, + validator: (v) => (v?.isEmpty ?? true) ? '请选择路径' : null, + onTap: state.isExporting + ? null + : () => notifier.pickOutputDir(), ), - - // 进度条 - if (vm.isExporting) ...[ - const SizedBox(height: 12), - LinearProgressIndicator( - value: vm.items.isNotEmpty - ? vm.progress / vm.items.length - : null, - ), - const SizedBox(height: 4), - Text( - '正在导出 ${vm.progress} / ${vm.items.length}', - style: Theme.of(context).textTheme.bodySmall, - ), + // 进度条 + if (state.isExporting) ...[ + const SizedBox(height: 12), + FDeterminateProgress( + value: state.items.isNotEmpty + ? state.progress / state.items.length + : 0, + ), + const SizedBox(height: 4), + Text( + '正在导出 ${state.progress} / ${state.items.length}', + style: Theme.of(context).textTheme.bodySmall, + ), + ], ], - ], + ), ), - ), - const Divider(height: 1), + const Divider(height: 1), - // 导出项列表 - Expanded( - child: ListView.separated( - padding: const EdgeInsets.all(16), - separatorBuilder: (_, __) => const SizedBox(height: 16), - itemCount: vm.items.length, - itemBuilder: (context, index) { - final item = vm.items[index]; - return Row( - children: [ - LocalImageWidget(imagePath: item.coverPath), - Expanded( - child: ListTile( - title: Text( - item.book.name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text('${item.book.localSubPaths.length} 页'), - ), + // 导出项列表 + Expanded( + child: FItemGroup.builder( + count: state.items.length, + itemBuilder: (context, index) { + final item = state.items[index]; + return FItem( + prefix: LocalImageWidget(imagePath: item.coverPath), + title: Text( + item.book.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - IconButton( - onPressed: () { - showGeneralDialog( - context: context, - pageBuilder: (_, _, _) { - return AlertDialog( - title: Text('编辑导出文件名'), - content: TextField( - controller: item.nameController, - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: '导出文件名', - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('取消'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('确定'), - ), - ], - ); - }, - ); - }, - icon: const Icon(Icons.edit), + subtitle: Text('${item.book.localSubPaths.length} 页'), + suffix: FButton.icon( + variant: .ghost, + onPress: () => _editFileName(context, item), + child: const Icon(FLucideIcons.pencil), ), - ], - ); - }, + ); + }, + ), ), - ), - // 底部导出按钮 - SafeArea( - child: Padding( - padding: const EdgeInsets.all(16), - child: FilledButton.icon( - onPressed: vm.isExporting ? null : () => vm.export(), - icon: vm.isExporting - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Icon(Icons.upload), - label: Text(vm.isExporting ? '导出中...' : '开始批量导出'), + // 底部导出按钮 + SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: FButton( + onPress: state.isExporting + ? null + : () { + if (_formKey.currentState!.validate()) { + notifier.doExport(); + } + }, + prefix: state.isExporting + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.upload), + child: Text(state.isExporting ? '导出中...' : '开始批量导出'), + ), ), ), + ], + ), + ), + ); + } + + void _editFileName(BuildContext context, ExportItem item) { + showFDialog( + context: context, + builder: (dialogContext, style, animate) => FDialog.adaptive( + style: style, + animation: animate, + horizontalBuilder: (context, dStyle) => Padding( + padding: .all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('编辑导出文件名', style: dStyle.titleTextStyle), + const SizedBox(height: 12), + FTextFormField( + control: FTextFieldControl.managed( + controller: item.nameController, + ), + label: Text('导出文件名'), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FButton( + variant: .outline, + onPress: () => Navigator.of(dialogContext).pop(), + child: Text('取消'), + ), + const SizedBox(width: 8), + FButton( + onPress: () => Navigator.of(dialogContext).pop(), + child: Text('确定'), + ), + ], + ), + ], + ), + ), + verticalBuilder: (context, dStyle) => Padding( + padding: .all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('编辑导出文件名', style: dStyle.titleTextStyle), + const SizedBox(height: 12), + FTextFormField( + control: FTextFieldControl.managed( + controller: item.nameController, + ), + label: Text('导出文件名'), + ), + const SizedBox(height: 16), + FButton( + onPress: () => Navigator.of(dialogContext).pop(), + child: Text('确定'), + ), + const SizedBox(height: 8), + FButton( + variant: .outline, + onPress: () => Navigator.of(dialogContext).pop(), + child: Text('取消'), + ), + ], ), - ], + ), ), ); } diff --git a/lib/feature/export/ui/view/export_single_form_view.dart b/lib/feature/export/ui/view/export_single_form_view.dart index c3dc316..9817c33 100644 --- a/lib/feature/export/ui/view/export_single_form_view.dart +++ b/lib/feature/export/ui/view/export_single_form_view.dart @@ -1,138 +1,146 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tele_book/common/config/global_config.dart'; +import 'package:tele_book/common/widget/local_image_widget.dart'; import 'package:tele_book/core/db/app_database.dart'; import 'package:tele_book/feature/export/enum/export_format.dart'; -import 'package:tele_book/feature/export/ui/viewmodel/export_single_viewmodel.dart'; +import 'package:tele_book/feature/export/ui/provider/export_single_provider.dart'; -class ExportSingleFormView extends StatelessWidget { +class ExportSingleFormView extends ConsumerStatefulWidget { final BookTableData book; const ExportSingleFormView({super.key, required this.book}); @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (_) => ExportSingleViewmodel(book: book), - child: const _ExportSingleFormContent(), - ); - } + ConsumerState createState() => + _ExportSingleFormViewState(); } -class _ExportSingleFormContent extends StatelessWidget { - const _ExportSingleFormContent(); +class _ExportSingleFormViewState extends ConsumerState { + final _formKey = GlobalKey(); @override Widget build(BuildContext context) { - final vm = context.watch(); + final bookId = widget.book.id; + final state = ref.watch(exportSingleProvider(bookId)); + final notifier = ref.read(exportSingleProvider(bookId).notifier); - // 成功后弹提示并返回 - if (vm.isDone) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('导出成功!'), - backgroundColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - ), + ref.listen(exportSingleProvider(bookId), (prev, next) { + if (prev?.isDone == false && next.isDone) { + showFToast( + context: context, + icon: const Icon(FLucideIcons.check), + title: const Text('导出成功'), + description: Text('${state.book.name} 已导出'), + swipeToDismiss: const [.right], + duration: const Duration(seconds: 3), ); Navigator.of(context).pop(); - }); - } + } + }); - return Scaffold( - appBar: AppBar(title: const Text('导出书籍')), - body: Padding( + return FScaffold( + header: FHeader.nested( + title: const Text('导出书籍'), + prefixes: [FHeaderAction.back(onPress: () => context.pop())], + ), + child: Padding( padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 书籍名提示 - ListTile( - contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.book), - title: Text(vm.book.name), - subtitle: Text('共 ${vm.book.localSubPaths.length} 页'), - ), - const Divider(), - const SizedBox(height: 8), - - // 导出格式 - LayoutBuilder( - builder: (context, constraints) => DropdownMenu( - width: constraints.maxWidth, - initialSelection: vm.format, - label: const Text('导出格式'), - leadingIcon: const Icon(Icons.file_present), - onSelected: (v) { - if (v != null) vm.setFormat(v); - }, - dropdownMenuEntries: ExportFormat.values - .map((f) => DropdownMenuEntry(value: f, label: f.label)) - .toList(), + child: Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FLabel( + layout: .horizontalTrailing, + label: Text(state.book.name), + description: Text('共 ${state.book.localSubPaths.length} 页'), + child: LocalImageWidget( + imagePath: GlobalConfig.resolveBookPath( + state.book.coverSubPath!, + ), + ), ), - ), - const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 8), - // 导出路径 - TextField( - readOnly: true, - controller: TextEditingController(text: vm.outputPath ?? ''), - decoration: InputDecoration( - labelText: '导出路径', - hintText: '请选择导出目录', - prefixIcon: const Icon(Icons.folder_open), - border: const OutlineInputBorder(), - suffixIcon: IconButton( - icon: const Icon(Icons.drive_folder_upload), - onPressed: vm.isExporting ? null : vm.pickOutputDir, + // 导出格式 + FSelect.rich( + control: FSelectControl.managed( + onChange: (value) { + if (value != null) notifier.setFormat(value); + }, ), + label: Text("导出格式"), + hint: "请选择导出格式", + format: (s) => s.label, + children: [ + for (final format in ExportFormat.values) + .item(title: Text(format.label), value: format), + ], ), - ), - const SizedBox(height: 16), + const SizedBox(height: 16), - // 导出文件名 - TextField( - controller: vm.fileNameController, - enabled: !vm.isExporting, - decoration: const InputDecoration( - labelText: '导出文件名', - prefixIcon: Icon(Icons.drive_file_rename_outline), - border: OutlineInputBorder(), + // 导出路径 + FTextFormField( + readOnly: true, + control: FTextFieldControl.managed( + controller: state.outputPathCrl, + ), + validator: (v) => (v == null || v.isEmpty) ? '请选择导出目录' : null, + label: Text('导出路径'), + hint: '请选择导出目录', + suffixBuilder: (context, style, variants) { + return FButton.icon( + style: style.obscureButtonStyle, + onPress: () => notifier.pickOutputDir(), + child: Icon(FLucideIcons.fileOutput), + ); + }, + onTap: () => notifier.pickOutputDir(), ), - ), - const SizedBox(height: 8), + const SizedBox(height: 16), - // 错误提示 - if (vm.errorMessage != null) - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Text( - vm.errorMessage!, - style: TextStyle(color: Theme.of(context).colorScheme.error), + // 导出文件名 + FTextFormField( + control: FTextFieldControl.managed( + controller: state.fileNameCrl, ), + validator: (v) => + (v == null || v.trim().isEmpty) ? '请输入文件名' : null, + enabled: !state.isExporting, + label: Text('导出文件名'), + hint: "请输入导出文件名", ), + const SizedBox(height: 8), - const Spacer(), + const Spacer(), - // 导出按钮 - FilledButton.icon( - onPressed: vm.isExporting ? null : () => vm.export(), - icon: vm.isExporting - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Icon(Icons.upload), - label: Text(vm.isExporting ? '导出中...' : '开始导出'), - ), - const SizedBox(height: 16), - ], + // 导出按钮 + FButton( + onPress: () { + if (_formKey.currentState!.validate()) { + notifier.doExport(); + } + }, + prefix: state.isExporting + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.upload), + child: Text(state.isExporting ? '导出中...' : '开始导出'), + ), + const SizedBox(height: 16), + ], + ), ), ), ); diff --git a/lib/feature/export/ui/viewmodel/export_batch_viewmodel.dart b/lib/feature/export/ui/viewmodel/export_batch_viewmodel.dart index 1294679..78452b2 100644 --- a/lib/feature/export/ui/viewmodel/export_batch_viewmodel.dart +++ b/lib/feature/export/ui/viewmodel/export_batch_viewmodel.dart @@ -23,7 +23,9 @@ class ExportBatchViewmodel extends ChangeNotifier { .map( (b) => ExportItem( book: b, - coverPath: "${GlobalConfig.booksDir.path}/${b.localSubPaths.first}", + coverPath: b.coverSubPath != null + ? '${GlobalConfig.booksDir.path}/${b.coverSubPath}' + : '${GlobalConfig.booksDir.path}/${b.localSubPaths.first}', ), ) .toList(); diff --git a/lib/feature/main/provider/main_provider.dart b/lib/feature/main/provider/main_provider.dart new file mode 100644 index 0000000..fa62f1f --- /dev/null +++ b/lib/feature/main/provider/main_provider.dart @@ -0,0 +1,24 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'main_provider.freezed.dart'; +part 'main_provider.g.dart'; + +@freezed +abstract class MainState with _$MainState { + const factory MainState({ + @Default(0) int currentIndex, + }) = _MainState; +} + +@riverpod +class Main extends _$Main { + @override + MainState build() { + return const MainState(); + } + + void updateCurrentIndex(int index) { + state = state.copyWith(currentIndex: index); + } +} \ No newline at end of file diff --git a/lib/feature/main/provider/main_provider.freezed.dart b/lib/feature/main/provider/main_provider.freezed.dart new file mode 100644 index 0000000..0a0565e --- /dev/null +++ b/lib/feature/main/provider/main_provider.freezed.dart @@ -0,0 +1,271 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'main_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$MainState { + + int get currentIndex; +/// Create a copy of MainState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$MainStateCopyWith get copyWith => _$MainStateCopyWithImpl(this as MainState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is MainState&&(identical(other.currentIndex, currentIndex) || other.currentIndex == currentIndex)); +} + + +@override +int get hashCode => Object.hash(runtimeType,currentIndex); + +@override +String toString() { + return 'MainState(currentIndex: $currentIndex)'; +} + + +} + +/// @nodoc +abstract mixin class $MainStateCopyWith<$Res> { + factory $MainStateCopyWith(MainState value, $Res Function(MainState) _then) = _$MainStateCopyWithImpl; +@useResult +$Res call({ + int currentIndex +}); + + + + +} +/// @nodoc +class _$MainStateCopyWithImpl<$Res> + implements $MainStateCopyWith<$Res> { + _$MainStateCopyWithImpl(this._self, this._then); + + final MainState _self; + final $Res Function(MainState) _then; + +/// Create a copy of MainState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? currentIndex = null,}) { + return _then(_self.copyWith( +currentIndex: null == currentIndex ? _self.currentIndex : currentIndex // ignore: cast_nullable_to_non_nullable +as int, + )); +} + +} + + +/// Adds pattern-matching-related methods to [MainState]. +extension MainStatePatterns on MainState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _MainState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _MainState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _MainState value) $default,){ +final _that = this; +switch (_that) { +case _MainState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _MainState value)? $default,){ +final _that = this; +switch (_that) { +case _MainState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( int currentIndex)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _MainState() when $default != null: +return $default(_that.currentIndex);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( int currentIndex) $default,) {final _that = this; +switch (_that) { +case _MainState(): +return $default(_that.currentIndex);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int currentIndex)? $default,) {final _that = this; +switch (_that) { +case _MainState() when $default != null: +return $default(_that.currentIndex);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _MainState implements MainState { + const _MainState({this.currentIndex = 0}); + + +@override@JsonKey() final int currentIndex; + +/// Create a copy of MainState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$MainStateCopyWith<_MainState> get copyWith => __$MainStateCopyWithImpl<_MainState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _MainState&&(identical(other.currentIndex, currentIndex) || other.currentIndex == currentIndex)); +} + + +@override +int get hashCode => Object.hash(runtimeType,currentIndex); + +@override +String toString() { + return 'MainState(currentIndex: $currentIndex)'; +} + + +} + +/// @nodoc +abstract mixin class _$MainStateCopyWith<$Res> implements $MainStateCopyWith<$Res> { + factory _$MainStateCopyWith(_MainState value, $Res Function(_MainState) _then) = __$MainStateCopyWithImpl; +@override @useResult +$Res call({ + int currentIndex +}); + + + + +} +/// @nodoc +class __$MainStateCopyWithImpl<$Res> + implements _$MainStateCopyWith<$Res> { + __$MainStateCopyWithImpl(this._self, this._then); + + final _MainState _self; + final $Res Function(_MainState) _then; + +/// Create a copy of MainState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? currentIndex = null,}) { + return _then(_MainState( +currentIndex: null == currentIndex ? _self.currentIndex : currentIndex // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + +// dart format on diff --git a/lib/feature/main/provider/main_provider.g.dart b/lib/feature/main/provider/main_provider.g.dart new file mode 100644 index 0000000..f113f8d --- /dev/null +++ b/lib/feature/main/provider/main_provider.g.dart @@ -0,0 +1,61 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'main_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(Main) +final mainProvider = MainProvider._(); + +final class MainProvider extends $NotifierProvider { + MainProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'mainProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$mainHash(); + + @$internal + @override + Main create() => Main(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(MainState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$mainHash() => r'd66107f193872b3658dcb2e8d98aa82eead28fca'; + +abstract class _$Main extends $Notifier { + MainState build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + MainState, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/feature/main/view/main_view.dart b/lib/feature/main/view/main_view.dart new file mode 100644 index 0000000..c329a2f --- /dev/null +++ b/lib/feature/main/view/main_view.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; +import 'package:tele_book/feature/book/ui/view/book_list_view.dart'; +import 'package:tele_book/feature/collection/ui/view/collection_view.dart'; +import 'package:tele_book/feature/download/ui/view/download_list_view.dart'; +import 'package:tele_book/feature/main/provider/main_provider.dart'; + +class MainView extends ConsumerWidget { + const MainView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(mainProvider); + final notifier = ref.read(mainProvider.notifier); + return FScaffold( + footer: FBottomNavigationBar( + index: state.currentIndex, + onChange: (index) => notifier.updateCurrentIndex(index), + children: [ + FBottomNavigationBarItem(icon: Icon(Icons.book), label: Text("书籍")), + FBottomNavigationBarItem( + icon: Icon(Icons.download), + label: Text("下载"), + ), + FBottomNavigationBarItem( + icon: Icon(Icons.collections), + label: Text("收藏夹"), + ), + ], + ), + child: IndexedStack( + index: state.currentIndex, + children: [BookListView(), DownloadListView(), CollectionView()], + ), + ); + } +} diff --git a/lib/feature/parse/service/parse_archive_service.dart b/lib/feature/parse/service/parse_archive_service.dart index 0f2ff37..547388d 100644 --- a/lib/feature/parse/service/parse_archive_service.dart +++ b/lib/feature/parse/service/parse_archive_service.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tele_book/common/config/global_config.dart'; import 'package:tele_book/core/util/failure_util.dart'; import 'package:tele_book/core/util/result_util.dart'; @@ -8,79 +9,13 @@ import 'package:tele_book/feature/parse/model/parse_batch_archive_vo.dart'; import 'package:uuid/uuid.dart'; import 'package:archive/archive_io.dart'; -// ── 顶层函数,供 compute() 在后台 Isolate 中调用 ────────── -// Isolate 中不能访问闭包,必须是 top-level / static -Future _extractInBackground(List args) async { - final archivePath = args[0]; - final outputDir = args[1]; - await extractFileToDisk(archivePath, outputDir); -} - -/// 在后台 Isolate 中扫描目录,返回图片路径列表 -List _collectImagePathsSync(String dirPath) { - final dir = Directory(dirPath); - final result = []; - for (final entity in dir.listSync(recursive: true, followLinks: false)) { - if (entity is File && _isImageFileStatic(entity.path)) { - result.add(entity.path); - } - } - result.sort(); - return result; -} - -List _collectDirectImagePathsSync(String dirPath) { - final dir = Directory(dirPath); - final result = []; - for (final entity in dir.listSync(recursive: false, followLinks: false)) { - if (entity is File && _isImageFileStatic(entity.path)) { - result.add(entity.path); - } - } - result.sort(); - return result; -} - -List _collectSubDirectoryPathsSync(String dirPath) { - final dir = Directory(dirPath); - final result = []; - for (final entity in dir.listSync(recursive: true, followLinks: false)) { - if (entity is Directory) { - result.add(entity.path); - } - } - result.sort(); - return result; -} - -bool _isImageFileStatic(String path) { - final p = path.toLowerCase(); - return p.endsWith('.jpg') || - p.endsWith('.jpeg') || - p.endsWith('.png') || - p.endsWith('.gif') || - p.endsWith('.bmp') || - p.endsWith('.webp'); -} - -String _baseName(String path) { - return path.split(RegExp(r'[\\/]')).last; -} - -String _dirName(String path) { - final normalized = path.replaceAll('\\', '/'); - final index = normalized.lastIndexOf('/'); - if (index <= 0) return ''; - return normalized.substring(0, index); -} +final parseArchiveServiceProvider = Provider((ref) => ParseArchiveService()); class ParseArchiveService { Future>> parseImagePaths(List imagePaths) async { try { - final images = imagePaths - .where((path) => _isImageFileStatic(path)) - .toList() - ..sort(); + final images = + imagePaths.where((path) => _isImageFileStatic(path)).toList()..sort(); return Result.success(images); } catch (e, st) { return Result.failure( @@ -92,24 +27,34 @@ class ParseArchiveService { Future>> _parseBatchArchivePaths( List archivePaths, Function(int total) onStart, - Function(int count) onProgress, - ) async { + Function(int count) onProgress, { + void Function(String currentName)? onCurrentItemChanged, + void Function(int current, int total)? onCurrentItemProgress, + }) async { try { - final filteredPaths = archivePaths - .where((path) => path.toLowerCase().endsWith('.zip')) - .toList() - ..sort(); + final filteredPaths = + archivePaths + .where((path) => path.toLowerCase().endsWith('.zip')) + .toList() + ..sort(); onStart(filteredPaths.length); final results = []; for (var index = 0; index < filteredPaths.length; index++) { final path = filteredPaths[index]; + onCurrentItemChanged?.call(_baseName(path)); + onCurrentItemProgress?.call(0, 2); // 每处理一个文件,让出事件循环,让 UI / GC 有机会运行 await Future.delayed(Duration.zero); - final parseResult = await parseArchive(path); + final parseResult = await parseArchive( + path, + onProgress: (current, total) { + onCurrentItemProgress?.call(current, total); + }, + ); if (parseResult.isSuccess) { results.add( ParseBatchArchiveVo( @@ -148,15 +93,22 @@ class ParseArchiveService { } // ── 单压缩包解析 ────────────────────────────────────── - Future>> parseArchive(String archivePath) async { + Future>> parseArchive( + String archivePath, { + void Function(int current, int total)? onProgress, + }) async { try { - final tempOutputDir = "${GlobalConfig.appTempDir.path}/${const Uuid().v4()}"; + final tempOutputDir = + "${GlobalConfig.appTempDir.path}/${const Uuid().v4()}"; + onProgress?.call(0, 2); // ① 解压放后台 Isolate(最重,可能 OOM,独立内存空间更安全) await compute(_extractInBackground, [archivePath, tempOutputDir]); + onProgress?.call(1, 2); // ② 扫描解压后的目录,也放后台 Isolate final imagePaths = await compute(_collectImagePathsSync, tempOutputDir); + onProgress?.call(2, 2); return Result.success(imagePaths); } catch (e, st) { @@ -170,8 +122,10 @@ class ParseArchiveService { Future>> parseBatchArchives( String archiveDirPath, Function(int total) onStart, - Function(int count) onProgress, - ) async { + Function(int count) onProgress, { + void Function(String currentName)? onCurrentItemChanged, + void Function(int current, int total)? onCurrentItemProgress, + }) async { try { final archiveDir = Directory(archiveDirPath); if (!await archiveDir.exists()) { @@ -185,7 +139,13 @@ class ParseArchiveService { .map((e) => e.path) .toList(); - return _parseBatchArchivePaths(archivePaths, onStart, onProgress); + return _parseBatchArchivePaths( + archivePaths, + onStart, + onProgress, + onCurrentItemChanged: onCurrentItemChanged, + onCurrentItemProgress: onCurrentItemProgress, + ); } catch (e, st) { return Result.failure( BusinessFailure(message: "批量解析压缩包失败", details: e, stackTrace: st), @@ -196,17 +156,27 @@ class ParseArchiveService { Future>> parseBatchArchivesFromPaths( List archivePaths, Function(int total) onStart, - Function(int count) onProgress, - ) { - return _parseBatchArchivePaths(archivePaths, onStart, onProgress); + Function(int count) onProgress, { + void Function(String currentName)? onCurrentItemChanged, + void Function(int current, int total)? onCurrentItemProgress, + }) { + return _parseBatchArchivePaths( + archivePaths, + onStart, + onProgress, + onCurrentItemChanged: onCurrentItemChanged, + onCurrentItemProgress: onCurrentItemProgress, + ); } // ── 批量文件夹解析 ──────────────────────────────────── Future>> parseBatchImageFolders( String parentDirPath, Function(int total) onStart, - Function(int count) onProgress, - ) async { + Function(int count) onProgress, { + void Function(String currentName)? onCurrentItemChanged, + void Function(int current, int total)? onCurrentItemProgress, + }) async { try { final parentDir = Directory(parentDirPath); if (!await parentDir.exists()) { @@ -214,13 +184,18 @@ class ParseArchiveService { } // 遍历父目录下的所有子文件夹(不包含父目录自身) - final folders = await compute(_collectSubDirectoryPathsSync, parentDirPath); + final folders = await compute( + _collectSubDirectoryPathsSync, + parentDirPath, + ); onStart(folders.length); final results = []; for (var index = 0; index < folders.length; index++) { final folderPath = folders[index]; + onCurrentItemChanged?.call(_baseName(folderPath)); + onCurrentItemProgress?.call(0, 1); // 让出事件循环 await Future.delayed(Duration.zero); @@ -229,12 +204,10 @@ class ParseArchiveService { final images = await compute(_collectDirectImagePathsSync, folderPath); if (images.length > 1) { results.add( - ParseBatchArchiveVo( - name: _baseName(folderPath), - tempPaths: images, - ), + ParseBatchArchiveVo(name: _baseName(folderPath), tempPaths: images), ); } + onCurrentItemProgress?.call(1, 1); onProgress(index + 1); } return Result.success(results); @@ -248,8 +221,10 @@ class ParseArchiveService { Future>> parseBatchImageFoldersFromPaths( List imagePaths, Function(int total) onStart, - Function(int count) onProgress, - ) async { + Function(int count) onProgress, { + void Function(String currentName)? onCurrentItemChanged, + void Function(int current, int total)? onCurrentItemProgress, + }) async { try { final grouped = >{}; for (final path in imagePaths) { @@ -264,6 +239,8 @@ class ParseArchiveService { final results = []; for (var index = 0; index < keys.length; index++) { final key = keys[index]; + onCurrentItemChanged?.call(key.isEmpty ? '未命名文件夹' : _baseName(key)); + onCurrentItemProgress?.call(0, 1); final paths = grouped[key]!..sort(); if (paths.length > 1) { results.add( @@ -273,6 +250,7 @@ class ParseArchiveService { ), ); } + onCurrentItemProgress?.call(1, 1); onProgress(index + 1); await Future.delayed(Duration.zero); } @@ -286,4 +264,68 @@ class ParseArchiveService { } } +// ── 顶层函数,供 compute() 在后台 Isolate 中调用 ────────── +// Isolate 中不能访问闭包,必须是 top-level / static +Future _extractInBackground(List args) async { + final archivePath = args[0]; + final outputDir = args[1]; + await extractFileToDisk(archivePath, outputDir); +} + +/// 在后台 Isolate 中扫描目录,返回图片路径列表 +List _collectImagePathsSync(String dirPath) { + final dir = Directory(dirPath); + final result = []; + for (final entity in dir.listSync(recursive: true, followLinks: false)) { + if (entity is File && _isImageFileStatic(entity.path)) { + result.add(entity.path); + } + } + result.sort(); + return result; +} + +List _collectDirectImagePathsSync(String dirPath) { + final dir = Directory(dirPath); + final result = []; + for (final entity in dir.listSync(recursive: false, followLinks: false)) { + if (entity is File && _isImageFileStatic(entity.path)) { + result.add(entity.path); + } + } + result.sort(); + return result; +} + +List _collectSubDirectoryPathsSync(String dirPath) { + final dir = Directory(dirPath); + final result = []; + for (final entity in dir.listSync(recursive: true, followLinks: false)) { + if (entity is Directory) { + result.add(entity.path); + } + } + result.sort(); + return result; +} +bool _isImageFileStatic(String path) { + final p = path.toLowerCase(); + return p.endsWith('.jpg') || + p.endsWith('.jpeg') || + p.endsWith('.png') || + p.endsWith('.gif') || + p.endsWith('.bmp') || + p.endsWith('.webp'); +} + +String _baseName(String path) { + return path.split(RegExp(r'[\\/]')).last; +} + +String _dirName(String path) { + final normalized = path.replaceAll('\\', '/'); + final index = normalized.lastIndexOf('/'); + if (index <= 0) return ''; + return normalized.substring(0, index); +} diff --git a/lib/feature/parse/service/parse_pdf_service.dart b/lib/feature/parse/service/parse_pdf_service.dart index 6196ee7..661385e 100644 --- a/lib/feature/parse/service/parse_pdf_service.dart +++ b/lib/feature/parse/service/parse_pdf_service.dart @@ -3,14 +3,17 @@ import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; -import 'package:path/path.dart' as p; -import 'package:pdfrx/pdfrx.dart'; import 'package:tele_book/common/config/global_config.dart'; import 'package:tele_book/core/util/failure_util.dart'; import 'package:tele_book/core/util/result_util.dart'; import 'package:tele_book/feature/parse/model/parse_batch_archive_vo.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path/path.dart' as p; +import 'package:pdfrx/pdfrx.dart'; import 'package:uuid/uuid.dart'; +final parsePdfServiceProvider = Provider((ref) => ParsePdfService()); + class ParsePdfService { static bool _pdfrxInitialized = false; @@ -44,6 +47,13 @@ class ParsePdfService { String pdfPath, { void Function(int current, int total)? onProgress, }) async { + final sourceFile = File(pdfPath); + if (!await sourceFile.exists()) { + return Result.failure( + BusinessFailure(message: 'PDF 文件不存在或无访问权限: $pdfPath'), + ); + } + final tempDir = p.join(GlobalConfig.appTempDir.path, const Uuid().v4()); await Directory(tempDir).create(recursive: true); @@ -106,29 +116,36 @@ class ParsePdfService { Future>> parseBatchPdfs( String pdfDirPath, void Function(int total) onStart, - void Function(int count) onProgress, - ) async { + void Function(int count) onProgress, { + void Function(String fileName)? onCurrentFileChanged, + void Function(int current, int total)? onCurrentFileProgress, + }) async { try { final dir = Directory(pdfDirPath); if (!await dir.exists()) { throw FileSystemException('目录不存在', pdfDirPath); } - final pdfPaths = await dir - .list() - .where((e) => e is File && e.path.toLowerCase().endsWith('.pdf')) - .map((e) => e.path) - .toList() - ..sort(); - - return _parseBatchPdfsByPaths(pdfPaths, onStart, onProgress); + final pdfPaths = + await dir + .list() + .where( + (e) => e is File && e.path.toLowerCase().endsWith('.pdf'), + ) + .map((e) => e.path) + .toList() + ..sort(); + + return _parseBatchPdfsByPaths( + pdfPaths, + onStart, + onProgress, + onCurrentFileChanged: onCurrentFileChanged, + onCurrentFileProgress: onCurrentFileProgress, + ); } catch (e, st) { return Result.failure( - BusinessFailure( - message: '批量解析 PDF 失败', - details: e, - stackTrace: st, - ), + BusinessFailure(message: '批量解析 PDF 失败', details: e, stackTrace: st), ); } } @@ -136,19 +153,29 @@ class ParsePdfService { Future>> parseBatchPdfsFromPaths( List pdfPaths, void Function(int total) onStart, - void Function(int count) onProgress, - ) { + void Function(int count) onProgress, { + void Function(String fileName)? onCurrentFileChanged, + void Function(int current, int total)? onCurrentFileProgress, + }) { final filtered = pdfPaths .where((path) => path.toLowerCase().endsWith('.pdf')) .toList(); - return _parseBatchPdfsByPaths(filtered, onStart, onProgress); + return _parseBatchPdfsByPaths( + filtered, + onStart, + onProgress, + onCurrentFileChanged: onCurrentFileChanged, + onCurrentFileProgress: onCurrentFileProgress, + ); } Future>> _parseBatchPdfsByPaths( List pdfPaths, void Function(int total) onStart, - void Function(int count) onProgress, - ) async { + void Function(int count) onProgress, { + void Function(String fileName)? onCurrentFileChanged, + void Function(int current, int total)? onCurrentFileProgress, + }) async { try { pdfPaths.sort(); @@ -158,8 +185,15 @@ class ParsePdfService { for (var i = 0; i < pdfPaths.length; i++) { await Future.delayed(Duration.zero); final path = pdfPaths[i]; - - final result = await parsePdf(path); + onCurrentFileChanged?.call(p.basename(path)); + onCurrentFileProgress?.call(0, 0); + + final result = await parsePdf( + path, + onProgress: (current, total) { + onCurrentFileProgress?.call(current, total); + }, + ); if (result.isSuccess) { results.add( ParseBatchArchiveVo( @@ -176,13 +210,8 @@ class ParsePdfService { return Result.success(results); } catch (e, st) { return Result.failure( - BusinessFailure( - message: '批量解析 PDF 失败', - details: e, - stackTrace: st, - ), + BusinessFailure(message: '批量解析 PDF 失败', details: e, stackTrace: st), ); } } } - diff --git a/lib/feature/parse/service/parse_web_service.dart b/lib/feature/parse/service/parse_web_service.dart index 32f94b4..2263e4d 100644 --- a/lib/feature/parse/service/parse_web_service.dart +++ b/lib/feature/parse/service/parse_web_service.dart @@ -1,6 +1,11 @@ import 'dart:convert'; import 'package:dio/dio.dart'; +import 'package:riverpod/riverpod.dart'; + +final parseWebServiceProvider = Provider((ref) { + return ParseWebService(); +}); class ParseWebService { Future extractTitleFromWebView({ @@ -29,7 +34,7 @@ class ParseWebService { return title.replaceAll(RegExp(r'[<>:"/\\|?*]'), '').trim(); } - Future> extractImagesFromWebView({ + Future> extractImagesFromWebView({ required Future Function(String) onExtractImages, }) async { final js = r""" @@ -80,7 +85,7 @@ class ParseWebService { } } - Future downloadImageToFile(String url, String saveDir) async { + Future downloadImageToFile(String url, String saveDir) async { final filePath = '$saveDir/${DateTime.now().microsecondsSinceEpoch}_${url.split('/').last}.jpg'; final dio = Dio(); diff --git a/lib/feature/parse/ui/provider/parse_archive_provider.dart b/lib/feature/parse/ui/provider/parse_archive_provider.dart new file mode 100644 index 0000000..95b7919 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_archive_provider.dart @@ -0,0 +1,157 @@ +import 'dart:io'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod/legacy.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tele_book/feature/book/model/dto/save_as_book_dto.dart'; +import 'package:tele_book/feature/book/repository/book_repository.dart'; +import 'package:tele_book/feature/parse/service/parse_archive_service.dart'; + +part 'parse_archive_provider.freezed.dart'; + +part 'parse_archive_provider.g.dart'; + +@freezed +abstract class ParseArchiveState with _$ParseArchiveState { + const factory ParseArchiveState({ + required String archiveName, + required List tempPaths, + }) = _ParseArchiveState; +} + +@freezed +abstract class ParseArchiveSaveBookParam with _$ParseArchiveSaveBookParam { + const factory ParseArchiveSaveBookParam({ + required String archiveName, + required List tempPaths, + }) = _ParseArchiveSaveBookParam; +} + +@freezed +abstract class ParseArchiveSaveBookProgress + with _$ParseArchiveSaveBookProgress { + const factory ParseArchiveSaveBookProgress({ + @Default(SaveStep.generateCover) SaveStep step, + @Default(0) int current, + @Default(0) int total, + }) = _ParseArchiveSaveBookProgress; + + const ParseArchiveSaveBookProgress._(); + + String get stepText => switch (step) { + SaveStep.generateCover => '生成封面图...', + SaveStep.generatePreview => '生成预览图... ($current/$total)', + SaveStep.saveOriginal => '保存原图... ($current/$total)', + SaveStep.saveDatabase => '保存中...', + }; +} + +final parseArchiveProgressProvider = + StateProvider.family<(int current, int total), String>((_, __) => (0, 0)); + +final parseArchiveSaveBookProgressProvider = + StateProvider( + (_) => const ParseArchiveSaveBookProgress(), + ); + +@riverpod +class ParseArchive extends _$ParseArchive { + ParseArchiveService get _parseArchiveService => + ref.read(parseArchiveServiceProvider); + + @override + FutureOr build(String archivePath) async { + final archiveName = archivePath.split(RegExp(r'[\\/]')).last; + Future.microtask(() => _parseArchive(archivePath)); + return ParseArchiveState(archiveName: archiveName, tempPaths: const []); + } + + Future _requestStoragePermission() async { + if (!Platform.isAndroid) return true; + if (await Permission.manageExternalStorage.isGranted) return true; + final status = await Permission.manageExternalStorage.request(); + if (status.isGranted) return true; + if (await Permission.storage.isGranted) return true; + final storageStatus = await Permission.storage.request(); + return storageStatus.isGranted; + } + + Future _parseArchive(String archivePath) async { + final hasPermission = await _requestStoragePermission(); + if (!ref.mounted) return; + if (!hasPermission) { + state = AsyncError('需要存储权限才能解析压缩包', StackTrace.current); + return; + } + + final archiveName = archivePath.split(RegExp(r'[\\/]')).last; + + ref.read(parseArchiveProgressProvider(archivePath).notifier).state = (0, 0); + state = const AsyncLoading(); + + final result = await _parseArchiveService.parseArchive( + archivePath, + onProgress: (current, total) { + ref.read(parseArchiveProgressProvider(archivePath).notifier).state = ( + current, + total, + ); + }, + ); + + if (!ref.mounted) return; + result.fold( + onSuccess: (data) { + state = AsyncData( + ParseArchiveState(archiveName: archiveName, tempPaths: data), + ); + }, + onError: (error) { + state = AsyncError(error.message, StackTrace.current); + }, + ); + } +} + +@riverpod +class ParseArchiveSaveBook extends _$ParseArchiveSaveBook { + BookRepository get _bookRepository => ref.read(bookRepositoryProvider); + + @override + FutureOr build() => null; + + Future submit(ParseArchiveSaveBookParam param) async { + if (state.isLoading || param.tempPaths.isEmpty) return; + + state = const AsyncLoading(); + ref.read(parseArchiveSaveBookProgressProvider.notifier).state = + ParseArchiveSaveBookProgress( + step: SaveStep.generateCover, + current: 0, + total: param.tempPaths.length, + ); + + final result = await _bookRepository.saveAsBook( + SaveAsBookDto(title: param.archiveName, paths: param.tempPaths), + onStepProgress: (step, current, total) { + ref.read(parseArchiveSaveBookProgressProvider.notifier).state = + ParseArchiveSaveBookProgress( + step: step, + current: current, + total: total, + ); + }, + ); + + result.fold( + onSuccess: (_) { + state = const AsyncData(null); + }, + onError: (error) { + state = AsyncError(error.message, StackTrace.current); + }, + ); + } +} diff --git a/lib/feature/parse/ui/provider/parse_archive_provider.freezed.dart b/lib/feature/parse/ui/provider/parse_archive_provider.freezed.dart new file mode 100644 index 0000000..cdfaa13 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_archive_provider.freezed.dart @@ -0,0 +1,809 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'parse_archive_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ParseArchiveState { + + String get archiveName; List get tempPaths; +/// Create a copy of ParseArchiveState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseArchiveStateCopyWith get copyWith => _$ParseArchiveStateCopyWithImpl(this as ParseArchiveState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseArchiveState&&(identical(other.archiveName, archiveName) || other.archiveName == archiveName)&&const DeepCollectionEquality().equals(other.tempPaths, tempPaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,archiveName,const DeepCollectionEquality().hash(tempPaths)); + +@override +String toString() { + return 'ParseArchiveState(archiveName: $archiveName, tempPaths: $tempPaths)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseArchiveStateCopyWith<$Res> { + factory $ParseArchiveStateCopyWith(ParseArchiveState value, $Res Function(ParseArchiveState) _then) = _$ParseArchiveStateCopyWithImpl; +@useResult +$Res call({ + String archiveName, List tempPaths +}); + + + + +} +/// @nodoc +class _$ParseArchiveStateCopyWithImpl<$Res> + implements $ParseArchiveStateCopyWith<$Res> { + _$ParseArchiveStateCopyWithImpl(this._self, this._then); + + final ParseArchiveState _self; + final $Res Function(ParseArchiveState) _then; + +/// Create a copy of ParseArchiveState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? archiveName = null,Object? tempPaths = null,}) { + return _then(_self.copyWith( +archiveName: null == archiveName ? _self.archiveName : archiveName // ignore: cast_nullable_to_non_nullable +as String,tempPaths: null == tempPaths ? _self.tempPaths : tempPaths // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseArchiveState]. +extension ParseArchiveStatePatterns on ParseArchiveState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseArchiveState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseArchiveState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseArchiveState value) $default,){ +final _that = this; +switch (_that) { +case _ParseArchiveState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseArchiveState value)? $default,){ +final _that = this; +switch (_that) { +case _ParseArchiveState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String archiveName, List tempPaths)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseArchiveState() when $default != null: +return $default(_that.archiveName,_that.tempPaths);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String archiveName, List tempPaths) $default,) {final _that = this; +switch (_that) { +case _ParseArchiveState(): +return $default(_that.archiveName,_that.tempPaths);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String archiveName, List tempPaths)? $default,) {final _that = this; +switch (_that) { +case _ParseArchiveState() when $default != null: +return $default(_that.archiveName,_that.tempPaths);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseArchiveState implements ParseArchiveState { + const _ParseArchiveState({required this.archiveName, required final List tempPaths}): _tempPaths = tempPaths; + + +@override final String archiveName; + final List _tempPaths; +@override List get tempPaths { + if (_tempPaths is EqualUnmodifiableListView) return _tempPaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_tempPaths); +} + + +/// Create a copy of ParseArchiveState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseArchiveStateCopyWith<_ParseArchiveState> get copyWith => __$ParseArchiveStateCopyWithImpl<_ParseArchiveState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseArchiveState&&(identical(other.archiveName, archiveName) || other.archiveName == archiveName)&&const DeepCollectionEquality().equals(other._tempPaths, _tempPaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,archiveName,const DeepCollectionEquality().hash(_tempPaths)); + +@override +String toString() { + return 'ParseArchiveState(archiveName: $archiveName, tempPaths: $tempPaths)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseArchiveStateCopyWith<$Res> implements $ParseArchiveStateCopyWith<$Res> { + factory _$ParseArchiveStateCopyWith(_ParseArchiveState value, $Res Function(_ParseArchiveState) _then) = __$ParseArchiveStateCopyWithImpl; +@override @useResult +$Res call({ + String archiveName, List tempPaths +}); + + + + +} +/// @nodoc +class __$ParseArchiveStateCopyWithImpl<$Res> + implements _$ParseArchiveStateCopyWith<$Res> { + __$ParseArchiveStateCopyWithImpl(this._self, this._then); + + final _ParseArchiveState _self; + final $Res Function(_ParseArchiveState) _then; + +/// Create a copy of ParseArchiveState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? archiveName = null,Object? tempPaths = null,}) { + return _then(_ParseArchiveState( +archiveName: null == archiveName ? _self.archiveName : archiveName // ignore: cast_nullable_to_non_nullable +as String,tempPaths: null == tempPaths ? _self._tempPaths : tempPaths // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc +mixin _$ParseArchiveSaveBookParam { + + String get archiveName; List get tempPaths; +/// Create a copy of ParseArchiveSaveBookParam +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseArchiveSaveBookParamCopyWith get copyWith => _$ParseArchiveSaveBookParamCopyWithImpl(this as ParseArchiveSaveBookParam, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseArchiveSaveBookParam&&(identical(other.archiveName, archiveName) || other.archiveName == archiveName)&&const DeepCollectionEquality().equals(other.tempPaths, tempPaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,archiveName,const DeepCollectionEquality().hash(tempPaths)); + +@override +String toString() { + return 'ParseArchiveSaveBookParam(archiveName: $archiveName, tempPaths: $tempPaths)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseArchiveSaveBookParamCopyWith<$Res> { + factory $ParseArchiveSaveBookParamCopyWith(ParseArchiveSaveBookParam value, $Res Function(ParseArchiveSaveBookParam) _then) = _$ParseArchiveSaveBookParamCopyWithImpl; +@useResult +$Res call({ + String archiveName, List tempPaths +}); + + + + +} +/// @nodoc +class _$ParseArchiveSaveBookParamCopyWithImpl<$Res> + implements $ParseArchiveSaveBookParamCopyWith<$Res> { + _$ParseArchiveSaveBookParamCopyWithImpl(this._self, this._then); + + final ParseArchiveSaveBookParam _self; + final $Res Function(ParseArchiveSaveBookParam) _then; + +/// Create a copy of ParseArchiveSaveBookParam +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? archiveName = null,Object? tempPaths = null,}) { + return _then(_self.copyWith( +archiveName: null == archiveName ? _self.archiveName : archiveName // ignore: cast_nullable_to_non_nullable +as String,tempPaths: null == tempPaths ? _self.tempPaths : tempPaths // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseArchiveSaveBookParam]. +extension ParseArchiveSaveBookParamPatterns on ParseArchiveSaveBookParam { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseArchiveSaveBookParam value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseArchiveSaveBookParam() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseArchiveSaveBookParam value) $default,){ +final _that = this; +switch (_that) { +case _ParseArchiveSaveBookParam(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseArchiveSaveBookParam value)? $default,){ +final _that = this; +switch (_that) { +case _ParseArchiveSaveBookParam() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String archiveName, List tempPaths)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseArchiveSaveBookParam() when $default != null: +return $default(_that.archiveName,_that.tempPaths);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String archiveName, List tempPaths) $default,) {final _that = this; +switch (_that) { +case _ParseArchiveSaveBookParam(): +return $default(_that.archiveName,_that.tempPaths);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String archiveName, List tempPaths)? $default,) {final _that = this; +switch (_that) { +case _ParseArchiveSaveBookParam() when $default != null: +return $default(_that.archiveName,_that.tempPaths);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseArchiveSaveBookParam implements ParseArchiveSaveBookParam { + const _ParseArchiveSaveBookParam({required this.archiveName, required final List tempPaths}): _tempPaths = tempPaths; + + +@override final String archiveName; + final List _tempPaths; +@override List get tempPaths { + if (_tempPaths is EqualUnmodifiableListView) return _tempPaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_tempPaths); +} + + +/// Create a copy of ParseArchiveSaveBookParam +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseArchiveSaveBookParamCopyWith<_ParseArchiveSaveBookParam> get copyWith => __$ParseArchiveSaveBookParamCopyWithImpl<_ParseArchiveSaveBookParam>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseArchiveSaveBookParam&&(identical(other.archiveName, archiveName) || other.archiveName == archiveName)&&const DeepCollectionEquality().equals(other._tempPaths, _tempPaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,archiveName,const DeepCollectionEquality().hash(_tempPaths)); + +@override +String toString() { + return 'ParseArchiveSaveBookParam(archiveName: $archiveName, tempPaths: $tempPaths)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseArchiveSaveBookParamCopyWith<$Res> implements $ParseArchiveSaveBookParamCopyWith<$Res> { + factory _$ParseArchiveSaveBookParamCopyWith(_ParseArchiveSaveBookParam value, $Res Function(_ParseArchiveSaveBookParam) _then) = __$ParseArchiveSaveBookParamCopyWithImpl; +@override @useResult +$Res call({ + String archiveName, List tempPaths +}); + + + + +} +/// @nodoc +class __$ParseArchiveSaveBookParamCopyWithImpl<$Res> + implements _$ParseArchiveSaveBookParamCopyWith<$Res> { + __$ParseArchiveSaveBookParamCopyWithImpl(this._self, this._then); + + final _ParseArchiveSaveBookParam _self; + final $Res Function(_ParseArchiveSaveBookParam) _then; + +/// Create a copy of ParseArchiveSaveBookParam +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? archiveName = null,Object? tempPaths = null,}) { + return _then(_ParseArchiveSaveBookParam( +archiveName: null == archiveName ? _self.archiveName : archiveName // ignore: cast_nullable_to_non_nullable +as String,tempPaths: null == tempPaths ? _self._tempPaths : tempPaths // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc +mixin _$ParseArchiveSaveBookProgress { + + SaveStep get step; int get current; int get total; +/// Create a copy of ParseArchiveSaveBookProgress +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseArchiveSaveBookProgressCopyWith get copyWith => _$ParseArchiveSaveBookProgressCopyWithImpl(this as ParseArchiveSaveBookProgress, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseArchiveSaveBookProgress&&(identical(other.step, step) || other.step == step)&&(identical(other.current, current) || other.current == current)&&(identical(other.total, total) || other.total == total)); +} + + +@override +int get hashCode => Object.hash(runtimeType,step,current,total); + +@override +String toString() { + return 'ParseArchiveSaveBookProgress(step: $step, current: $current, total: $total)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseArchiveSaveBookProgressCopyWith<$Res> { + factory $ParseArchiveSaveBookProgressCopyWith(ParseArchiveSaveBookProgress value, $Res Function(ParseArchiveSaveBookProgress) _then) = _$ParseArchiveSaveBookProgressCopyWithImpl; +@useResult +$Res call({ + SaveStep step, int current, int total +}); + + + + +} +/// @nodoc +class _$ParseArchiveSaveBookProgressCopyWithImpl<$Res> + implements $ParseArchiveSaveBookProgressCopyWith<$Res> { + _$ParseArchiveSaveBookProgressCopyWithImpl(this._self, this._then); + + final ParseArchiveSaveBookProgress _self; + final $Res Function(ParseArchiveSaveBookProgress) _then; + +/// Create a copy of ParseArchiveSaveBookProgress +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? step = null,Object? current = null,Object? total = null,}) { + return _then(_self.copyWith( +step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable +as SaveStep,current: null == current ? _self.current : current // ignore: cast_nullable_to_non_nullable +as int,total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable +as int, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseArchiveSaveBookProgress]. +extension ParseArchiveSaveBookProgressPatterns on ParseArchiveSaveBookProgress { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseArchiveSaveBookProgress value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseArchiveSaveBookProgress() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseArchiveSaveBookProgress value) $default,){ +final _that = this; +switch (_that) { +case _ParseArchiveSaveBookProgress(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseArchiveSaveBookProgress value)? $default,){ +final _that = this; +switch (_that) { +case _ParseArchiveSaveBookProgress() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( SaveStep step, int current, int total)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseArchiveSaveBookProgress() when $default != null: +return $default(_that.step,_that.current,_that.total);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( SaveStep step, int current, int total) $default,) {final _that = this; +switch (_that) { +case _ParseArchiveSaveBookProgress(): +return $default(_that.step,_that.current,_that.total);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( SaveStep step, int current, int total)? $default,) {final _that = this; +switch (_that) { +case _ParseArchiveSaveBookProgress() when $default != null: +return $default(_that.step,_that.current,_that.total);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseArchiveSaveBookProgress extends ParseArchiveSaveBookProgress { + const _ParseArchiveSaveBookProgress({this.step = SaveStep.generateCover, this.current = 0, this.total = 0}): super._(); + + +@override@JsonKey() final SaveStep step; +@override@JsonKey() final int current; +@override@JsonKey() final int total; + +/// Create a copy of ParseArchiveSaveBookProgress +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseArchiveSaveBookProgressCopyWith<_ParseArchiveSaveBookProgress> get copyWith => __$ParseArchiveSaveBookProgressCopyWithImpl<_ParseArchiveSaveBookProgress>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseArchiveSaveBookProgress&&(identical(other.step, step) || other.step == step)&&(identical(other.current, current) || other.current == current)&&(identical(other.total, total) || other.total == total)); +} + + +@override +int get hashCode => Object.hash(runtimeType,step,current,total); + +@override +String toString() { + return 'ParseArchiveSaveBookProgress(step: $step, current: $current, total: $total)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseArchiveSaveBookProgressCopyWith<$Res> implements $ParseArchiveSaveBookProgressCopyWith<$Res> { + factory _$ParseArchiveSaveBookProgressCopyWith(_ParseArchiveSaveBookProgress value, $Res Function(_ParseArchiveSaveBookProgress) _then) = __$ParseArchiveSaveBookProgressCopyWithImpl; +@override @useResult +$Res call({ + SaveStep step, int current, int total +}); + + + + +} +/// @nodoc +class __$ParseArchiveSaveBookProgressCopyWithImpl<$Res> + implements _$ParseArchiveSaveBookProgressCopyWith<$Res> { + __$ParseArchiveSaveBookProgressCopyWithImpl(this._self, this._then); + + final _ParseArchiveSaveBookProgress _self; + final $Res Function(_ParseArchiveSaveBookProgress) _then; + +/// Create a copy of ParseArchiveSaveBookProgress +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? step = null,Object? current = null,Object? total = null,}) { + return _then(_ParseArchiveSaveBookProgress( +step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable +as SaveStep,current: null == current ? _self.current : current // ignore: cast_nullable_to_non_nullable +as int,total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + +// dart format on diff --git a/lib/feature/parse/ui/provider/parse_archive_provider.g.dart b/lib/feature/parse/ui/provider/parse_archive_provider.g.dart new file mode 100644 index 0000000..b6f95b8 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_archive_provider.g.dart @@ -0,0 +1,145 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'parse_archive_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ParseArchive) +final parseArchiveProvider = ParseArchiveFamily._(); + +final class ParseArchiveProvider + extends $AsyncNotifierProvider { + ParseArchiveProvider._({ + required ParseArchiveFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'parseArchiveProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$parseArchiveHash(); + + @override + String toString() { + return r'parseArchiveProvider' + '' + '($argument)'; + } + + @$internal + @override + ParseArchive create() => ParseArchive(); + + @override + bool operator ==(Object other) { + return other is ParseArchiveProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$parseArchiveHash() => r'550d2a3343eafbb33bc33f18bb234ffd12ead4d7'; + +final class ParseArchiveFamily extends $Family + with + $ClassFamilyOverride< + ParseArchive, + AsyncValue, + ParseArchiveState, + FutureOr, + String + > { + ParseArchiveFamily._() + : super( + retry: null, + name: r'parseArchiveProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + ParseArchiveProvider call(String archivePath) => + ParseArchiveProvider._(argument: archivePath, from: this); + + @override + String toString() => r'parseArchiveProvider'; +} + +abstract class _$ParseArchive extends $AsyncNotifier { + late final _$args = ref.$arg as String; + String get archivePath => _$args; + + FutureOr build(String archivePath); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref as $Ref, ParseArchiveState>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, ParseArchiveState>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} + +@ProviderFor(ParseArchiveSaveBook) +final parseArchiveSaveBookProvider = ParseArchiveSaveBookProvider._(); + +final class ParseArchiveSaveBookProvider + extends $AsyncNotifierProvider { + ParseArchiveSaveBookProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'parseArchiveSaveBookProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$parseArchiveSaveBookHash(); + + @$internal + @override + ParseArchiveSaveBook create() => ParseArchiveSaveBook(); +} + +String _$parseArchiveSaveBookHash() => + r'731cd951752090666be1330594b8248c21d50f2f'; + +abstract class _$ParseArchiveSaveBook extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/feature/parse/ui/provider/parse_batch_archive_provider.dart b/lib/feature/parse/ui/provider/parse_batch_archive_provider.dart new file mode 100644 index 0000000..5b8f076 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_batch_archive_provider.dart @@ -0,0 +1,220 @@ +import 'dart:io'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod/legacy.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tele_book/feature/book/model/dto/save_as_book_dto.dart'; +import 'package:tele_book/feature/book/repository/book_repository.dart'; +import 'package:tele_book/feature/parse/model/parse_batch_archive_vo.dart'; +import 'package:tele_book/feature/parse/service/parse_archive_service.dart'; + +part 'parse_batch_archive_provider.freezed.dart'; + +part 'parse_batch_archive_provider.g.dart'; + +@freezed +abstract class ParseBatchArchiveParam with _$ParseBatchArchiveParam { + const factory ParseBatchArchiveParam({ + String? archiveDirPath, + List? archivePaths, + }) = _ParseBatchArchiveParam; +} + +@freezed +abstract class ParseBatchArchiveState with _$ParseBatchArchiveState { + const factory ParseBatchArchiveState({ + required List parseBatchArchiveList, + }) = _ParseBatchArchiveState; +} + +@freezed +abstract class ParseBatchArchiveProgress with _$ParseBatchArchiveProgress { + const factory ParseBatchArchiveProgress({ + required int completeCount, + required int totalCount, + required String currentFileName, + required int currentFileProgress, + required int currentFileTotal, + }) = _ParseBatchArchiveProgress; + + const ParseBatchArchiveProgress._(); + + String get currentFileProgressText { + if (currentFileName.isEmpty) return ''; + if (currentFileTotal <= 0) return '当前文件进度:准备中'; + return '当前文件进度:$currentFileProgress / $currentFileTotal'; + } +} + +@freezed +abstract class ParseBatchArchiveSaveBookProgress + with _$ParseBatchArchiveSaveBookProgress { + const factory ParseBatchArchiveSaveBookProgress({ + @Default(0) int current, + @Default(0) int total, + @Default(SaveStep.generateCover) SaveStep step, + @Default(0) int stepCurrent, + @Default(0) int stepTotal, + @Default(0) int bookIndex, + }) = _ParseBatchArchiveSaveBookProgress; + + const ParseBatchArchiveSaveBookProgress._(); + + String get stepText { + final bookInfo = total > 0 ? '(${bookIndex + 1}/$total) ' : ''; + return switch (step) { + SaveStep.generateCover => '$bookInfo生成封面图...', + SaveStep.generatePreview => '$bookInfo生成预览图... ($stepCurrent/$stepTotal)', + SaveStep.saveOriginal => '$bookInfo保存原图... ($stepCurrent/$stepTotal)', + SaveStep.saveDatabase => '保存数据...', + }; + } +} + +final parseBatchArchiveProgressProvider = + StateProvider( + (_) => const ParseBatchArchiveProgress( + completeCount: 0, + totalCount: 0, + currentFileName: '', + currentFileProgress: 0, + currentFileTotal: 0, + ), + ); + +final parseBatchArchiveSaveBookProgressProvider = + StateProvider( + (_) => const ParseBatchArchiveSaveBookProgress(), + ); + +@riverpod +class ParseBatchArchive extends _$ParseBatchArchive { + ParseArchiveService get _parseArchiveService => + ref.read(parseArchiveServiceProvider); + + late final ParseBatchArchiveParam _param; + + @override + FutureOr build(ParseBatchArchiveParam param) async { + _param = param; + return _parseBatchArchive(); + } + + Future _requestStoragePermission() async { + if (!Platform.isAndroid) return true; + if (await Permission.manageExternalStorage.isGranted) return true; + final status = await Permission.manageExternalStorage.request(); + if (status.isGranted) return true; + + if (await Permission.storage.isGranted) return true; + final storageStatus = await Permission.storage.request(); + return storageStatus.isGranted; + } + + Future _parseBatchArchive() async { + state = const AsyncLoading(); + final hasPermission = await _requestStoragePermission(); + if (!hasPermission) { + throw Exception('需要「所有文件访问权限」才能读取外部目录,请在系统设置中授权后重试。'); + } + + final parseResult = + _param.archivePaths != null && _param.archivePaths!.isNotEmpty + ? await _parseArchiveService.parseBatchArchivesFromPaths( + _param.archivePaths!, + _onStart, + _onProgress, + onCurrentItemChanged: _onCurrentItemChanged, + onCurrentItemProgress: _onCurrentItemProgress, + ) + : await _parseArchiveService.parseBatchArchives( + _param.archiveDirPath ?? '', + _onStart, + _onProgress, + onCurrentItemChanged: _onCurrentItemChanged, + onCurrentItemProgress: _onCurrentItemProgress, + ); + + if (parseResult.isError) { + throw Exception(parseResult.error?.message); + } + + return ParseBatchArchiveState(parseBatchArchiveList: parseResult.data!); + } + + void _onStart(int total) { + if (!ref.mounted) return; + final current = ref.read(parseBatchArchiveProgressProvider); + ref.read(parseBatchArchiveProgressProvider.notifier).state = current + .copyWith(totalCount: total); + } + + void _onProgress(int count) { + if (!ref.mounted) return; + final current = ref.read(parseBatchArchiveProgressProvider); + ref.read(parseBatchArchiveProgressProvider.notifier).state = current + .copyWith(completeCount: count); + } + + void _onCurrentItemChanged(String fileName) { + if (!ref.mounted) return; + final current = ref.read(parseBatchArchiveProgressProvider); + ref.read(parseBatchArchiveProgressProvider.notifier).state = current + .copyWith( + currentFileName: fileName, + currentFileProgress: 0, + currentFileTotal: 0, + ); + } + + void _onCurrentItemProgress(int currentCount, int total) { + if (!ref.mounted) return; + final current = ref.read(parseBatchArchiveProgressProvider); + ref.read(parseBatchArchiveProgressProvider.notifier).state = current + .copyWith(currentFileProgress: currentCount, currentFileTotal: total); + } +} + +@riverpod +class ParseBatchArchiveSaveBook extends _$ParseBatchArchiveSaveBook { + BookRepository get _bookRepository => ref.read(bookRepositoryProvider); + + @override + FutureOr build() => null; + + Future saveBatchAsBook(List parseBatchList) async { + if (state.isLoading || parseBatchList.isEmpty) return; + + state = const AsyncLoading(); + ref + .read(parseBatchArchiveSaveBookProgressProvider.notifier) + .state = ParseBatchArchiveSaveBookProgress( + current: 0, + total: parseBatchList.length, + ); + + final dos = parseBatchList + .map((e) => SaveAsBookDto(title: e.name, paths: e.tempPaths)) + .toList(); + + final result = await _bookRepository.saveBatchAsBooks(dos, (count) { + ref + .read(parseBatchArchiveSaveBookProgressProvider.notifier) + .state = ParseBatchArchiveSaveBookProgress( + current: count, + total: parseBatchList.length, + ); + }); + + result.fold( + onSuccess: (_) { + state = const AsyncData(null); + }, + onError: (error) { + state = AsyncError(error.message, StackTrace.current); + }, + ); + } +} diff --git a/lib/feature/parse/ui/provider/parse_batch_archive_provider.freezed.dart b/lib/feature/parse/ui/provider/parse_batch_archive_provider.freezed.dart new file mode 100644 index 0000000..311584a --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_batch_archive_provider.freezed.dart @@ -0,0 +1,1086 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'parse_batch_archive_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ParseBatchArchiveParam { + + String? get archiveDirPath; List? get archivePaths; +/// Create a copy of ParseBatchArchiveParam +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseBatchArchiveParamCopyWith get copyWith => _$ParseBatchArchiveParamCopyWithImpl(this as ParseBatchArchiveParam, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseBatchArchiveParam&&(identical(other.archiveDirPath, archiveDirPath) || other.archiveDirPath == archiveDirPath)&&const DeepCollectionEquality().equals(other.archivePaths, archivePaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,archiveDirPath,const DeepCollectionEquality().hash(archivePaths)); + +@override +String toString() { + return 'ParseBatchArchiveParam(archiveDirPath: $archiveDirPath, archivePaths: $archivePaths)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseBatchArchiveParamCopyWith<$Res> { + factory $ParseBatchArchiveParamCopyWith(ParseBatchArchiveParam value, $Res Function(ParseBatchArchiveParam) _then) = _$ParseBatchArchiveParamCopyWithImpl; +@useResult +$Res call({ + String? archiveDirPath, List? archivePaths +}); + + + + +} +/// @nodoc +class _$ParseBatchArchiveParamCopyWithImpl<$Res> + implements $ParseBatchArchiveParamCopyWith<$Res> { + _$ParseBatchArchiveParamCopyWithImpl(this._self, this._then); + + final ParseBatchArchiveParam _self; + final $Res Function(ParseBatchArchiveParam) _then; + +/// Create a copy of ParseBatchArchiveParam +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? archiveDirPath = freezed,Object? archivePaths = freezed,}) { + return _then(_self.copyWith( +archiveDirPath: freezed == archiveDirPath ? _self.archiveDirPath : archiveDirPath // ignore: cast_nullable_to_non_nullable +as String?,archivePaths: freezed == archivePaths ? _self.archivePaths : archivePaths // ignore: cast_nullable_to_non_nullable +as List?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseBatchArchiveParam]. +extension ParseBatchArchiveParamPatterns on ParseBatchArchiveParam { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseBatchArchiveParam value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseBatchArchiveParam() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseBatchArchiveParam value) $default,){ +final _that = this; +switch (_that) { +case _ParseBatchArchiveParam(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseBatchArchiveParam value)? $default,){ +final _that = this; +switch (_that) { +case _ParseBatchArchiveParam() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String? archiveDirPath, List? archivePaths)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseBatchArchiveParam() when $default != null: +return $default(_that.archiveDirPath,_that.archivePaths);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String? archiveDirPath, List? archivePaths) $default,) {final _that = this; +switch (_that) { +case _ParseBatchArchiveParam(): +return $default(_that.archiveDirPath,_that.archivePaths);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? archiveDirPath, List? archivePaths)? $default,) {final _that = this; +switch (_that) { +case _ParseBatchArchiveParam() when $default != null: +return $default(_that.archiveDirPath,_that.archivePaths);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseBatchArchiveParam implements ParseBatchArchiveParam { + const _ParseBatchArchiveParam({this.archiveDirPath, final List? archivePaths}): _archivePaths = archivePaths; + + +@override final String? archiveDirPath; + final List? _archivePaths; +@override List? get archivePaths { + final value = _archivePaths; + if (value == null) return null; + if (_archivePaths is EqualUnmodifiableListView) return _archivePaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + + +/// Create a copy of ParseBatchArchiveParam +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseBatchArchiveParamCopyWith<_ParseBatchArchiveParam> get copyWith => __$ParseBatchArchiveParamCopyWithImpl<_ParseBatchArchiveParam>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseBatchArchiveParam&&(identical(other.archiveDirPath, archiveDirPath) || other.archiveDirPath == archiveDirPath)&&const DeepCollectionEquality().equals(other._archivePaths, _archivePaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,archiveDirPath,const DeepCollectionEquality().hash(_archivePaths)); + +@override +String toString() { + return 'ParseBatchArchiveParam(archiveDirPath: $archiveDirPath, archivePaths: $archivePaths)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseBatchArchiveParamCopyWith<$Res> implements $ParseBatchArchiveParamCopyWith<$Res> { + factory _$ParseBatchArchiveParamCopyWith(_ParseBatchArchiveParam value, $Res Function(_ParseBatchArchiveParam) _then) = __$ParseBatchArchiveParamCopyWithImpl; +@override @useResult +$Res call({ + String? archiveDirPath, List? archivePaths +}); + + + + +} +/// @nodoc +class __$ParseBatchArchiveParamCopyWithImpl<$Res> + implements _$ParseBatchArchiveParamCopyWith<$Res> { + __$ParseBatchArchiveParamCopyWithImpl(this._self, this._then); + + final _ParseBatchArchiveParam _self; + final $Res Function(_ParseBatchArchiveParam) _then; + +/// Create a copy of ParseBatchArchiveParam +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? archiveDirPath = freezed,Object? archivePaths = freezed,}) { + return _then(_ParseBatchArchiveParam( +archiveDirPath: freezed == archiveDirPath ? _self.archiveDirPath : archiveDirPath // ignore: cast_nullable_to_non_nullable +as String?,archivePaths: freezed == archivePaths ? _self._archivePaths : archivePaths // ignore: cast_nullable_to_non_nullable +as List?, + )); +} + + +} + +/// @nodoc +mixin _$ParseBatchArchiveState { + + List get parseBatchArchiveList; +/// Create a copy of ParseBatchArchiveState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseBatchArchiveStateCopyWith get copyWith => _$ParseBatchArchiveStateCopyWithImpl(this as ParseBatchArchiveState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseBatchArchiveState&&const DeepCollectionEquality().equals(other.parseBatchArchiveList, parseBatchArchiveList)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(parseBatchArchiveList)); + +@override +String toString() { + return 'ParseBatchArchiveState(parseBatchArchiveList: $parseBatchArchiveList)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseBatchArchiveStateCopyWith<$Res> { + factory $ParseBatchArchiveStateCopyWith(ParseBatchArchiveState value, $Res Function(ParseBatchArchiveState) _then) = _$ParseBatchArchiveStateCopyWithImpl; +@useResult +$Res call({ + List parseBatchArchiveList +}); + + + + +} +/// @nodoc +class _$ParseBatchArchiveStateCopyWithImpl<$Res> + implements $ParseBatchArchiveStateCopyWith<$Res> { + _$ParseBatchArchiveStateCopyWithImpl(this._self, this._then); + + final ParseBatchArchiveState _self; + final $Res Function(ParseBatchArchiveState) _then; + +/// Create a copy of ParseBatchArchiveState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? parseBatchArchiveList = null,}) { + return _then(_self.copyWith( +parseBatchArchiveList: null == parseBatchArchiveList ? _self.parseBatchArchiveList : parseBatchArchiveList // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseBatchArchiveState]. +extension ParseBatchArchiveStatePatterns on ParseBatchArchiveState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseBatchArchiveState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseBatchArchiveState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseBatchArchiveState value) $default,){ +final _that = this; +switch (_that) { +case _ParseBatchArchiveState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseBatchArchiveState value)? $default,){ +final _that = this; +switch (_that) { +case _ParseBatchArchiveState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List parseBatchArchiveList)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseBatchArchiveState() when $default != null: +return $default(_that.parseBatchArchiveList);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List parseBatchArchiveList) $default,) {final _that = this; +switch (_that) { +case _ParseBatchArchiveState(): +return $default(_that.parseBatchArchiveList);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List parseBatchArchiveList)? $default,) {final _that = this; +switch (_that) { +case _ParseBatchArchiveState() when $default != null: +return $default(_that.parseBatchArchiveList);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseBatchArchiveState implements ParseBatchArchiveState { + const _ParseBatchArchiveState({required final List parseBatchArchiveList}): _parseBatchArchiveList = parseBatchArchiveList; + + + final List _parseBatchArchiveList; +@override List get parseBatchArchiveList { + if (_parseBatchArchiveList is EqualUnmodifiableListView) return _parseBatchArchiveList; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_parseBatchArchiveList); +} + + +/// Create a copy of ParseBatchArchiveState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseBatchArchiveStateCopyWith<_ParseBatchArchiveState> get copyWith => __$ParseBatchArchiveStateCopyWithImpl<_ParseBatchArchiveState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseBatchArchiveState&&const DeepCollectionEquality().equals(other._parseBatchArchiveList, _parseBatchArchiveList)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_parseBatchArchiveList)); + +@override +String toString() { + return 'ParseBatchArchiveState(parseBatchArchiveList: $parseBatchArchiveList)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseBatchArchiveStateCopyWith<$Res> implements $ParseBatchArchiveStateCopyWith<$Res> { + factory _$ParseBatchArchiveStateCopyWith(_ParseBatchArchiveState value, $Res Function(_ParseBatchArchiveState) _then) = __$ParseBatchArchiveStateCopyWithImpl; +@override @useResult +$Res call({ + List parseBatchArchiveList +}); + + + + +} +/// @nodoc +class __$ParseBatchArchiveStateCopyWithImpl<$Res> + implements _$ParseBatchArchiveStateCopyWith<$Res> { + __$ParseBatchArchiveStateCopyWithImpl(this._self, this._then); + + final _ParseBatchArchiveState _self; + final $Res Function(_ParseBatchArchiveState) _then; + +/// Create a copy of ParseBatchArchiveState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? parseBatchArchiveList = null,}) { + return _then(_ParseBatchArchiveState( +parseBatchArchiveList: null == parseBatchArchiveList ? _self._parseBatchArchiveList : parseBatchArchiveList // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc +mixin _$ParseBatchArchiveProgress { + + int get completeCount; int get totalCount; String get currentFileName; int get currentFileProgress; int get currentFileTotal; +/// Create a copy of ParseBatchArchiveProgress +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseBatchArchiveProgressCopyWith get copyWith => _$ParseBatchArchiveProgressCopyWithImpl(this as ParseBatchArchiveProgress, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseBatchArchiveProgress&&(identical(other.completeCount, completeCount) || other.completeCount == completeCount)&&(identical(other.totalCount, totalCount) || other.totalCount == totalCount)&&(identical(other.currentFileName, currentFileName) || other.currentFileName == currentFileName)&&(identical(other.currentFileProgress, currentFileProgress) || other.currentFileProgress == currentFileProgress)&&(identical(other.currentFileTotal, currentFileTotal) || other.currentFileTotal == currentFileTotal)); +} + + +@override +int get hashCode => Object.hash(runtimeType,completeCount,totalCount,currentFileName,currentFileProgress,currentFileTotal); + +@override +String toString() { + return 'ParseBatchArchiveProgress(completeCount: $completeCount, totalCount: $totalCount, currentFileName: $currentFileName, currentFileProgress: $currentFileProgress, currentFileTotal: $currentFileTotal)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseBatchArchiveProgressCopyWith<$Res> { + factory $ParseBatchArchiveProgressCopyWith(ParseBatchArchiveProgress value, $Res Function(ParseBatchArchiveProgress) _then) = _$ParseBatchArchiveProgressCopyWithImpl; +@useResult +$Res call({ + int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal +}); + + + + +} +/// @nodoc +class _$ParseBatchArchiveProgressCopyWithImpl<$Res> + implements $ParseBatchArchiveProgressCopyWith<$Res> { + _$ParseBatchArchiveProgressCopyWithImpl(this._self, this._then); + + final ParseBatchArchiveProgress _self; + final $Res Function(ParseBatchArchiveProgress) _then; + +/// Create a copy of ParseBatchArchiveProgress +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? completeCount = null,Object? totalCount = null,Object? currentFileName = null,Object? currentFileProgress = null,Object? currentFileTotal = null,}) { + return _then(_self.copyWith( +completeCount: null == completeCount ? _self.completeCount : completeCount // ignore: cast_nullable_to_non_nullable +as int,totalCount: null == totalCount ? _self.totalCount : totalCount // ignore: cast_nullable_to_non_nullable +as int,currentFileName: null == currentFileName ? _self.currentFileName : currentFileName // ignore: cast_nullable_to_non_nullable +as String,currentFileProgress: null == currentFileProgress ? _self.currentFileProgress : currentFileProgress // ignore: cast_nullable_to_non_nullable +as int,currentFileTotal: null == currentFileTotal ? _self.currentFileTotal : currentFileTotal // ignore: cast_nullable_to_non_nullable +as int, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseBatchArchiveProgress]. +extension ParseBatchArchiveProgressPatterns on ParseBatchArchiveProgress { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseBatchArchiveProgress value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseBatchArchiveProgress() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseBatchArchiveProgress value) $default,){ +final _that = this; +switch (_that) { +case _ParseBatchArchiveProgress(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseBatchArchiveProgress value)? $default,){ +final _that = this; +switch (_that) { +case _ParseBatchArchiveProgress() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseBatchArchiveProgress() when $default != null: +return $default(_that.completeCount,_that.totalCount,_that.currentFileName,_that.currentFileProgress,_that.currentFileTotal);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal) $default,) {final _that = this; +switch (_that) { +case _ParseBatchArchiveProgress(): +return $default(_that.completeCount,_that.totalCount,_that.currentFileName,_that.currentFileProgress,_that.currentFileTotal);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal)? $default,) {final _that = this; +switch (_that) { +case _ParseBatchArchiveProgress() when $default != null: +return $default(_that.completeCount,_that.totalCount,_that.currentFileName,_that.currentFileProgress,_that.currentFileTotal);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseBatchArchiveProgress extends ParseBatchArchiveProgress { + const _ParseBatchArchiveProgress({required this.completeCount, required this.totalCount, required this.currentFileName, required this.currentFileProgress, required this.currentFileTotal}): super._(); + + +@override final int completeCount; +@override final int totalCount; +@override final String currentFileName; +@override final int currentFileProgress; +@override final int currentFileTotal; + +/// Create a copy of ParseBatchArchiveProgress +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseBatchArchiveProgressCopyWith<_ParseBatchArchiveProgress> get copyWith => __$ParseBatchArchiveProgressCopyWithImpl<_ParseBatchArchiveProgress>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseBatchArchiveProgress&&(identical(other.completeCount, completeCount) || other.completeCount == completeCount)&&(identical(other.totalCount, totalCount) || other.totalCount == totalCount)&&(identical(other.currentFileName, currentFileName) || other.currentFileName == currentFileName)&&(identical(other.currentFileProgress, currentFileProgress) || other.currentFileProgress == currentFileProgress)&&(identical(other.currentFileTotal, currentFileTotal) || other.currentFileTotal == currentFileTotal)); +} + + +@override +int get hashCode => Object.hash(runtimeType,completeCount,totalCount,currentFileName,currentFileProgress,currentFileTotal); + +@override +String toString() { + return 'ParseBatchArchiveProgress(completeCount: $completeCount, totalCount: $totalCount, currentFileName: $currentFileName, currentFileProgress: $currentFileProgress, currentFileTotal: $currentFileTotal)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseBatchArchiveProgressCopyWith<$Res> implements $ParseBatchArchiveProgressCopyWith<$Res> { + factory _$ParseBatchArchiveProgressCopyWith(_ParseBatchArchiveProgress value, $Res Function(_ParseBatchArchiveProgress) _then) = __$ParseBatchArchiveProgressCopyWithImpl; +@override @useResult +$Res call({ + int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal +}); + + + + +} +/// @nodoc +class __$ParseBatchArchiveProgressCopyWithImpl<$Res> + implements _$ParseBatchArchiveProgressCopyWith<$Res> { + __$ParseBatchArchiveProgressCopyWithImpl(this._self, this._then); + + final _ParseBatchArchiveProgress _self; + final $Res Function(_ParseBatchArchiveProgress) _then; + +/// Create a copy of ParseBatchArchiveProgress +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? completeCount = null,Object? totalCount = null,Object? currentFileName = null,Object? currentFileProgress = null,Object? currentFileTotal = null,}) { + return _then(_ParseBatchArchiveProgress( +completeCount: null == completeCount ? _self.completeCount : completeCount // ignore: cast_nullable_to_non_nullable +as int,totalCount: null == totalCount ? _self.totalCount : totalCount // ignore: cast_nullable_to_non_nullable +as int,currentFileName: null == currentFileName ? _self.currentFileName : currentFileName // ignore: cast_nullable_to_non_nullable +as String,currentFileProgress: null == currentFileProgress ? _self.currentFileProgress : currentFileProgress // ignore: cast_nullable_to_non_nullable +as int,currentFileTotal: null == currentFileTotal ? _self.currentFileTotal : currentFileTotal // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + +/// @nodoc +mixin _$ParseBatchArchiveSaveBookProgress { + + int get current; int get total; SaveStep get step; int get stepCurrent; int get stepTotal; int get bookIndex; +/// Create a copy of ParseBatchArchiveSaveBookProgress +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseBatchArchiveSaveBookProgressCopyWith get copyWith => _$ParseBatchArchiveSaveBookProgressCopyWithImpl(this as ParseBatchArchiveSaveBookProgress, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseBatchArchiveSaveBookProgress&&(identical(other.current, current) || other.current == current)&&(identical(other.total, total) || other.total == total)&&(identical(other.step, step) || other.step == step)&&(identical(other.stepCurrent, stepCurrent) || other.stepCurrent == stepCurrent)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.bookIndex, bookIndex) || other.bookIndex == bookIndex)); +} + + +@override +int get hashCode => Object.hash(runtimeType,current,total,step,stepCurrent,stepTotal,bookIndex); + +@override +String toString() { + return 'ParseBatchArchiveSaveBookProgress(current: $current, total: $total, step: $step, stepCurrent: $stepCurrent, stepTotal: $stepTotal, bookIndex: $bookIndex)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseBatchArchiveSaveBookProgressCopyWith<$Res> { + factory $ParseBatchArchiveSaveBookProgressCopyWith(ParseBatchArchiveSaveBookProgress value, $Res Function(ParseBatchArchiveSaveBookProgress) _then) = _$ParseBatchArchiveSaveBookProgressCopyWithImpl; +@useResult +$Res call({ + int current, int total, SaveStep step, int stepCurrent, int stepTotal, int bookIndex +}); + + + + +} +/// @nodoc +class _$ParseBatchArchiveSaveBookProgressCopyWithImpl<$Res> + implements $ParseBatchArchiveSaveBookProgressCopyWith<$Res> { + _$ParseBatchArchiveSaveBookProgressCopyWithImpl(this._self, this._then); + + final ParseBatchArchiveSaveBookProgress _self; + final $Res Function(ParseBatchArchiveSaveBookProgress) _then; + +/// Create a copy of ParseBatchArchiveSaveBookProgress +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? current = null,Object? total = null,Object? step = null,Object? stepCurrent = null,Object? stepTotal = null,Object? bookIndex = null,}) { + return _then(_self.copyWith( +current: null == current ? _self.current : current // ignore: cast_nullable_to_non_nullable +as int,total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable +as int,step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable +as SaveStep,stepCurrent: null == stepCurrent ? _self.stepCurrent : stepCurrent // ignore: cast_nullable_to_non_nullable +as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable +as int,bookIndex: null == bookIndex ? _self.bookIndex : bookIndex // ignore: cast_nullable_to_non_nullable +as int, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseBatchArchiveSaveBookProgress]. +extension ParseBatchArchiveSaveBookProgressPatterns on ParseBatchArchiveSaveBookProgress { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseBatchArchiveSaveBookProgress value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseBatchArchiveSaveBookProgress() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseBatchArchiveSaveBookProgress value) $default,){ +final _that = this; +switch (_that) { +case _ParseBatchArchiveSaveBookProgress(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseBatchArchiveSaveBookProgress value)? $default,){ +final _that = this; +switch (_that) { +case _ParseBatchArchiveSaveBookProgress() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( int current, int total, SaveStep step, int stepCurrent, int stepTotal, int bookIndex)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseBatchArchiveSaveBookProgress() when $default != null: +return $default(_that.current,_that.total,_that.step,_that.stepCurrent,_that.stepTotal,_that.bookIndex);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( int current, int total, SaveStep step, int stepCurrent, int stepTotal, int bookIndex) $default,) {final _that = this; +switch (_that) { +case _ParseBatchArchiveSaveBookProgress(): +return $default(_that.current,_that.total,_that.step,_that.stepCurrent,_that.stepTotal,_that.bookIndex);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int current, int total, SaveStep step, int stepCurrent, int stepTotal, int bookIndex)? $default,) {final _that = this; +switch (_that) { +case _ParseBatchArchiveSaveBookProgress() when $default != null: +return $default(_that.current,_that.total,_that.step,_that.stepCurrent,_that.stepTotal,_that.bookIndex);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseBatchArchiveSaveBookProgress extends ParseBatchArchiveSaveBookProgress { + const _ParseBatchArchiveSaveBookProgress({this.current = 0, this.total = 0, this.step = SaveStep.generateCover, this.stepCurrent = 0, this.stepTotal = 0, this.bookIndex = 0}): super._(); + + +@override@JsonKey() final int current; +@override@JsonKey() final int total; +@override@JsonKey() final SaveStep step; +@override@JsonKey() final int stepCurrent; +@override@JsonKey() final int stepTotal; +@override@JsonKey() final int bookIndex; + +/// Create a copy of ParseBatchArchiveSaveBookProgress +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseBatchArchiveSaveBookProgressCopyWith<_ParseBatchArchiveSaveBookProgress> get copyWith => __$ParseBatchArchiveSaveBookProgressCopyWithImpl<_ParseBatchArchiveSaveBookProgress>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseBatchArchiveSaveBookProgress&&(identical(other.current, current) || other.current == current)&&(identical(other.total, total) || other.total == total)&&(identical(other.step, step) || other.step == step)&&(identical(other.stepCurrent, stepCurrent) || other.stepCurrent == stepCurrent)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.bookIndex, bookIndex) || other.bookIndex == bookIndex)); +} + + +@override +int get hashCode => Object.hash(runtimeType,current,total,step,stepCurrent,stepTotal,bookIndex); + +@override +String toString() { + return 'ParseBatchArchiveSaveBookProgress(current: $current, total: $total, step: $step, stepCurrent: $stepCurrent, stepTotal: $stepTotal, bookIndex: $bookIndex)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseBatchArchiveSaveBookProgressCopyWith<$Res> implements $ParseBatchArchiveSaveBookProgressCopyWith<$Res> { + factory _$ParseBatchArchiveSaveBookProgressCopyWith(_ParseBatchArchiveSaveBookProgress value, $Res Function(_ParseBatchArchiveSaveBookProgress) _then) = __$ParseBatchArchiveSaveBookProgressCopyWithImpl; +@override @useResult +$Res call({ + int current, int total, SaveStep step, int stepCurrent, int stepTotal, int bookIndex +}); + + + + +} +/// @nodoc +class __$ParseBatchArchiveSaveBookProgressCopyWithImpl<$Res> + implements _$ParseBatchArchiveSaveBookProgressCopyWith<$Res> { + __$ParseBatchArchiveSaveBookProgressCopyWithImpl(this._self, this._then); + + final _ParseBatchArchiveSaveBookProgress _self; + final $Res Function(_ParseBatchArchiveSaveBookProgress) _then; + +/// Create a copy of ParseBatchArchiveSaveBookProgress +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? current = null,Object? total = null,Object? step = null,Object? stepCurrent = null,Object? stepTotal = null,Object? bookIndex = null,}) { + return _then(_ParseBatchArchiveSaveBookProgress( +current: null == current ? _self.current : current // ignore: cast_nullable_to_non_nullable +as int,total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable +as int,step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable +as SaveStep,stepCurrent: null == stepCurrent ? _self.stepCurrent : stepCurrent // ignore: cast_nullable_to_non_nullable +as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable +as int,bookIndex: null == bookIndex ? _self.bookIndex : bookIndex // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + +// dart format on diff --git a/lib/feature/parse/ui/provider/parse_batch_archive_provider.g.dart b/lib/feature/parse/ui/provider/parse_batch_archive_provider.g.dart new file mode 100644 index 0000000..11b6bcd --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_batch_archive_provider.g.dart @@ -0,0 +1,150 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'parse_batch_archive_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ParseBatchArchive) +final parseBatchArchiveProvider = ParseBatchArchiveFamily._(); + +final class ParseBatchArchiveProvider + extends $AsyncNotifierProvider { + ParseBatchArchiveProvider._({ + required ParseBatchArchiveFamily super.from, + required ParseBatchArchiveParam super.argument, + }) : super( + retry: null, + name: r'parseBatchArchiveProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$parseBatchArchiveHash(); + + @override + String toString() { + return r'parseBatchArchiveProvider' + '' + '($argument)'; + } + + @$internal + @override + ParseBatchArchive create() => ParseBatchArchive(); + + @override + bool operator ==(Object other) { + return other is ParseBatchArchiveProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$parseBatchArchiveHash() => r'd0ea0561ebb2ca43ab7f2fae08254bf67582edfe'; + +final class ParseBatchArchiveFamily extends $Family + with + $ClassFamilyOverride< + ParseBatchArchive, + AsyncValue, + ParseBatchArchiveState, + FutureOr, + ParseBatchArchiveParam + > { + ParseBatchArchiveFamily._() + : super( + retry: null, + name: r'parseBatchArchiveProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + ParseBatchArchiveProvider call(ParseBatchArchiveParam param) => + ParseBatchArchiveProvider._(argument: param, from: this); + + @override + String toString() => r'parseBatchArchiveProvider'; +} + +abstract class _$ParseBatchArchive + extends $AsyncNotifier { + late final _$args = ref.$arg as ParseBatchArchiveParam; + ParseBatchArchiveParam get param => _$args; + + FutureOr build(ParseBatchArchiveParam param); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref + as $Ref, ParseBatchArchiveState>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + AsyncValue, + ParseBatchArchiveState + >, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} + +@ProviderFor(ParseBatchArchiveSaveBook) +final parseBatchArchiveSaveBookProvider = ParseBatchArchiveSaveBookProvider._(); + +final class ParseBatchArchiveSaveBookProvider + extends $AsyncNotifierProvider { + ParseBatchArchiveSaveBookProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'parseBatchArchiveSaveBookProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$parseBatchArchiveSaveBookHash(); + + @$internal + @override + ParseBatchArchiveSaveBook create() => ParseBatchArchiveSaveBook(); +} + +String _$parseBatchArchiveSaveBookHash() => + r'990b49c199855638b06aa45a0256fa98ba4a739f'; + +abstract class _$ParseBatchArchiveSaveBook extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/feature/parse/ui/provider/parse_batch_image_folder.dart b/lib/feature/parse/ui/provider/parse_batch_image_folder.dart new file mode 100644 index 0000000..93c4b82 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_batch_image_folder.dart @@ -0,0 +1,242 @@ +import 'dart:io'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tele_book/feature/book/model/dto/save_as_book_dto.dart'; +import 'package:tele_book/feature/book/repository/book_repository.dart'; +import 'package:tele_book/feature/parse/model/parse_batch_archive_vo.dart'; +import 'package:tele_book/feature/parse/service/parse_archive_service.dart'; + +part 'parse_batch_image_folder.freezed.dart'; + +part 'parse_batch_image_folder.g.dart'; + +@freezed +abstract class ParseBatchImageFolderState with _$ParseBatchImageFolderState { + const factory ParseBatchImageFolderState({ + @Default([]) List parseBatchFolderList, + @Default(0) int completeCount, + @Default(0) int totalCount, + @Default('') String currentFileName, + @Default(0) int currentFileProgress, + @Default(0) int currentFileTotal, + @Default(false) bool isParsing, + }) = _ParseBatchImageFolderState; + + const ParseBatchImageFolderState._(); + + String get currentFileProgressText { + if (currentFileName.isEmpty) return ''; + if (currentFileTotal <= 0) return '当前文件进度,准备中'; + return '当前文件进度,$currentFileProgress / $currentFileTotal'; + } +} + +@freezed +abstract class SaveBatchAsBookState with _$SaveBatchAsBookState { + const factory SaveBatchAsBookState({ + @Default(0) int saveAsBookCount, + @Default(0) int totalCount, + @Default(SaveStep.generateCover) SaveStep step, + @Default(0) int stepCurrent, + @Default(0) int stepTotal, + @Default(0) int bookIndex, + @Default(AsyncData(null)) AsyncValue submitState, + }) = _SaveBatchAsBookState; + + const SaveBatchAsBookState._(); + + String get stepText { + final bookInfo = totalCount > 0 ? '(${bookIndex + 1}/$totalCount) ' : ''; + return switch (step) { + SaveStep.generateCover => '$bookInfo生成封面图...', + SaveStep.generatePreview => '$bookInfo生成预览图... ($stepCurrent/$stepTotal)', + SaveStep.saveOriginal => '$bookInfo保存原图... ($stepCurrent/$stepTotal)', + SaveStep.saveDatabase => '保存数据...', + }; + } +} + +@freezed +abstract class ParseBatchImageFolderParam with _$ParseBatchImageFolderParam { + const factory ParseBatchImageFolderParam({ + String? parentDirPath, + List? imagePaths, + }) = _ParseBatchImageFolderParam; +} + +@riverpod +class ParseBatchImageFolder extends _$ParseBatchImageFolder { + ParseArchiveService get _parseArchiveService => + ref.read(parseArchiveServiceProvider); + + @override + FutureOr build(ParseBatchImageFolderParam param) { + Future.microtask(() => _parseBatchFolders(param)); + return const ParseBatchImageFolderState(isParsing: true); + } + + Future _requestStoragePermission() async { + if (!Platform.isAndroid) return true; + if (await Permission.manageExternalStorage.isGranted) return true; + final status = await Permission.manageExternalStorage.request(); + if (status.isGranted) return true; + + if (await Permission.storage.isGranted) return true; + final storageStatus = await Permission.storage.request(); + return storageStatus.isGranted; + } + + Future _parseBatchFolders(ParseBatchImageFolderParam param) async { + state = AsyncData( + state.value?.copyWith( + isParsing: true, + parseBatchFolderList: const [], + totalCount: 0, + completeCount: 0, + currentFileName: '', + currentFileProgress: 0, + currentFileTotal: 0, + ) ?? + const ParseBatchImageFolderState(isParsing: true), + ); + + final hasPermission = await _requestStoragePermission(); + if (!ref.mounted) return; + if (!hasPermission) { + state = AsyncValue.error("需要存储权限才能读取图片文件夹", StackTrace.current); + return; + } + + var localState = state.value ?? + const ParseBatchImageFolderState(isParsing: true); + + final result = param.imagePaths != null && param.imagePaths!.isNotEmpty + ? await _parseArchiveService.parseBatchImageFoldersFromPaths( + param.imagePaths!, + (total) { + localState = localState.copyWith(totalCount: total); + if (!ref.mounted) return; + state = AsyncData(localState); + }, + (count) { + localState = localState.copyWith(completeCount: count); + if (!ref.mounted) return; + state = AsyncData(localState); + }, + onCurrentItemChanged: (fileName) { + localState = localState.copyWith( + currentFileName: fileName, + currentFileProgress: 0, + currentFileTotal: 0, + ); + if (!ref.mounted) return; + state = AsyncData(localState); + }, + onCurrentItemProgress: (current, total) { + localState = localState.copyWith( + currentFileProgress: current, + currentFileTotal: total, + ); + if (!ref.mounted) return; + state = AsyncData(localState); + }, + ) + : await _parseArchiveService.parseBatchImageFolders( + param.parentDirPath ?? '', + (total) { + localState = localState.copyWith(totalCount: total); + if (!ref.mounted) return; + state = AsyncData(localState); + }, + (count) { + localState = localState.copyWith(completeCount: count); + if (!ref.mounted) return; + state = AsyncData(localState); + }, + onCurrentItemChanged: (fileName) { + localState = localState.copyWith( + currentFileName: fileName, + currentFileProgress: 0, + currentFileTotal: 0, + ); + if (!ref.mounted) return; + state = AsyncData(localState); + }, + onCurrentItemProgress: (current, total) { + localState = localState.copyWith( + currentFileProgress: current, + currentFileTotal: total, + ); + if (!ref.mounted) return; + state = AsyncData(localState); + }, + ); + + if (!ref.mounted) return; + result.fold( + onSuccess: (data) { + state = AsyncData(localState.copyWith( + parseBatchFolderList: data, + isParsing: false, + )); + }, + onError: (error) { + state = AsyncValue.error(error.message, StackTrace.current); + }, + ); + } + + Future refresh() async { + await _parseBatchFolders(param); + } +} + +@riverpod +class SaveBatchAsBook extends _$SaveBatchAsBook { + BookRepository get _bookRepository => ref.read(bookRepositoryProvider); + + @override + SaveBatchAsBookState build(ParseBatchImageFolderParam param) { + return const SaveBatchAsBookState(); + } + + Future submit(List parseList) async { + if (state.submitState.isLoading) return; + state = state.copyWith( + submitState: const AsyncLoading(), + saveAsBookCount: 0, + totalCount: parseList.length, + ); + + final dos = parseList + .map((e) => SaveAsBookDto(title: e.name, paths: e.tempPaths)) + .toList(); + + final result = await _bookRepository.saveBatchAsBooks( + dos, + (count) { + state = state.copyWith(saveAsBookCount: count); + }, + ); + + result.fold( + onSuccess: (_) { + state = state.copyWith( + submitState: const AsyncData(null), + saveAsBookCount: 0, + ); + }, + onError: (error) { + state = state.copyWith( + submitState: AsyncValue.error( + error.message, + StackTrace.current, + ), + ); + }, + ); + } +} diff --git a/lib/feature/parse/ui/provider/parse_batch_image_folder.freezed.dart b/lib/feature/parse/ui/provider/parse_batch_image_folder.freezed.dart new file mode 100644 index 0000000..24b96a0 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_batch_image_folder.freezed.dart @@ -0,0 +1,838 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'parse_batch_image_folder.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ParseBatchImageFolderState { + + List get parseBatchFolderList; int get completeCount; int get totalCount; String get currentFileName; int get currentFileProgress; int get currentFileTotal; bool get isParsing; +/// Create a copy of ParseBatchImageFolderState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseBatchImageFolderStateCopyWith get copyWith => _$ParseBatchImageFolderStateCopyWithImpl(this as ParseBatchImageFolderState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseBatchImageFolderState&&const DeepCollectionEquality().equals(other.parseBatchFolderList, parseBatchFolderList)&&(identical(other.completeCount, completeCount) || other.completeCount == completeCount)&&(identical(other.totalCount, totalCount) || other.totalCount == totalCount)&&(identical(other.currentFileName, currentFileName) || other.currentFileName == currentFileName)&&(identical(other.currentFileProgress, currentFileProgress) || other.currentFileProgress == currentFileProgress)&&(identical(other.currentFileTotal, currentFileTotal) || other.currentFileTotal == currentFileTotal)&&(identical(other.isParsing, isParsing) || other.isParsing == isParsing)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(parseBatchFolderList),completeCount,totalCount,currentFileName,currentFileProgress,currentFileTotal,isParsing); + +@override +String toString() { + return 'ParseBatchImageFolderState(parseBatchFolderList: $parseBatchFolderList, completeCount: $completeCount, totalCount: $totalCount, currentFileName: $currentFileName, currentFileProgress: $currentFileProgress, currentFileTotal: $currentFileTotal, isParsing: $isParsing)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseBatchImageFolderStateCopyWith<$Res> { + factory $ParseBatchImageFolderStateCopyWith(ParseBatchImageFolderState value, $Res Function(ParseBatchImageFolderState) _then) = _$ParseBatchImageFolderStateCopyWithImpl; +@useResult +$Res call({ + List parseBatchFolderList, int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal, bool isParsing +}); + + + + +} +/// @nodoc +class _$ParseBatchImageFolderStateCopyWithImpl<$Res> + implements $ParseBatchImageFolderStateCopyWith<$Res> { + _$ParseBatchImageFolderStateCopyWithImpl(this._self, this._then); + + final ParseBatchImageFolderState _self; + final $Res Function(ParseBatchImageFolderState) _then; + +/// Create a copy of ParseBatchImageFolderState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? parseBatchFolderList = null,Object? completeCount = null,Object? totalCount = null,Object? currentFileName = null,Object? currentFileProgress = null,Object? currentFileTotal = null,Object? isParsing = null,}) { + return _then(_self.copyWith( +parseBatchFolderList: null == parseBatchFolderList ? _self.parseBatchFolderList : parseBatchFolderList // ignore: cast_nullable_to_non_nullable +as List,completeCount: null == completeCount ? _self.completeCount : completeCount // ignore: cast_nullable_to_non_nullable +as int,totalCount: null == totalCount ? _self.totalCount : totalCount // ignore: cast_nullable_to_non_nullable +as int,currentFileName: null == currentFileName ? _self.currentFileName : currentFileName // ignore: cast_nullable_to_non_nullable +as String,currentFileProgress: null == currentFileProgress ? _self.currentFileProgress : currentFileProgress // ignore: cast_nullable_to_non_nullable +as int,currentFileTotal: null == currentFileTotal ? _self.currentFileTotal : currentFileTotal // ignore: cast_nullable_to_non_nullable +as int,isParsing: null == isParsing ? _self.isParsing : isParsing // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseBatchImageFolderState]. +extension ParseBatchImageFolderStatePatterns on ParseBatchImageFolderState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseBatchImageFolderState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseBatchImageFolderState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseBatchImageFolderState value) $default,){ +final _that = this; +switch (_that) { +case _ParseBatchImageFolderState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseBatchImageFolderState value)? $default,){ +final _that = this; +switch (_that) { +case _ParseBatchImageFolderState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List parseBatchFolderList, int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal, bool isParsing)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseBatchImageFolderState() when $default != null: +return $default(_that.parseBatchFolderList,_that.completeCount,_that.totalCount,_that.currentFileName,_that.currentFileProgress,_that.currentFileTotal,_that.isParsing);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List parseBatchFolderList, int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal, bool isParsing) $default,) {final _that = this; +switch (_that) { +case _ParseBatchImageFolderState(): +return $default(_that.parseBatchFolderList,_that.completeCount,_that.totalCount,_that.currentFileName,_that.currentFileProgress,_that.currentFileTotal,_that.isParsing);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List parseBatchFolderList, int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal, bool isParsing)? $default,) {final _that = this; +switch (_that) { +case _ParseBatchImageFolderState() when $default != null: +return $default(_that.parseBatchFolderList,_that.completeCount,_that.totalCount,_that.currentFileName,_that.currentFileProgress,_that.currentFileTotal,_that.isParsing);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseBatchImageFolderState extends ParseBatchImageFolderState { + const _ParseBatchImageFolderState({final List parseBatchFolderList = const [], this.completeCount = 0, this.totalCount = 0, this.currentFileName = '', this.currentFileProgress = 0, this.currentFileTotal = 0, this.isParsing = false}): _parseBatchFolderList = parseBatchFolderList,super._(); + + + final List _parseBatchFolderList; +@override@JsonKey() List get parseBatchFolderList { + if (_parseBatchFolderList is EqualUnmodifiableListView) return _parseBatchFolderList; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_parseBatchFolderList); +} + +@override@JsonKey() final int completeCount; +@override@JsonKey() final int totalCount; +@override@JsonKey() final String currentFileName; +@override@JsonKey() final int currentFileProgress; +@override@JsonKey() final int currentFileTotal; +@override@JsonKey() final bool isParsing; + +/// Create a copy of ParseBatchImageFolderState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseBatchImageFolderStateCopyWith<_ParseBatchImageFolderState> get copyWith => __$ParseBatchImageFolderStateCopyWithImpl<_ParseBatchImageFolderState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseBatchImageFolderState&&const DeepCollectionEquality().equals(other._parseBatchFolderList, _parseBatchFolderList)&&(identical(other.completeCount, completeCount) || other.completeCount == completeCount)&&(identical(other.totalCount, totalCount) || other.totalCount == totalCount)&&(identical(other.currentFileName, currentFileName) || other.currentFileName == currentFileName)&&(identical(other.currentFileProgress, currentFileProgress) || other.currentFileProgress == currentFileProgress)&&(identical(other.currentFileTotal, currentFileTotal) || other.currentFileTotal == currentFileTotal)&&(identical(other.isParsing, isParsing) || other.isParsing == isParsing)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_parseBatchFolderList),completeCount,totalCount,currentFileName,currentFileProgress,currentFileTotal,isParsing); + +@override +String toString() { + return 'ParseBatchImageFolderState(parseBatchFolderList: $parseBatchFolderList, completeCount: $completeCount, totalCount: $totalCount, currentFileName: $currentFileName, currentFileProgress: $currentFileProgress, currentFileTotal: $currentFileTotal, isParsing: $isParsing)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseBatchImageFolderStateCopyWith<$Res> implements $ParseBatchImageFolderStateCopyWith<$Res> { + factory _$ParseBatchImageFolderStateCopyWith(_ParseBatchImageFolderState value, $Res Function(_ParseBatchImageFolderState) _then) = __$ParseBatchImageFolderStateCopyWithImpl; +@override @useResult +$Res call({ + List parseBatchFolderList, int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal, bool isParsing +}); + + + + +} +/// @nodoc +class __$ParseBatchImageFolderStateCopyWithImpl<$Res> + implements _$ParseBatchImageFolderStateCopyWith<$Res> { + __$ParseBatchImageFolderStateCopyWithImpl(this._self, this._then); + + final _ParseBatchImageFolderState _self; + final $Res Function(_ParseBatchImageFolderState) _then; + +/// Create a copy of ParseBatchImageFolderState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? parseBatchFolderList = null,Object? completeCount = null,Object? totalCount = null,Object? currentFileName = null,Object? currentFileProgress = null,Object? currentFileTotal = null,Object? isParsing = null,}) { + return _then(_ParseBatchImageFolderState( +parseBatchFolderList: null == parseBatchFolderList ? _self._parseBatchFolderList : parseBatchFolderList // ignore: cast_nullable_to_non_nullable +as List,completeCount: null == completeCount ? _self.completeCount : completeCount // ignore: cast_nullable_to_non_nullable +as int,totalCount: null == totalCount ? _self.totalCount : totalCount // ignore: cast_nullable_to_non_nullable +as int,currentFileName: null == currentFileName ? _self.currentFileName : currentFileName // ignore: cast_nullable_to_non_nullable +as String,currentFileProgress: null == currentFileProgress ? _self.currentFileProgress : currentFileProgress // ignore: cast_nullable_to_non_nullable +as int,currentFileTotal: null == currentFileTotal ? _self.currentFileTotal : currentFileTotal // ignore: cast_nullable_to_non_nullable +as int,isParsing: null == isParsing ? _self.isParsing : isParsing // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +/// @nodoc +mixin _$SaveBatchAsBookState { + + int get saveAsBookCount; int get totalCount; SaveStep get step; int get stepCurrent; int get stepTotal; int get bookIndex; AsyncValue get submitState; +/// Create a copy of SaveBatchAsBookState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SaveBatchAsBookStateCopyWith get copyWith => _$SaveBatchAsBookStateCopyWithImpl(this as SaveBatchAsBookState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SaveBatchAsBookState&&(identical(other.saveAsBookCount, saveAsBookCount) || other.saveAsBookCount == saveAsBookCount)&&(identical(other.totalCount, totalCount) || other.totalCount == totalCount)&&(identical(other.step, step) || other.step == step)&&(identical(other.stepCurrent, stepCurrent) || other.stepCurrent == stepCurrent)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.bookIndex, bookIndex) || other.bookIndex == bookIndex)&&(identical(other.submitState, submitState) || other.submitState == submitState)); +} + + +@override +int get hashCode => Object.hash(runtimeType,saveAsBookCount,totalCount,step,stepCurrent,stepTotal,bookIndex,submitState); + +@override +String toString() { + return 'SaveBatchAsBookState(saveAsBookCount: $saveAsBookCount, totalCount: $totalCount, step: $step, stepCurrent: $stepCurrent, stepTotal: $stepTotal, bookIndex: $bookIndex, submitState: $submitState)'; +} + + +} + +/// @nodoc +abstract mixin class $SaveBatchAsBookStateCopyWith<$Res> { + factory $SaveBatchAsBookStateCopyWith(SaveBatchAsBookState value, $Res Function(SaveBatchAsBookState) _then) = _$SaveBatchAsBookStateCopyWithImpl; +@useResult +$Res call({ + int saveAsBookCount, int totalCount, SaveStep step, int stepCurrent, int stepTotal, int bookIndex, AsyncValue submitState +}); + + + + +} +/// @nodoc +class _$SaveBatchAsBookStateCopyWithImpl<$Res> + implements $SaveBatchAsBookStateCopyWith<$Res> { + _$SaveBatchAsBookStateCopyWithImpl(this._self, this._then); + + final SaveBatchAsBookState _self; + final $Res Function(SaveBatchAsBookState) _then; + +/// Create a copy of SaveBatchAsBookState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? saveAsBookCount = null,Object? totalCount = null,Object? step = null,Object? stepCurrent = null,Object? stepTotal = null,Object? bookIndex = null,Object? submitState = null,}) { + return _then(_self.copyWith( +saveAsBookCount: null == saveAsBookCount ? _self.saveAsBookCount : saveAsBookCount // ignore: cast_nullable_to_non_nullable +as int,totalCount: null == totalCount ? _self.totalCount : totalCount // ignore: cast_nullable_to_non_nullable +as int,step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable +as SaveStep,stepCurrent: null == stepCurrent ? _self.stepCurrent : stepCurrent // ignore: cast_nullable_to_non_nullable +as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable +as int,bookIndex: null == bookIndex ? _self.bookIndex : bookIndex // ignore: cast_nullable_to_non_nullable +as int,submitState: null == submitState ? _self.submitState : submitState // ignore: cast_nullable_to_non_nullable +as AsyncValue, + )); +} + +} + + +/// Adds pattern-matching-related methods to [SaveBatchAsBookState]. +extension SaveBatchAsBookStatePatterns on SaveBatchAsBookState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _SaveBatchAsBookState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _SaveBatchAsBookState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _SaveBatchAsBookState value) $default,){ +final _that = this; +switch (_that) { +case _SaveBatchAsBookState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SaveBatchAsBookState value)? $default,){ +final _that = this; +switch (_that) { +case _SaveBatchAsBookState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( int saveAsBookCount, int totalCount, SaveStep step, int stepCurrent, int stepTotal, int bookIndex, AsyncValue submitState)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _SaveBatchAsBookState() when $default != null: +return $default(_that.saveAsBookCount,_that.totalCount,_that.step,_that.stepCurrent,_that.stepTotal,_that.bookIndex,_that.submitState);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( int saveAsBookCount, int totalCount, SaveStep step, int stepCurrent, int stepTotal, int bookIndex, AsyncValue submitState) $default,) {final _that = this; +switch (_that) { +case _SaveBatchAsBookState(): +return $default(_that.saveAsBookCount,_that.totalCount,_that.step,_that.stepCurrent,_that.stepTotal,_that.bookIndex,_that.submitState);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int saveAsBookCount, int totalCount, SaveStep step, int stepCurrent, int stepTotal, int bookIndex, AsyncValue submitState)? $default,) {final _that = this; +switch (_that) { +case _SaveBatchAsBookState() when $default != null: +return $default(_that.saveAsBookCount,_that.totalCount,_that.step,_that.stepCurrent,_that.stepTotal,_that.bookIndex,_that.submitState);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _SaveBatchAsBookState extends SaveBatchAsBookState { + const _SaveBatchAsBookState({this.saveAsBookCount = 0, this.totalCount = 0, this.step = SaveStep.generateCover, this.stepCurrent = 0, this.stepTotal = 0, this.bookIndex = 0, this.submitState = const AsyncData(null)}): super._(); + + +@override@JsonKey() final int saveAsBookCount; +@override@JsonKey() final int totalCount; +@override@JsonKey() final SaveStep step; +@override@JsonKey() final int stepCurrent; +@override@JsonKey() final int stepTotal; +@override@JsonKey() final int bookIndex; +@override@JsonKey() final AsyncValue submitState; + +/// Create a copy of SaveBatchAsBookState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SaveBatchAsBookStateCopyWith<_SaveBatchAsBookState> get copyWith => __$SaveBatchAsBookStateCopyWithImpl<_SaveBatchAsBookState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SaveBatchAsBookState&&(identical(other.saveAsBookCount, saveAsBookCount) || other.saveAsBookCount == saveAsBookCount)&&(identical(other.totalCount, totalCount) || other.totalCount == totalCount)&&(identical(other.step, step) || other.step == step)&&(identical(other.stepCurrent, stepCurrent) || other.stepCurrent == stepCurrent)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.bookIndex, bookIndex) || other.bookIndex == bookIndex)&&(identical(other.submitState, submitState) || other.submitState == submitState)); +} + + +@override +int get hashCode => Object.hash(runtimeType,saveAsBookCount,totalCount,step,stepCurrent,stepTotal,bookIndex,submitState); + +@override +String toString() { + return 'SaveBatchAsBookState(saveAsBookCount: $saveAsBookCount, totalCount: $totalCount, step: $step, stepCurrent: $stepCurrent, stepTotal: $stepTotal, bookIndex: $bookIndex, submitState: $submitState)'; +} + + +} + +/// @nodoc +abstract mixin class _$SaveBatchAsBookStateCopyWith<$Res> implements $SaveBatchAsBookStateCopyWith<$Res> { + factory _$SaveBatchAsBookStateCopyWith(_SaveBatchAsBookState value, $Res Function(_SaveBatchAsBookState) _then) = __$SaveBatchAsBookStateCopyWithImpl; +@override @useResult +$Res call({ + int saveAsBookCount, int totalCount, SaveStep step, int stepCurrent, int stepTotal, int bookIndex, AsyncValue submitState +}); + + + + +} +/// @nodoc +class __$SaveBatchAsBookStateCopyWithImpl<$Res> + implements _$SaveBatchAsBookStateCopyWith<$Res> { + __$SaveBatchAsBookStateCopyWithImpl(this._self, this._then); + + final _SaveBatchAsBookState _self; + final $Res Function(_SaveBatchAsBookState) _then; + +/// Create a copy of SaveBatchAsBookState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? saveAsBookCount = null,Object? totalCount = null,Object? step = null,Object? stepCurrent = null,Object? stepTotal = null,Object? bookIndex = null,Object? submitState = null,}) { + return _then(_SaveBatchAsBookState( +saveAsBookCount: null == saveAsBookCount ? _self.saveAsBookCount : saveAsBookCount // ignore: cast_nullable_to_non_nullable +as int,totalCount: null == totalCount ? _self.totalCount : totalCount // ignore: cast_nullable_to_non_nullable +as int,step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable +as SaveStep,stepCurrent: null == stepCurrent ? _self.stepCurrent : stepCurrent // ignore: cast_nullable_to_non_nullable +as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable +as int,bookIndex: null == bookIndex ? _self.bookIndex : bookIndex // ignore: cast_nullable_to_non_nullable +as int,submitState: null == submitState ? _self.submitState : submitState // ignore: cast_nullable_to_non_nullable +as AsyncValue, + )); +} + + +} + +/// @nodoc +mixin _$ParseBatchImageFolderParam { + + String? get parentDirPath; List? get imagePaths; +/// Create a copy of ParseBatchImageFolderParam +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseBatchImageFolderParamCopyWith get copyWith => _$ParseBatchImageFolderParamCopyWithImpl(this as ParseBatchImageFolderParam, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseBatchImageFolderParam&&(identical(other.parentDirPath, parentDirPath) || other.parentDirPath == parentDirPath)&&const DeepCollectionEquality().equals(other.imagePaths, imagePaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,parentDirPath,const DeepCollectionEquality().hash(imagePaths)); + +@override +String toString() { + return 'ParseBatchImageFolderParam(parentDirPath: $parentDirPath, imagePaths: $imagePaths)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseBatchImageFolderParamCopyWith<$Res> { + factory $ParseBatchImageFolderParamCopyWith(ParseBatchImageFolderParam value, $Res Function(ParseBatchImageFolderParam) _then) = _$ParseBatchImageFolderParamCopyWithImpl; +@useResult +$Res call({ + String? parentDirPath, List? imagePaths +}); + + + + +} +/// @nodoc +class _$ParseBatchImageFolderParamCopyWithImpl<$Res> + implements $ParseBatchImageFolderParamCopyWith<$Res> { + _$ParseBatchImageFolderParamCopyWithImpl(this._self, this._then); + + final ParseBatchImageFolderParam _self; + final $Res Function(ParseBatchImageFolderParam) _then; + +/// Create a copy of ParseBatchImageFolderParam +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? parentDirPath = freezed,Object? imagePaths = freezed,}) { + return _then(_self.copyWith( +parentDirPath: freezed == parentDirPath ? _self.parentDirPath : parentDirPath // ignore: cast_nullable_to_non_nullable +as String?,imagePaths: freezed == imagePaths ? _self.imagePaths : imagePaths // ignore: cast_nullable_to_non_nullable +as List?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseBatchImageFolderParam]. +extension ParseBatchImageFolderParamPatterns on ParseBatchImageFolderParam { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseBatchImageFolderParam value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseBatchImageFolderParam() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseBatchImageFolderParam value) $default,){ +final _that = this; +switch (_that) { +case _ParseBatchImageFolderParam(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseBatchImageFolderParam value)? $default,){ +final _that = this; +switch (_that) { +case _ParseBatchImageFolderParam() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String? parentDirPath, List? imagePaths)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseBatchImageFolderParam() when $default != null: +return $default(_that.parentDirPath,_that.imagePaths);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String? parentDirPath, List? imagePaths) $default,) {final _that = this; +switch (_that) { +case _ParseBatchImageFolderParam(): +return $default(_that.parentDirPath,_that.imagePaths);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? parentDirPath, List? imagePaths)? $default,) {final _that = this; +switch (_that) { +case _ParseBatchImageFolderParam() when $default != null: +return $default(_that.parentDirPath,_that.imagePaths);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseBatchImageFolderParam implements ParseBatchImageFolderParam { + const _ParseBatchImageFolderParam({this.parentDirPath, final List? imagePaths}): _imagePaths = imagePaths; + + +@override final String? parentDirPath; + final List? _imagePaths; +@override List? get imagePaths { + final value = _imagePaths; + if (value == null) return null; + if (_imagePaths is EqualUnmodifiableListView) return _imagePaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + + +/// Create a copy of ParseBatchImageFolderParam +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseBatchImageFolderParamCopyWith<_ParseBatchImageFolderParam> get copyWith => __$ParseBatchImageFolderParamCopyWithImpl<_ParseBatchImageFolderParam>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseBatchImageFolderParam&&(identical(other.parentDirPath, parentDirPath) || other.parentDirPath == parentDirPath)&&const DeepCollectionEquality().equals(other._imagePaths, _imagePaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,parentDirPath,const DeepCollectionEquality().hash(_imagePaths)); + +@override +String toString() { + return 'ParseBatchImageFolderParam(parentDirPath: $parentDirPath, imagePaths: $imagePaths)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseBatchImageFolderParamCopyWith<$Res> implements $ParseBatchImageFolderParamCopyWith<$Res> { + factory _$ParseBatchImageFolderParamCopyWith(_ParseBatchImageFolderParam value, $Res Function(_ParseBatchImageFolderParam) _then) = __$ParseBatchImageFolderParamCopyWithImpl; +@override @useResult +$Res call({ + String? parentDirPath, List? imagePaths +}); + + + + +} +/// @nodoc +class __$ParseBatchImageFolderParamCopyWithImpl<$Res> + implements _$ParseBatchImageFolderParamCopyWith<$Res> { + __$ParseBatchImageFolderParamCopyWithImpl(this._self, this._then); + + final _ParseBatchImageFolderParam _self; + final $Res Function(_ParseBatchImageFolderParam) _then; + +/// Create a copy of ParseBatchImageFolderParam +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? parentDirPath = freezed,Object? imagePaths = freezed,}) { + return _then(_ParseBatchImageFolderParam( +parentDirPath: freezed == parentDirPath ? _self.parentDirPath : parentDirPath // ignore: cast_nullable_to_non_nullable +as String?,imagePaths: freezed == imagePaths ? _self._imagePaths : imagePaths // ignore: cast_nullable_to_non_nullable +as List?, + )); +} + + +} + +// dart format on diff --git a/lib/feature/parse/ui/provider/parse_batch_image_folder.g.dart b/lib/feature/parse/ui/provider/parse_batch_image_folder.g.dart new file mode 100644 index 0000000..b24d98c --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_batch_image_folder.g.dart @@ -0,0 +1,210 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'parse_batch_image_folder.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ParseBatchImageFolder) +final parseBatchImageFolderProvider = ParseBatchImageFolderFamily._(); + +final class ParseBatchImageFolderProvider + extends + $AsyncNotifierProvider< + ParseBatchImageFolder, + ParseBatchImageFolderState + > { + ParseBatchImageFolderProvider._({ + required ParseBatchImageFolderFamily super.from, + required ParseBatchImageFolderParam super.argument, + }) : super( + retry: null, + name: r'parseBatchImageFolderProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$parseBatchImageFolderHash(); + + @override + String toString() { + return r'parseBatchImageFolderProvider' + '' + '($argument)'; + } + + @$internal + @override + ParseBatchImageFolder create() => ParseBatchImageFolder(); + + @override + bool operator ==(Object other) { + return other is ParseBatchImageFolderProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$parseBatchImageFolderHash() => + r'020c694ab8f1d17f99bf74075a61d1908039bb32'; + +final class ParseBatchImageFolderFamily extends $Family + with + $ClassFamilyOverride< + ParseBatchImageFolder, + AsyncValue, + ParseBatchImageFolderState, + FutureOr, + ParseBatchImageFolderParam + > { + ParseBatchImageFolderFamily._() + : super( + retry: null, + name: r'parseBatchImageFolderProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + ParseBatchImageFolderProvider call(ParseBatchImageFolderParam param) => + ParseBatchImageFolderProvider._(argument: param, from: this); + + @override + String toString() => r'parseBatchImageFolderProvider'; +} + +abstract class _$ParseBatchImageFolder + extends $AsyncNotifier { + late final _$args = ref.$arg as ParseBatchImageFolderParam; + ParseBatchImageFolderParam get param => _$args; + + FutureOr build(ParseBatchImageFolderParam param); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref + as $Ref< + AsyncValue, + ParseBatchImageFolderState + >; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + AsyncValue, + ParseBatchImageFolderState + >, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} + +@ProviderFor(SaveBatchAsBook) +final saveBatchAsBookProvider = SaveBatchAsBookFamily._(); + +final class SaveBatchAsBookProvider + extends $NotifierProvider { + SaveBatchAsBookProvider._({ + required SaveBatchAsBookFamily super.from, + required ParseBatchImageFolderParam super.argument, + }) : super( + retry: null, + name: r'saveBatchAsBookProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$saveBatchAsBookHash(); + + @override + String toString() { + return r'saveBatchAsBookProvider' + '' + '($argument)'; + } + + @$internal + @override + SaveBatchAsBook create() => SaveBatchAsBook(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(SaveBatchAsBookState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } + + @override + bool operator ==(Object other) { + return other is SaveBatchAsBookProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$saveBatchAsBookHash() => r'aca3134e86c0e740a17d3e20b69c137043d9bb74'; + +final class SaveBatchAsBookFamily extends $Family + with + $ClassFamilyOverride< + SaveBatchAsBook, + SaveBatchAsBookState, + SaveBatchAsBookState, + SaveBatchAsBookState, + ParseBatchImageFolderParam + > { + SaveBatchAsBookFamily._() + : super( + retry: null, + name: r'saveBatchAsBookProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + SaveBatchAsBookProvider call(ParseBatchImageFolderParam param) => + SaveBatchAsBookProvider._(argument: param, from: this); + + @override + String toString() => r'saveBatchAsBookProvider'; +} + +abstract class _$SaveBatchAsBook extends $Notifier { + late final _$args = ref.$arg as ParseBatchImageFolderParam; + ParseBatchImageFolderParam get param => _$args; + + SaveBatchAsBookState build(ParseBatchImageFolderParam param); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + SaveBatchAsBookState, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} diff --git a/lib/feature/parse/ui/provider/parse_batch_pdf_provider.dart b/lib/feature/parse/ui/provider/parse_batch_pdf_provider.dart new file mode 100644 index 0000000..fd49e68 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_batch_pdf_provider.dart @@ -0,0 +1,229 @@ +import 'dart:io'; + +import 'package:flutter_riverpod/legacy.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tele_book/feature/book/model/dto/save_as_book_dto.dart'; +import 'package:tele_book/feature/book/repository/book_repository.dart'; +import 'package:tele_book/feature/parse/model/parse_batch_archive_vo.dart'; +import 'package:tele_book/feature/parse/service/parse_pdf_service.dart'; + +part 'parse_batch_pdf_provider.freezed.dart'; + +part 'parse_batch_pdf_provider.g.dart'; + +@freezed +abstract class ParseBatchPdfProgress with _$ParseBatchPdfProgress { + const factory ParseBatchPdfProgress({ + required int completeCount, + required int totalCount, + required String currentFileName, + required int currentFileProgress, + required int currentFileTotal, + }) = _ParseBatchPdfProgress; +} + +@freezed +abstract class ParseBatchPdfState with _$ParseBatchPdfState { + const factory ParseBatchPdfState({ + required List parseBatchList, + }) = _ParseBatchPdfState; +} + +@freezed +abstract class ParseBatchPdfParam with _$ParseBatchPdfParam { + const factory ParseBatchPdfParam({ + required String? pdfDirPath, + required List? pdfPaths, + }) = _ParseBatchPdfParam; +} + +@freezed +abstract class ParseBatchPdfSaveBookProgressState + with _$ParseBatchPdfSaveBookProgressState { + const factory ParseBatchPdfSaveBookProgressState({ + @Default(0) int current, + @Default(0) int total, + @Default(SaveStep.generateCover) SaveStep step, + @Default(0) int stepCurrent, + @Default(0) int stepTotal, + @Default(0) int bookIndex, + }) = _ParseBatchPdfSaveBookProgressState; + + const ParseBatchPdfSaveBookProgressState._(); + + String get stepText { + final bookInfo = total > 0 ? '(${bookIndex + 1}/$total) ' : ''; + return switch (step) { + SaveStep.generateCover => '$bookInfo生成封面图...', + SaveStep.generatePreview => '$bookInfo生成预览图... ($stepCurrent/$stepTotal)', + SaveStep.saveOriginal => '$bookInfo保存原图... ($stepCurrent/$stepTotal)', + SaveStep.saveDatabase => '保存数据...', + }; + } +} + +final parseBatchProgressProvider = StateProvider((ref) { + return ParseBatchPdfProgress( + completeCount: 0, + totalCount: 0, + currentFileName: '', + currentFileProgress: 0, + currentFileTotal: 0, + ); +}); + +@riverpod +class ParseBatchPdf extends _$ParseBatchPdf { + ParsePdfService get _service => ref.read(parsePdfServiceProvider); + + late final ParseBatchPdfParam _param; + + @override + FutureOr build(ParseBatchPdfParam param) async { + _param = param; + return await _parseBatch(); + } + + Future _requestStoragePermission() async { + if (!Platform.isAndroid) return true; + if (await Permission.manageExternalStorage.isGranted) return true; + final status = await Permission.manageExternalStorage.request(); + if (status.isGranted) return true; + if (await Permission.storage.isGranted) return true; + final storageStatus = await Permission.storage.request(); + return storageStatus.isGranted; + } + + Future _parseBatch() async { + state = const AsyncValue.loading(); + final hasPermission = await _requestStoragePermission(); + if (!hasPermission) { + throw Exception("需要存储权限才能解析PDF"); + } + + final pdfPaths = _param.pdfPaths; + final pdfDirPath = _param.pdfDirPath; + final parseResult = pdfPaths != null && pdfPaths.isNotEmpty + ? await _service.parseBatchPdfsFromPaths( + pdfPaths, + (total) { + if (!ref.mounted) return; + final progress = ref.read(parseBatchProgressProvider); + ref.read(parseBatchProgressProvider.notifier).state = progress + .copyWith(totalCount: total); + }, + (count) { + if (!ref.mounted) return; + final progress = ref.read(parseBatchProgressProvider); + ref.read(parseBatchProgressProvider.notifier).state = progress + .copyWith(completeCount: count); + }, + onCurrentFileChanged: (fileName) { + if (!ref.mounted) return; + final progress = ref.read(parseBatchProgressProvider); + ref.read(parseBatchProgressProvider.notifier).state = progress + .copyWith( + currentFileName: fileName, + currentFileProgress: 0, + currentFileTotal: 0, + ); + }, + onCurrentFileProgress: (current, total) { + if (!ref.mounted) return; + final progress = ref.read(parseBatchProgressProvider); + ref.read(parseBatchProgressProvider.notifier).state = progress + .copyWith( + currentFileProgress: current, + currentFileTotal: total, + ); + }, + ) + : await _service.parseBatchPdfs( + pdfDirPath ?? '', + (total) { + if (!ref.mounted) return; + final progress = ref.read(parseBatchProgressProvider); + ref.read(parseBatchProgressProvider.notifier).state = progress + .copyWith(totalCount: total); + }, + (count) { + if (!ref.mounted) return; + final progress = ref.read(parseBatchProgressProvider); + ref.read(parseBatchProgressProvider.notifier).state = progress + .copyWith(completeCount: count); + }, + onCurrentFileChanged: (fileName) { + if (!ref.mounted) return; + final progress = ref.read(parseBatchProgressProvider); + ref.read(parseBatchProgressProvider.notifier).state = progress + .copyWith( + currentFileName: fileName, + currentFileProgress: 0, + currentFileTotal: 0, + ); + }, + onCurrentFileProgress: (current, total) { + if (!ref.mounted) return; + final progress = ref.read(parseBatchProgressProvider); + ref.read(parseBatchProgressProvider.notifier).state = progress + .copyWith( + currentFileProgress: current, + currentFileTotal: total, + ); + }, + ); + + if (parseResult.isError) { + throw Exception(parseResult.error); + } + + return ParseBatchPdfState(parseBatchList: parseResult.data!); + } +} + +final parseBatchPdfSaveBookProgressProvider = + StateProvider((ref) { + return const ParseBatchPdfSaveBookProgressState(); + }); + +@riverpod +class ParseBatchPdfSaveBook extends _$ParseBatchPdfSaveBook { + BookRepository get _repository => ref.read(bookRepositoryProvider); + + @override + FutureOr build() => null; + + Future saveBatchAsBook(List parseBatchList) async { + if (parseBatchList.isEmpty) return; + + state = const AsyncValue.loading(); + + final dos = parseBatchList + .map((e) => SaveAsBookDto(title: e.name, paths: e.tempPaths)) + .toList(); + + ref.read(parseBatchPdfSaveBookProgressProvider.notifier).state = + ParseBatchPdfSaveBookProgressState( + current: 0, + total: parseBatchList.length, + ); + + + final result = await _repository.saveBatchAsBooks(dos, (count) { + final progress = ref.read(parseBatchPdfSaveBookProgressProvider); + ref.read(parseBatchPdfSaveBookProgressProvider.notifier).state = progress + .copyWith(current: count); + }); + + result.fold( + onSuccess: (_) { + state = const AsyncValue.data(null); + }, + onError: (error) { + state = AsyncError(error.message, StackTrace.current); + }, + ); + } +} diff --git a/lib/feature/parse/ui/provider/parse_batch_pdf_provider.freezed.dart b/lib/feature/parse/ui/provider/parse_batch_pdf_provider.freezed.dart new file mode 100644 index 0000000..bb23c44 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_batch_pdf_provider.freezed.dart @@ -0,0 +1,1086 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'parse_batch_pdf_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ParseBatchPdfProgress { + + int get completeCount; int get totalCount; String get currentFileName; int get currentFileProgress; int get currentFileTotal; +/// Create a copy of ParseBatchPdfProgress +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseBatchPdfProgressCopyWith get copyWith => _$ParseBatchPdfProgressCopyWithImpl(this as ParseBatchPdfProgress, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseBatchPdfProgress&&(identical(other.completeCount, completeCount) || other.completeCount == completeCount)&&(identical(other.totalCount, totalCount) || other.totalCount == totalCount)&&(identical(other.currentFileName, currentFileName) || other.currentFileName == currentFileName)&&(identical(other.currentFileProgress, currentFileProgress) || other.currentFileProgress == currentFileProgress)&&(identical(other.currentFileTotal, currentFileTotal) || other.currentFileTotal == currentFileTotal)); +} + + +@override +int get hashCode => Object.hash(runtimeType,completeCount,totalCount,currentFileName,currentFileProgress,currentFileTotal); + +@override +String toString() { + return 'ParseBatchPdfProgress(completeCount: $completeCount, totalCount: $totalCount, currentFileName: $currentFileName, currentFileProgress: $currentFileProgress, currentFileTotal: $currentFileTotal)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseBatchPdfProgressCopyWith<$Res> { + factory $ParseBatchPdfProgressCopyWith(ParseBatchPdfProgress value, $Res Function(ParseBatchPdfProgress) _then) = _$ParseBatchPdfProgressCopyWithImpl; +@useResult +$Res call({ + int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal +}); + + + + +} +/// @nodoc +class _$ParseBatchPdfProgressCopyWithImpl<$Res> + implements $ParseBatchPdfProgressCopyWith<$Res> { + _$ParseBatchPdfProgressCopyWithImpl(this._self, this._then); + + final ParseBatchPdfProgress _self; + final $Res Function(ParseBatchPdfProgress) _then; + +/// Create a copy of ParseBatchPdfProgress +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? completeCount = null,Object? totalCount = null,Object? currentFileName = null,Object? currentFileProgress = null,Object? currentFileTotal = null,}) { + return _then(_self.copyWith( +completeCount: null == completeCount ? _self.completeCount : completeCount // ignore: cast_nullable_to_non_nullable +as int,totalCount: null == totalCount ? _self.totalCount : totalCount // ignore: cast_nullable_to_non_nullable +as int,currentFileName: null == currentFileName ? _self.currentFileName : currentFileName // ignore: cast_nullable_to_non_nullable +as String,currentFileProgress: null == currentFileProgress ? _self.currentFileProgress : currentFileProgress // ignore: cast_nullable_to_non_nullable +as int,currentFileTotal: null == currentFileTotal ? _self.currentFileTotal : currentFileTotal // ignore: cast_nullable_to_non_nullable +as int, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseBatchPdfProgress]. +extension ParseBatchPdfProgressPatterns on ParseBatchPdfProgress { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseBatchPdfProgress value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseBatchPdfProgress() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseBatchPdfProgress value) $default,){ +final _that = this; +switch (_that) { +case _ParseBatchPdfProgress(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseBatchPdfProgress value)? $default,){ +final _that = this; +switch (_that) { +case _ParseBatchPdfProgress() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseBatchPdfProgress() when $default != null: +return $default(_that.completeCount,_that.totalCount,_that.currentFileName,_that.currentFileProgress,_that.currentFileTotal);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal) $default,) {final _that = this; +switch (_that) { +case _ParseBatchPdfProgress(): +return $default(_that.completeCount,_that.totalCount,_that.currentFileName,_that.currentFileProgress,_that.currentFileTotal);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal)? $default,) {final _that = this; +switch (_that) { +case _ParseBatchPdfProgress() when $default != null: +return $default(_that.completeCount,_that.totalCount,_that.currentFileName,_that.currentFileProgress,_that.currentFileTotal);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseBatchPdfProgress implements ParseBatchPdfProgress { + const _ParseBatchPdfProgress({required this.completeCount, required this.totalCount, required this.currentFileName, required this.currentFileProgress, required this.currentFileTotal}); + + +@override final int completeCount; +@override final int totalCount; +@override final String currentFileName; +@override final int currentFileProgress; +@override final int currentFileTotal; + +/// Create a copy of ParseBatchPdfProgress +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseBatchPdfProgressCopyWith<_ParseBatchPdfProgress> get copyWith => __$ParseBatchPdfProgressCopyWithImpl<_ParseBatchPdfProgress>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseBatchPdfProgress&&(identical(other.completeCount, completeCount) || other.completeCount == completeCount)&&(identical(other.totalCount, totalCount) || other.totalCount == totalCount)&&(identical(other.currentFileName, currentFileName) || other.currentFileName == currentFileName)&&(identical(other.currentFileProgress, currentFileProgress) || other.currentFileProgress == currentFileProgress)&&(identical(other.currentFileTotal, currentFileTotal) || other.currentFileTotal == currentFileTotal)); +} + + +@override +int get hashCode => Object.hash(runtimeType,completeCount,totalCount,currentFileName,currentFileProgress,currentFileTotal); + +@override +String toString() { + return 'ParseBatchPdfProgress(completeCount: $completeCount, totalCount: $totalCount, currentFileName: $currentFileName, currentFileProgress: $currentFileProgress, currentFileTotal: $currentFileTotal)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseBatchPdfProgressCopyWith<$Res> implements $ParseBatchPdfProgressCopyWith<$Res> { + factory _$ParseBatchPdfProgressCopyWith(_ParseBatchPdfProgress value, $Res Function(_ParseBatchPdfProgress) _then) = __$ParseBatchPdfProgressCopyWithImpl; +@override @useResult +$Res call({ + int completeCount, int totalCount, String currentFileName, int currentFileProgress, int currentFileTotal +}); + + + + +} +/// @nodoc +class __$ParseBatchPdfProgressCopyWithImpl<$Res> + implements _$ParseBatchPdfProgressCopyWith<$Res> { + __$ParseBatchPdfProgressCopyWithImpl(this._self, this._then); + + final _ParseBatchPdfProgress _self; + final $Res Function(_ParseBatchPdfProgress) _then; + +/// Create a copy of ParseBatchPdfProgress +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? completeCount = null,Object? totalCount = null,Object? currentFileName = null,Object? currentFileProgress = null,Object? currentFileTotal = null,}) { + return _then(_ParseBatchPdfProgress( +completeCount: null == completeCount ? _self.completeCount : completeCount // ignore: cast_nullable_to_non_nullable +as int,totalCount: null == totalCount ? _self.totalCount : totalCount // ignore: cast_nullable_to_non_nullable +as int,currentFileName: null == currentFileName ? _self.currentFileName : currentFileName // ignore: cast_nullable_to_non_nullable +as String,currentFileProgress: null == currentFileProgress ? _self.currentFileProgress : currentFileProgress // ignore: cast_nullable_to_non_nullable +as int,currentFileTotal: null == currentFileTotal ? _self.currentFileTotal : currentFileTotal // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + +/// @nodoc +mixin _$ParseBatchPdfState { + + List get parseBatchList; +/// Create a copy of ParseBatchPdfState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseBatchPdfStateCopyWith get copyWith => _$ParseBatchPdfStateCopyWithImpl(this as ParseBatchPdfState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseBatchPdfState&&const DeepCollectionEquality().equals(other.parseBatchList, parseBatchList)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(parseBatchList)); + +@override +String toString() { + return 'ParseBatchPdfState(parseBatchList: $parseBatchList)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseBatchPdfStateCopyWith<$Res> { + factory $ParseBatchPdfStateCopyWith(ParseBatchPdfState value, $Res Function(ParseBatchPdfState) _then) = _$ParseBatchPdfStateCopyWithImpl; +@useResult +$Res call({ + List parseBatchList +}); + + + + +} +/// @nodoc +class _$ParseBatchPdfStateCopyWithImpl<$Res> + implements $ParseBatchPdfStateCopyWith<$Res> { + _$ParseBatchPdfStateCopyWithImpl(this._self, this._then); + + final ParseBatchPdfState _self; + final $Res Function(ParseBatchPdfState) _then; + +/// Create a copy of ParseBatchPdfState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? parseBatchList = null,}) { + return _then(_self.copyWith( +parseBatchList: null == parseBatchList ? _self.parseBatchList : parseBatchList // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseBatchPdfState]. +extension ParseBatchPdfStatePatterns on ParseBatchPdfState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseBatchPdfState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseBatchPdfState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseBatchPdfState value) $default,){ +final _that = this; +switch (_that) { +case _ParseBatchPdfState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseBatchPdfState value)? $default,){ +final _that = this; +switch (_that) { +case _ParseBatchPdfState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List parseBatchList)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseBatchPdfState() when $default != null: +return $default(_that.parseBatchList);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List parseBatchList) $default,) {final _that = this; +switch (_that) { +case _ParseBatchPdfState(): +return $default(_that.parseBatchList);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List parseBatchList)? $default,) {final _that = this; +switch (_that) { +case _ParseBatchPdfState() when $default != null: +return $default(_that.parseBatchList);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseBatchPdfState implements ParseBatchPdfState { + const _ParseBatchPdfState({required final List parseBatchList}): _parseBatchList = parseBatchList; + + + final List _parseBatchList; +@override List get parseBatchList { + if (_parseBatchList is EqualUnmodifiableListView) return _parseBatchList; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_parseBatchList); +} + + +/// Create a copy of ParseBatchPdfState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseBatchPdfStateCopyWith<_ParseBatchPdfState> get copyWith => __$ParseBatchPdfStateCopyWithImpl<_ParseBatchPdfState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseBatchPdfState&&const DeepCollectionEquality().equals(other._parseBatchList, _parseBatchList)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_parseBatchList)); + +@override +String toString() { + return 'ParseBatchPdfState(parseBatchList: $parseBatchList)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseBatchPdfStateCopyWith<$Res> implements $ParseBatchPdfStateCopyWith<$Res> { + factory _$ParseBatchPdfStateCopyWith(_ParseBatchPdfState value, $Res Function(_ParseBatchPdfState) _then) = __$ParseBatchPdfStateCopyWithImpl; +@override @useResult +$Res call({ + List parseBatchList +}); + + + + +} +/// @nodoc +class __$ParseBatchPdfStateCopyWithImpl<$Res> + implements _$ParseBatchPdfStateCopyWith<$Res> { + __$ParseBatchPdfStateCopyWithImpl(this._self, this._then); + + final _ParseBatchPdfState _self; + final $Res Function(_ParseBatchPdfState) _then; + +/// Create a copy of ParseBatchPdfState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? parseBatchList = null,}) { + return _then(_ParseBatchPdfState( +parseBatchList: null == parseBatchList ? _self._parseBatchList : parseBatchList // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc +mixin _$ParseBatchPdfParam { + + String? get pdfDirPath; List? get pdfPaths; +/// Create a copy of ParseBatchPdfParam +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseBatchPdfParamCopyWith get copyWith => _$ParseBatchPdfParamCopyWithImpl(this as ParseBatchPdfParam, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseBatchPdfParam&&(identical(other.pdfDirPath, pdfDirPath) || other.pdfDirPath == pdfDirPath)&&const DeepCollectionEquality().equals(other.pdfPaths, pdfPaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,pdfDirPath,const DeepCollectionEquality().hash(pdfPaths)); + +@override +String toString() { + return 'ParseBatchPdfParam(pdfDirPath: $pdfDirPath, pdfPaths: $pdfPaths)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseBatchPdfParamCopyWith<$Res> { + factory $ParseBatchPdfParamCopyWith(ParseBatchPdfParam value, $Res Function(ParseBatchPdfParam) _then) = _$ParseBatchPdfParamCopyWithImpl; +@useResult +$Res call({ + String? pdfDirPath, List? pdfPaths +}); + + + + +} +/// @nodoc +class _$ParseBatchPdfParamCopyWithImpl<$Res> + implements $ParseBatchPdfParamCopyWith<$Res> { + _$ParseBatchPdfParamCopyWithImpl(this._self, this._then); + + final ParseBatchPdfParam _self; + final $Res Function(ParseBatchPdfParam) _then; + +/// Create a copy of ParseBatchPdfParam +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? pdfDirPath = freezed,Object? pdfPaths = freezed,}) { + return _then(_self.copyWith( +pdfDirPath: freezed == pdfDirPath ? _self.pdfDirPath : pdfDirPath // ignore: cast_nullable_to_non_nullable +as String?,pdfPaths: freezed == pdfPaths ? _self.pdfPaths : pdfPaths // ignore: cast_nullable_to_non_nullable +as List?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseBatchPdfParam]. +extension ParseBatchPdfParamPatterns on ParseBatchPdfParam { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseBatchPdfParam value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseBatchPdfParam() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseBatchPdfParam value) $default,){ +final _that = this; +switch (_that) { +case _ParseBatchPdfParam(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseBatchPdfParam value)? $default,){ +final _that = this; +switch (_that) { +case _ParseBatchPdfParam() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String? pdfDirPath, List? pdfPaths)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseBatchPdfParam() when $default != null: +return $default(_that.pdfDirPath,_that.pdfPaths);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String? pdfDirPath, List? pdfPaths) $default,) {final _that = this; +switch (_that) { +case _ParseBatchPdfParam(): +return $default(_that.pdfDirPath,_that.pdfPaths);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? pdfDirPath, List? pdfPaths)? $default,) {final _that = this; +switch (_that) { +case _ParseBatchPdfParam() when $default != null: +return $default(_that.pdfDirPath,_that.pdfPaths);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseBatchPdfParam implements ParseBatchPdfParam { + const _ParseBatchPdfParam({required this.pdfDirPath, required final List? pdfPaths}): _pdfPaths = pdfPaths; + + +@override final String? pdfDirPath; + final List? _pdfPaths; +@override List? get pdfPaths { + final value = _pdfPaths; + if (value == null) return null; + if (_pdfPaths is EqualUnmodifiableListView) return _pdfPaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + + +/// Create a copy of ParseBatchPdfParam +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseBatchPdfParamCopyWith<_ParseBatchPdfParam> get copyWith => __$ParseBatchPdfParamCopyWithImpl<_ParseBatchPdfParam>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseBatchPdfParam&&(identical(other.pdfDirPath, pdfDirPath) || other.pdfDirPath == pdfDirPath)&&const DeepCollectionEquality().equals(other._pdfPaths, _pdfPaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,pdfDirPath,const DeepCollectionEquality().hash(_pdfPaths)); + +@override +String toString() { + return 'ParseBatchPdfParam(pdfDirPath: $pdfDirPath, pdfPaths: $pdfPaths)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseBatchPdfParamCopyWith<$Res> implements $ParseBatchPdfParamCopyWith<$Res> { + factory _$ParseBatchPdfParamCopyWith(_ParseBatchPdfParam value, $Res Function(_ParseBatchPdfParam) _then) = __$ParseBatchPdfParamCopyWithImpl; +@override @useResult +$Res call({ + String? pdfDirPath, List? pdfPaths +}); + + + + +} +/// @nodoc +class __$ParseBatchPdfParamCopyWithImpl<$Res> + implements _$ParseBatchPdfParamCopyWith<$Res> { + __$ParseBatchPdfParamCopyWithImpl(this._self, this._then); + + final _ParseBatchPdfParam _self; + final $Res Function(_ParseBatchPdfParam) _then; + +/// Create a copy of ParseBatchPdfParam +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? pdfDirPath = freezed,Object? pdfPaths = freezed,}) { + return _then(_ParseBatchPdfParam( +pdfDirPath: freezed == pdfDirPath ? _self.pdfDirPath : pdfDirPath // ignore: cast_nullable_to_non_nullable +as String?,pdfPaths: freezed == pdfPaths ? _self._pdfPaths : pdfPaths // ignore: cast_nullable_to_non_nullable +as List?, + )); +} + + +} + +/// @nodoc +mixin _$ParseBatchPdfSaveBookProgressState { + + int get current; int get total; SaveStep get step; int get stepCurrent; int get stepTotal; int get bookIndex; +/// Create a copy of ParseBatchPdfSaveBookProgressState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseBatchPdfSaveBookProgressStateCopyWith get copyWith => _$ParseBatchPdfSaveBookProgressStateCopyWithImpl(this as ParseBatchPdfSaveBookProgressState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseBatchPdfSaveBookProgressState&&(identical(other.current, current) || other.current == current)&&(identical(other.total, total) || other.total == total)&&(identical(other.step, step) || other.step == step)&&(identical(other.stepCurrent, stepCurrent) || other.stepCurrent == stepCurrent)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.bookIndex, bookIndex) || other.bookIndex == bookIndex)); +} + + +@override +int get hashCode => Object.hash(runtimeType,current,total,step,stepCurrent,stepTotal,bookIndex); + +@override +String toString() { + return 'ParseBatchPdfSaveBookProgressState(current: $current, total: $total, step: $step, stepCurrent: $stepCurrent, stepTotal: $stepTotal, bookIndex: $bookIndex)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseBatchPdfSaveBookProgressStateCopyWith<$Res> { + factory $ParseBatchPdfSaveBookProgressStateCopyWith(ParseBatchPdfSaveBookProgressState value, $Res Function(ParseBatchPdfSaveBookProgressState) _then) = _$ParseBatchPdfSaveBookProgressStateCopyWithImpl; +@useResult +$Res call({ + int current, int total, SaveStep step, int stepCurrent, int stepTotal, int bookIndex +}); + + + + +} +/// @nodoc +class _$ParseBatchPdfSaveBookProgressStateCopyWithImpl<$Res> + implements $ParseBatchPdfSaveBookProgressStateCopyWith<$Res> { + _$ParseBatchPdfSaveBookProgressStateCopyWithImpl(this._self, this._then); + + final ParseBatchPdfSaveBookProgressState _self; + final $Res Function(ParseBatchPdfSaveBookProgressState) _then; + +/// Create a copy of ParseBatchPdfSaveBookProgressState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? current = null,Object? total = null,Object? step = null,Object? stepCurrent = null,Object? stepTotal = null,Object? bookIndex = null,}) { + return _then(_self.copyWith( +current: null == current ? _self.current : current // ignore: cast_nullable_to_non_nullable +as int,total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable +as int,step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable +as SaveStep,stepCurrent: null == stepCurrent ? _self.stepCurrent : stepCurrent // ignore: cast_nullable_to_non_nullable +as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable +as int,bookIndex: null == bookIndex ? _self.bookIndex : bookIndex // ignore: cast_nullable_to_non_nullable +as int, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseBatchPdfSaveBookProgressState]. +extension ParseBatchPdfSaveBookProgressStatePatterns on ParseBatchPdfSaveBookProgressState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseBatchPdfSaveBookProgressState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseBatchPdfSaveBookProgressState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseBatchPdfSaveBookProgressState value) $default,){ +final _that = this; +switch (_that) { +case _ParseBatchPdfSaveBookProgressState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseBatchPdfSaveBookProgressState value)? $default,){ +final _that = this; +switch (_that) { +case _ParseBatchPdfSaveBookProgressState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( int current, int total, SaveStep step, int stepCurrent, int stepTotal, int bookIndex)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseBatchPdfSaveBookProgressState() when $default != null: +return $default(_that.current,_that.total,_that.step,_that.stepCurrent,_that.stepTotal,_that.bookIndex);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( int current, int total, SaveStep step, int stepCurrent, int stepTotal, int bookIndex) $default,) {final _that = this; +switch (_that) { +case _ParseBatchPdfSaveBookProgressState(): +return $default(_that.current,_that.total,_that.step,_that.stepCurrent,_that.stepTotal,_that.bookIndex);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int current, int total, SaveStep step, int stepCurrent, int stepTotal, int bookIndex)? $default,) {final _that = this; +switch (_that) { +case _ParseBatchPdfSaveBookProgressState() when $default != null: +return $default(_that.current,_that.total,_that.step,_that.stepCurrent,_that.stepTotal,_that.bookIndex);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseBatchPdfSaveBookProgressState extends ParseBatchPdfSaveBookProgressState { + const _ParseBatchPdfSaveBookProgressState({this.current = 0, this.total = 0, this.step = SaveStep.generateCover, this.stepCurrent = 0, this.stepTotal = 0, this.bookIndex = 0}): super._(); + + +@override@JsonKey() final int current; +@override@JsonKey() final int total; +@override@JsonKey() final SaveStep step; +@override@JsonKey() final int stepCurrent; +@override@JsonKey() final int stepTotal; +@override@JsonKey() final int bookIndex; + +/// Create a copy of ParseBatchPdfSaveBookProgressState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseBatchPdfSaveBookProgressStateCopyWith<_ParseBatchPdfSaveBookProgressState> get copyWith => __$ParseBatchPdfSaveBookProgressStateCopyWithImpl<_ParseBatchPdfSaveBookProgressState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseBatchPdfSaveBookProgressState&&(identical(other.current, current) || other.current == current)&&(identical(other.total, total) || other.total == total)&&(identical(other.step, step) || other.step == step)&&(identical(other.stepCurrent, stepCurrent) || other.stepCurrent == stepCurrent)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.bookIndex, bookIndex) || other.bookIndex == bookIndex)); +} + + +@override +int get hashCode => Object.hash(runtimeType,current,total,step,stepCurrent,stepTotal,bookIndex); + +@override +String toString() { + return 'ParseBatchPdfSaveBookProgressState(current: $current, total: $total, step: $step, stepCurrent: $stepCurrent, stepTotal: $stepTotal, bookIndex: $bookIndex)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseBatchPdfSaveBookProgressStateCopyWith<$Res> implements $ParseBatchPdfSaveBookProgressStateCopyWith<$Res> { + factory _$ParseBatchPdfSaveBookProgressStateCopyWith(_ParseBatchPdfSaveBookProgressState value, $Res Function(_ParseBatchPdfSaveBookProgressState) _then) = __$ParseBatchPdfSaveBookProgressStateCopyWithImpl; +@override @useResult +$Res call({ + int current, int total, SaveStep step, int stepCurrent, int stepTotal, int bookIndex +}); + + + + +} +/// @nodoc +class __$ParseBatchPdfSaveBookProgressStateCopyWithImpl<$Res> + implements _$ParseBatchPdfSaveBookProgressStateCopyWith<$Res> { + __$ParseBatchPdfSaveBookProgressStateCopyWithImpl(this._self, this._then); + + final _ParseBatchPdfSaveBookProgressState _self; + final $Res Function(_ParseBatchPdfSaveBookProgressState) _then; + +/// Create a copy of ParseBatchPdfSaveBookProgressState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? current = null,Object? total = null,Object? step = null,Object? stepCurrent = null,Object? stepTotal = null,Object? bookIndex = null,}) { + return _then(_ParseBatchPdfSaveBookProgressState( +current: null == current ? _self.current : current // ignore: cast_nullable_to_non_nullable +as int,total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable +as int,step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable +as SaveStep,stepCurrent: null == stepCurrent ? _self.stepCurrent : stepCurrent // ignore: cast_nullable_to_non_nullable +as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable +as int,bookIndex: null == bookIndex ? _self.bookIndex : bookIndex // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + +// dart format on diff --git a/lib/feature/parse/ui/provider/parse_batch_pdf_provider.g.dart b/lib/feature/parse/ui/provider/parse_batch_pdf_provider.g.dart new file mode 100644 index 0000000..58f2ead --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_batch_pdf_provider.g.dart @@ -0,0 +1,145 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'parse_batch_pdf_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ParseBatchPdf) +final parseBatchPdfProvider = ParseBatchPdfFamily._(); + +final class ParseBatchPdfProvider + extends $AsyncNotifierProvider { + ParseBatchPdfProvider._({ + required ParseBatchPdfFamily super.from, + required ParseBatchPdfParam super.argument, + }) : super( + retry: null, + name: r'parseBatchPdfProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$parseBatchPdfHash(); + + @override + String toString() { + return r'parseBatchPdfProvider' + '' + '($argument)'; + } + + @$internal + @override + ParseBatchPdf create() => ParseBatchPdf(); + + @override + bool operator ==(Object other) { + return other is ParseBatchPdfProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$parseBatchPdfHash() => r'4528c3d2c95825540661e8230b492c74f4184a00'; + +final class ParseBatchPdfFamily extends $Family + with + $ClassFamilyOverride< + ParseBatchPdf, + AsyncValue, + ParseBatchPdfState, + FutureOr, + ParseBatchPdfParam + > { + ParseBatchPdfFamily._() + : super( + retry: null, + name: r'parseBatchPdfProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + ParseBatchPdfProvider call(ParseBatchPdfParam param) => + ParseBatchPdfProvider._(argument: param, from: this); + + @override + String toString() => r'parseBatchPdfProvider'; +} + +abstract class _$ParseBatchPdf extends $AsyncNotifier { + late final _$args = ref.$arg as ParseBatchPdfParam; + ParseBatchPdfParam get param => _$args; + + FutureOr build(ParseBatchPdfParam param); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref as $Ref, ParseBatchPdfState>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, ParseBatchPdfState>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} + +@ProviderFor(ParseBatchPdfSaveBook) +final parseBatchPdfSaveBookProvider = ParseBatchPdfSaveBookProvider._(); + +final class ParseBatchPdfSaveBookProvider + extends $AsyncNotifierProvider { + ParseBatchPdfSaveBookProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'parseBatchPdfSaveBookProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$parseBatchPdfSaveBookHash(); + + @$internal + @override + ParseBatchPdfSaveBook create() => ParseBatchPdfSaveBook(); +} + +String _$parseBatchPdfSaveBookHash() => + r'4934b2ba4dd24931ffcef41c591f0331bcc18294'; + +abstract class _$ParseBatchPdfSaveBook extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/feature/parse/ui/provider/parse_form_provider.dart b/lib/feature/parse/ui/provider/parse_form_provider.dart new file mode 100644 index 0000000..b8872f8 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_form_provider.dart @@ -0,0 +1,285 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:go_router/go_router.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tele_book/core/route/app_route.dart'; + +part 'parse_form_provider.freezed.dart'; + +part 'parse_form_provider.g.dart'; + +@freezed +abstract class ParseFormState with _$ParseFormState { + const factory ParseFormState({ + @Default(ParseFormType.web) ParseFormType type, + @Default('') String url, + @Default('') String archivePath, + @Default('') String batchArchivePath, + @Default([]) List batchArchivePaths, + @Default('') String imageFolderPath, + @Default([]) List imagePaths, + @Default('') String batchImageFolderPath, + @Default([]) List batchImagePaths, + @Default('') String pdfPath, + @Default('') String batchPdfPath, + @Default([]) List batchPdfPaths, + }) = _ParseFormState; +} + +@riverpod +class ParseForm extends _$ParseForm { + late final TextEditingController urlController; + late final TextEditingController archivePathController; + late final TextEditingController batchArchivePathController; + late final TextEditingController imageFolderPathController; + late final TextEditingController batchImageFolderPathController; + late final TextEditingController pdfPathController; + late final TextEditingController batchPdfPathController; + + @override + ParseFormState build() { + urlController = TextEditingController(); + archivePathController = TextEditingController(); + batchArchivePathController = TextEditingController(); + imageFolderPathController = TextEditingController(); + batchImageFolderPathController = TextEditingController(); + pdfPathController = TextEditingController(); + batchPdfPathController = TextEditingController(); + + ref.onDispose(() { + urlController.dispose(); + archivePathController.dispose(); + batchArchivePathController.dispose(); + imageFolderPathController.dispose(); + batchImageFolderPathController.dispose(); + pdfPathController.dispose(); + batchPdfPathController.dispose(); + }); + + return const ParseFormState(); + } + + void setType(ParseFormType? type) { + if (type == null) return; + state = state.copyWith(type: type); + } + + void onParse(BuildContext context) { + switch (state.type) { + case ParseFormType.web: + context.push(AppRoute.parseWeb, extra: state.url); + break; + case ParseFormType.archive: + context.push(AppRoute.parseArchiveSingle, extra: state.archivePath); + break; + case ParseFormType.batchArchive: + context.push( + AppRoute.parseArchiveBatch, + extra: state.batchArchivePaths.isNotEmpty + ? state.batchArchivePaths + : state.batchArchivePath, + ); + break; + case ParseFormType.imageFolder: + context.push( + AppRoute.parseImageFolder, + extra: state.imagePaths.isNotEmpty + ? state.imagePaths + : state.imageFolderPath, + ); + break; + case ParseFormType.batchImageFolder: + context.push( + AppRoute.parseBatchImageFolder, + extra: state.batchImagePaths.isNotEmpty + ? state.batchImagePaths + : state.batchImageFolderPath, + ); + break; + case ParseFormType.pdf: + context.push(AppRoute.parsePdf, extra: state.pdfPath); + break; + case ParseFormType.batchPdf: + context.push( + AppRoute.parseBatchPdf, + extra: state.batchPdfPaths.isNotEmpty + ? state.batchPdfPaths + : state.batchPdfPath, + ); + break; + } + } + + Future getClipboardUrl() async { + final clipboardData = await Clipboard.getData('text/plain'); + final text = clipboardData?.text ?? ''; + if (Uri.tryParse(text)?.hasAbsolutePath == true) { + urlController.text = text; + state = state.copyWith(url: text); + } + } + + void onUrlChanged(String value) { + state = state.copyWith(url: value); + } + + Future pickerArchive() async { + final result = await FilePicker.platform.pickFiles( + dialogTitle: '选择 tele_book 导出的书籍归档文件', + type: FileType.custom, + allowedExtensions: ['zip'], + ); + if (result != null && result.files.single.path != null) { + final path = result.files.single.path!; + archivePathController.text = path; + state = state.copyWith(archivePath: path); + } + } + + Future pickerBatchArchive() async { + if (Platform.isIOS) { + final result = await FilePicker.platform.pickFiles( + dialogTitle: '选择一个或多个 ZIP 压缩包', + type: FileType.custom, + allowedExtensions: ['zip'], + allowMultiple: true, + ); + if (result != null) { + final paths = result.paths.whereType().toList(); + final text = paths.isEmpty ? '' : '已选择 ${paths.length} 个 ZIP 文件'; + batchArchivePathController.text = text; + state = state.copyWith( + batchArchivePaths: paths, + batchArchivePath: text, + ); + } + return; + } + + final result = await FilePicker.platform.getDirectoryPath( + dialogTitle: '选择 tele_book 导出��书籍归档文件夹', + ); + if (result != null) { + batchArchivePathController.text = result; + state = state.copyWith( + batchArchivePaths: const [], + batchArchivePath: result, + ); + } + } + + Future pickerImageFolder() async { + if (Platform.isIOS) { + final result = await FilePicker.platform.pickFiles( + dialogTitle: '选择图片文件', + type: FileType.custom, + allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'], + allowMultiple: true, + ); + if (result != null) { + final paths = result.paths.whereType().toList(); + final text = paths.isEmpty ? '' : '已选择 ${paths.length} 张图片'; + imageFolderPathController.text = text; + state = state.copyWith(imagePaths: paths, imageFolderPath: text); + } + return; + } + + final result = await FilePicker.platform.getDirectoryPath( + dialogTitle: '选择包含图片的文件夹', + ); + if (result != null) { + imageFolderPathController.text = result; + state = state.copyWith(imagePaths: const [], imageFolderPath: result); + } + } + + Future pickerBatchImageFolder() async { + if (Platform.isIOS) { + final result = await FilePicker.platform.pickFiles( + dialogTitle: '选择批量图片文件', + type: FileType.custom, + allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'], + allowMultiple: true, + ); + if (result != null) { + final paths = result.paths.whereType().toList(); + final text = paths.isEmpty ? '' : '已选择 ${paths.length} 张图片(按所在文件夹分组)'; + batchImageFolderPathController.text = text; + state = state.copyWith( + batchImagePaths: paths, + batchImageFolderPath: text, + ); + } + return; + } + + final result = await FilePicker.platform.getDirectoryPath( + dialogTitle: '选择批量图片文件夹的父目录', + ); + if (result != null) { + batchImageFolderPathController.text = result; + state = state.copyWith( + batchImagePaths: const [], + batchImageFolderPath: result, + ); + } + } + + Future pickerPdf() async { + final result = await FilePicker.platform.pickFiles( + dialogTitle: '选择 PDF 文件', + type: FileType.custom, + allowedExtensions: ['pdf'], + ); + if (result != null && result.files.single.path != null) { + final path = result.files.single.path!; + pdfPathController.text = path; + state = state.copyWith(pdfPath: path); + } + } + + Future pickerBatchPdf() async { + if (Platform.isIOS) { + final result = await FilePicker.platform.pickFiles( + dialogTitle: '选择一个或多个 PDF 文件', + type: FileType.custom, + allowedExtensions: ['pdf'], + allowMultiple: true, + ); + if (result != null) { + final paths = result.paths.whereType().toList(); + final text = paths.isEmpty ? '' : '已选择 ${paths.length} 个 PDF 文件'; + batchPdfPathController.text = text; + state = state.copyWith(batchPdfPaths: paths, batchPdfPath: text); + } + return; + } + + final result = await FilePicker.platform.getDirectoryPath( + dialogTitle: '选择包含 PDF 的文件夹', + ); + if (result != null) { + batchPdfPathController.text = result; + state = state.copyWith(batchPdfPaths: const [], batchPdfPath: result); + } + } +} + +enum ParseFormType { + web("网页"), + archive("压缩包"), + batchArchive("批量压缩包"), + imageFolder("图片文件夹"), + batchImageFolder("批量图片文件夹"), + pdf("PDF"), + batchPdf("批量PDF"); + + final String description; + + const ParseFormType(this.description); +} diff --git a/lib/feature/parse/ui/provider/parse_form_provider.freezed.dart b/lib/feature/parse/ui/provider/parse_form_provider.freezed.dart new file mode 100644 index 0000000..ce5de6d --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_form_provider.freezed.dart @@ -0,0 +1,328 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'parse_form_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ParseFormState { + + ParseFormType get type; String get url; String get archivePath; String get batchArchivePath; List get batchArchivePaths; String get imageFolderPath; List get imagePaths; String get batchImageFolderPath; List get batchImagePaths; String get pdfPath; String get batchPdfPath; List get batchPdfPaths; +/// Create a copy of ParseFormState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseFormStateCopyWith get copyWith => _$ParseFormStateCopyWithImpl(this as ParseFormState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseFormState&&(identical(other.type, type) || other.type == type)&&(identical(other.url, url) || other.url == url)&&(identical(other.archivePath, archivePath) || other.archivePath == archivePath)&&(identical(other.batchArchivePath, batchArchivePath) || other.batchArchivePath == batchArchivePath)&&const DeepCollectionEquality().equals(other.batchArchivePaths, batchArchivePaths)&&(identical(other.imageFolderPath, imageFolderPath) || other.imageFolderPath == imageFolderPath)&&const DeepCollectionEquality().equals(other.imagePaths, imagePaths)&&(identical(other.batchImageFolderPath, batchImageFolderPath) || other.batchImageFolderPath == batchImageFolderPath)&&const DeepCollectionEquality().equals(other.batchImagePaths, batchImagePaths)&&(identical(other.pdfPath, pdfPath) || other.pdfPath == pdfPath)&&(identical(other.batchPdfPath, batchPdfPath) || other.batchPdfPath == batchPdfPath)&&const DeepCollectionEquality().equals(other.batchPdfPaths, batchPdfPaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,type,url,archivePath,batchArchivePath,const DeepCollectionEquality().hash(batchArchivePaths),imageFolderPath,const DeepCollectionEquality().hash(imagePaths),batchImageFolderPath,const DeepCollectionEquality().hash(batchImagePaths),pdfPath,batchPdfPath,const DeepCollectionEquality().hash(batchPdfPaths)); + +@override +String toString() { + return 'ParseFormState(type: $type, url: $url, archivePath: $archivePath, batchArchivePath: $batchArchivePath, batchArchivePaths: $batchArchivePaths, imageFolderPath: $imageFolderPath, imagePaths: $imagePaths, batchImageFolderPath: $batchImageFolderPath, batchImagePaths: $batchImagePaths, pdfPath: $pdfPath, batchPdfPath: $batchPdfPath, batchPdfPaths: $batchPdfPaths)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseFormStateCopyWith<$Res> { + factory $ParseFormStateCopyWith(ParseFormState value, $Res Function(ParseFormState) _then) = _$ParseFormStateCopyWithImpl; +@useResult +$Res call({ + ParseFormType type, String url, String archivePath, String batchArchivePath, List batchArchivePaths, String imageFolderPath, List imagePaths, String batchImageFolderPath, List batchImagePaths, String pdfPath, String batchPdfPath, List batchPdfPaths +}); + + + + +} +/// @nodoc +class _$ParseFormStateCopyWithImpl<$Res> + implements $ParseFormStateCopyWith<$Res> { + _$ParseFormStateCopyWithImpl(this._self, this._then); + + final ParseFormState _self; + final $Res Function(ParseFormState) _then; + +/// Create a copy of ParseFormState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? url = null,Object? archivePath = null,Object? batchArchivePath = null,Object? batchArchivePaths = null,Object? imageFolderPath = null,Object? imagePaths = null,Object? batchImageFolderPath = null,Object? batchImagePaths = null,Object? pdfPath = null,Object? batchPdfPath = null,Object? batchPdfPaths = null,}) { + return _then(_self.copyWith( +type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as ParseFormType,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable +as String,archivePath: null == archivePath ? _self.archivePath : archivePath // ignore: cast_nullable_to_non_nullable +as String,batchArchivePath: null == batchArchivePath ? _self.batchArchivePath : batchArchivePath // ignore: cast_nullable_to_non_nullable +as String,batchArchivePaths: null == batchArchivePaths ? _self.batchArchivePaths : batchArchivePaths // ignore: cast_nullable_to_non_nullable +as List,imageFolderPath: null == imageFolderPath ? _self.imageFolderPath : imageFolderPath // ignore: cast_nullable_to_non_nullable +as String,imagePaths: null == imagePaths ? _self.imagePaths : imagePaths // ignore: cast_nullable_to_non_nullable +as List,batchImageFolderPath: null == batchImageFolderPath ? _self.batchImageFolderPath : batchImageFolderPath // ignore: cast_nullable_to_non_nullable +as String,batchImagePaths: null == batchImagePaths ? _self.batchImagePaths : batchImagePaths // ignore: cast_nullable_to_non_nullable +as List,pdfPath: null == pdfPath ? _self.pdfPath : pdfPath // ignore: cast_nullable_to_non_nullable +as String,batchPdfPath: null == batchPdfPath ? _self.batchPdfPath : batchPdfPath // ignore: cast_nullable_to_non_nullable +as String,batchPdfPaths: null == batchPdfPaths ? _self.batchPdfPaths : batchPdfPaths // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseFormState]. +extension ParseFormStatePatterns on ParseFormState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseFormState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseFormState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseFormState value) $default,){ +final _that = this; +switch (_that) { +case _ParseFormState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseFormState value)? $default,){ +final _that = this; +switch (_that) { +case _ParseFormState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( ParseFormType type, String url, String archivePath, String batchArchivePath, List batchArchivePaths, String imageFolderPath, List imagePaths, String batchImageFolderPath, List batchImagePaths, String pdfPath, String batchPdfPath, List batchPdfPaths)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseFormState() when $default != null: +return $default(_that.type,_that.url,_that.archivePath,_that.batchArchivePath,_that.batchArchivePaths,_that.imageFolderPath,_that.imagePaths,_that.batchImageFolderPath,_that.batchImagePaths,_that.pdfPath,_that.batchPdfPath,_that.batchPdfPaths);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( ParseFormType type, String url, String archivePath, String batchArchivePath, List batchArchivePaths, String imageFolderPath, List imagePaths, String batchImageFolderPath, List batchImagePaths, String pdfPath, String batchPdfPath, List batchPdfPaths) $default,) {final _that = this; +switch (_that) { +case _ParseFormState(): +return $default(_that.type,_that.url,_that.archivePath,_that.batchArchivePath,_that.batchArchivePaths,_that.imageFolderPath,_that.imagePaths,_that.batchImageFolderPath,_that.batchImagePaths,_that.pdfPath,_that.batchPdfPath,_that.batchPdfPaths);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( ParseFormType type, String url, String archivePath, String batchArchivePath, List batchArchivePaths, String imageFolderPath, List imagePaths, String batchImageFolderPath, List batchImagePaths, String pdfPath, String batchPdfPath, List batchPdfPaths)? $default,) {final _that = this; +switch (_that) { +case _ParseFormState() when $default != null: +return $default(_that.type,_that.url,_that.archivePath,_that.batchArchivePath,_that.batchArchivePaths,_that.imageFolderPath,_that.imagePaths,_that.batchImageFolderPath,_that.batchImagePaths,_that.pdfPath,_that.batchPdfPath,_that.batchPdfPaths);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseFormState implements ParseFormState { + const _ParseFormState({this.type = ParseFormType.web, this.url = '', this.archivePath = '', this.batchArchivePath = '', final List batchArchivePaths = const [], this.imageFolderPath = '', final List imagePaths = const [], this.batchImageFolderPath = '', final List batchImagePaths = const [], this.pdfPath = '', this.batchPdfPath = '', final List batchPdfPaths = const []}): _batchArchivePaths = batchArchivePaths,_imagePaths = imagePaths,_batchImagePaths = batchImagePaths,_batchPdfPaths = batchPdfPaths; + + +@override@JsonKey() final ParseFormType type; +@override@JsonKey() final String url; +@override@JsonKey() final String archivePath; +@override@JsonKey() final String batchArchivePath; + final List _batchArchivePaths; +@override@JsonKey() List get batchArchivePaths { + if (_batchArchivePaths is EqualUnmodifiableListView) return _batchArchivePaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_batchArchivePaths); +} + +@override@JsonKey() final String imageFolderPath; + final List _imagePaths; +@override@JsonKey() List get imagePaths { + if (_imagePaths is EqualUnmodifiableListView) return _imagePaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_imagePaths); +} + +@override@JsonKey() final String batchImageFolderPath; + final List _batchImagePaths; +@override@JsonKey() List get batchImagePaths { + if (_batchImagePaths is EqualUnmodifiableListView) return _batchImagePaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_batchImagePaths); +} + +@override@JsonKey() final String pdfPath; +@override@JsonKey() final String batchPdfPath; + final List _batchPdfPaths; +@override@JsonKey() List get batchPdfPaths { + if (_batchPdfPaths is EqualUnmodifiableListView) return _batchPdfPaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_batchPdfPaths); +} + + +/// Create a copy of ParseFormState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseFormStateCopyWith<_ParseFormState> get copyWith => __$ParseFormStateCopyWithImpl<_ParseFormState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseFormState&&(identical(other.type, type) || other.type == type)&&(identical(other.url, url) || other.url == url)&&(identical(other.archivePath, archivePath) || other.archivePath == archivePath)&&(identical(other.batchArchivePath, batchArchivePath) || other.batchArchivePath == batchArchivePath)&&const DeepCollectionEquality().equals(other._batchArchivePaths, _batchArchivePaths)&&(identical(other.imageFolderPath, imageFolderPath) || other.imageFolderPath == imageFolderPath)&&const DeepCollectionEquality().equals(other._imagePaths, _imagePaths)&&(identical(other.batchImageFolderPath, batchImageFolderPath) || other.batchImageFolderPath == batchImageFolderPath)&&const DeepCollectionEquality().equals(other._batchImagePaths, _batchImagePaths)&&(identical(other.pdfPath, pdfPath) || other.pdfPath == pdfPath)&&(identical(other.batchPdfPath, batchPdfPath) || other.batchPdfPath == batchPdfPath)&&const DeepCollectionEquality().equals(other._batchPdfPaths, _batchPdfPaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,type,url,archivePath,batchArchivePath,const DeepCollectionEquality().hash(_batchArchivePaths),imageFolderPath,const DeepCollectionEquality().hash(_imagePaths),batchImageFolderPath,const DeepCollectionEquality().hash(_batchImagePaths),pdfPath,batchPdfPath,const DeepCollectionEquality().hash(_batchPdfPaths)); + +@override +String toString() { + return 'ParseFormState(type: $type, url: $url, archivePath: $archivePath, batchArchivePath: $batchArchivePath, batchArchivePaths: $batchArchivePaths, imageFolderPath: $imageFolderPath, imagePaths: $imagePaths, batchImageFolderPath: $batchImageFolderPath, batchImagePaths: $batchImagePaths, pdfPath: $pdfPath, batchPdfPath: $batchPdfPath, batchPdfPaths: $batchPdfPaths)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseFormStateCopyWith<$Res> implements $ParseFormStateCopyWith<$Res> { + factory _$ParseFormStateCopyWith(_ParseFormState value, $Res Function(_ParseFormState) _then) = __$ParseFormStateCopyWithImpl; +@override @useResult +$Res call({ + ParseFormType type, String url, String archivePath, String batchArchivePath, List batchArchivePaths, String imageFolderPath, List imagePaths, String batchImageFolderPath, List batchImagePaths, String pdfPath, String batchPdfPath, List batchPdfPaths +}); + + + + +} +/// @nodoc +class __$ParseFormStateCopyWithImpl<$Res> + implements _$ParseFormStateCopyWith<$Res> { + __$ParseFormStateCopyWithImpl(this._self, this._then); + + final _ParseFormState _self; + final $Res Function(_ParseFormState) _then; + +/// Create a copy of ParseFormState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? url = null,Object? archivePath = null,Object? batchArchivePath = null,Object? batchArchivePaths = null,Object? imageFolderPath = null,Object? imagePaths = null,Object? batchImageFolderPath = null,Object? batchImagePaths = null,Object? pdfPath = null,Object? batchPdfPath = null,Object? batchPdfPaths = null,}) { + return _then(_ParseFormState( +type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as ParseFormType,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable +as String,archivePath: null == archivePath ? _self.archivePath : archivePath // ignore: cast_nullable_to_non_nullable +as String,batchArchivePath: null == batchArchivePath ? _self.batchArchivePath : batchArchivePath // ignore: cast_nullable_to_non_nullable +as String,batchArchivePaths: null == batchArchivePaths ? _self._batchArchivePaths : batchArchivePaths // ignore: cast_nullable_to_non_nullable +as List,imageFolderPath: null == imageFolderPath ? _self.imageFolderPath : imageFolderPath // ignore: cast_nullable_to_non_nullable +as String,imagePaths: null == imagePaths ? _self._imagePaths : imagePaths // ignore: cast_nullable_to_non_nullable +as List,batchImageFolderPath: null == batchImageFolderPath ? _self.batchImageFolderPath : batchImageFolderPath // ignore: cast_nullable_to_non_nullable +as String,batchImagePaths: null == batchImagePaths ? _self._batchImagePaths : batchImagePaths // ignore: cast_nullable_to_non_nullable +as List,pdfPath: null == pdfPath ? _self.pdfPath : pdfPath // ignore: cast_nullable_to_non_nullable +as String,batchPdfPath: null == batchPdfPath ? _self.batchPdfPath : batchPdfPath // ignore: cast_nullable_to_non_nullable +as String,batchPdfPaths: null == batchPdfPaths ? _self._batchPdfPaths : batchPdfPaths // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +// dart format on diff --git a/lib/feature/parse/ui/provider/parse_form_provider.g.dart b/lib/feature/parse/ui/provider/parse_form_provider.g.dart new file mode 100644 index 0000000..c02e94e --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_form_provider.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'parse_form_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ParseForm) +final parseFormProvider = ParseFormProvider._(); + +final class ParseFormProvider + extends $NotifierProvider { + ParseFormProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'parseFormProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$parseFormHash(); + + @$internal + @override + ParseForm create() => ParseForm(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ParseFormState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$parseFormHash() => r'd3c29d56619ed0e61eaf8817baf9a355a374555a'; + +abstract class _$ParseForm extends $Notifier { + ParseFormState build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + ParseFormState, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/feature/parse/ui/provider/parse_image_folder_provider.dart b/lib/feature/parse/ui/provider/parse_image_folder_provider.dart new file mode 100644 index 0000000..4df7528 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_image_folder_provider.dart @@ -0,0 +1,173 @@ +import 'dart:io'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod/legacy.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tele_book/feature/book/model/dto/save_as_book_dto.dart'; +import 'package:tele_book/feature/book/repository/book_repository.dart'; +import 'package:tele_book/feature/parse/service/parse_archive_service.dart'; + +part 'parse_image_folder_provider.freezed.dart'; + +part 'parse_image_folder_provider.g.dart'; + +@freezed +abstract class ParseImageFolderParam with _$ParseImageFolderParam { + const factory ParseImageFolderParam({ + String? folderPath, + List? imagePathsInput, + }) = _ParseImageFolderParam; +} + +@freezed +abstract class ParseImageFolderState with _$ParseImageFolderState { + const factory ParseImageFolderState({ + required String folderName, + required List imagePaths, + }) = _ParseImageFolderState; +} + +@freezed +abstract class ParseImageFolderSaveBookParam + with _$ParseImageFolderSaveBookParam { + const factory ParseImageFolderSaveBookParam({ + required String folderName, + required List imagePaths, + }) = _ParseImageFolderSaveBookParam; +} + +@freezed +abstract class ParseImageFolderSaveBookProgress + with _$ParseImageFolderSaveBookProgress { + const factory ParseImageFolderSaveBookProgress({ + @Default(SaveStep.generateCover) SaveStep step, + @Default(0) int current, + @Default(0) int total, + }) = _ParseImageFolderSaveBookProgress; + + const ParseImageFolderSaveBookProgress._(); + + String get stepText => switch (step) { + SaveStep.generateCover => '生成封面图...', + SaveStep.generatePreview => '生成预览图... ($current/$total)', + SaveStep.saveOriginal => '保存原图... ($current/$total)', + SaveStep.saveDatabase => '保存中...', + }; +} + +final parseImageFolderSaveBookProgressProvider = + StateProvider.family< + ParseImageFolderSaveBookProgress, + ParseImageFolderParam + >((_, __) => const ParseImageFolderSaveBookProgress()); + +@riverpod +class ParseImageFolder extends _$ParseImageFolder { + ParseArchiveService get _parseArchiveService => + ref.read(parseArchiveServiceProvider); + + @override + FutureOr build(ParseImageFolderParam param) async { + return await _parseImageFolder(param); + } + + String _resolveFolderName(ParseImageFolderParam param) { + final folderPath = param.folderPath; + final imagePathsInput = param.imagePathsInput; + if (folderPath != null && folderPath.isNotEmpty) { + return folderPath.split(RegExp(r'[\\/]')).last; + } + + if (imagePathsInput != null && imagePathsInput.isNotEmpty) { + final parts = imagePathsInput.first.split(RegExp(r'[\\/]')); + return parts.length > 1 ? parts[parts.length - 2] : '导入图片'; + } + + return '导入图片'; + } + + Future _requestStoragePermission() async { + if (!Platform.isAndroid) return true; + if (await Permission.manageExternalStorage.isGranted) return true; + final status = await Permission.manageExternalStorage.request(); + if (status.isGranted) return true; + + if (await Permission.storage.isGranted) return true; + final storageStatus = await Permission.storage.request(); + return storageStatus.isGranted; + } + + Future _parseImageFolder( + ParseImageFolderParam param, + ) async { + state = const AsyncLoading(); + + final hasPermission = await _requestStoragePermission(); + if (!hasPermission) { + throw Exception('需要存储权限才能读取图片文件夹'); + } + + final folderName = _resolveFolderName(param); + final result = + param.imagePathsInput != null && param.imagePathsInput!.isNotEmpty + ? await _parseArchiveService.parseImagePaths(param.imagePathsInput!) + : await _parseArchiveService.parseImageFolder(param.folderPath ?? ''); + + if (result.isError) { + throw Exception(result.error?.message); + } + + return ParseImageFolderState( + folderName: folderName, + imagePaths: result.data!, + ); + } +} + +@riverpod +class ParseImageFolderSaveBook extends _$ParseImageFolderSaveBook { + BookRepository get _bookRepository => ref.read(bookRepositoryProvider); + + @override + FutureOr build(ParseImageFolderParam param) => null; + + Future submit(ParseImageFolderSaveBookParam submitParam) async { + if (state.isLoading || submitParam.imagePaths.isEmpty) return; + + state = const AsyncLoading(); + ref + .read(parseImageFolderSaveBookProgressProvider(param).notifier) + .state = ParseImageFolderSaveBookProgress( + step: SaveStep.generateCover, + current: 0, + total: submitParam.imagePaths.length, + ); + + final result = await _bookRepository.saveAsBook( + SaveAsBookDto( + title: submitParam.folderName, + paths: submitParam.imagePaths, + ), + onStepProgress: (step, current, total) { + ref + .read(parseImageFolderSaveBookProgressProvider(param).notifier) + .state = ParseImageFolderSaveBookProgress( + step: step, + current: current, + total: total, + ); + }, + ); + + result.fold( + onSuccess: (_) { + state = const AsyncData(null); + }, + onError: (error) { + state = AsyncError(error.message, StackTrace.current); + }, + ); + } +} diff --git a/lib/feature/parse/ui/provider/parse_image_folder_provider.freezed.dart b/lib/feature/parse/ui/provider/parse_image_folder_provider.freezed.dart new file mode 100644 index 0000000..f2b0893 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_image_folder_provider.freezed.dart @@ -0,0 +1,1077 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'parse_image_folder_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ParseImageFolderParam { + + String? get folderPath; List? get imagePathsInput; +/// Create a copy of ParseImageFolderParam +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseImageFolderParamCopyWith get copyWith => _$ParseImageFolderParamCopyWithImpl(this as ParseImageFolderParam, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseImageFolderParam&&(identical(other.folderPath, folderPath) || other.folderPath == folderPath)&&const DeepCollectionEquality().equals(other.imagePathsInput, imagePathsInput)); +} + + +@override +int get hashCode => Object.hash(runtimeType,folderPath,const DeepCollectionEquality().hash(imagePathsInput)); + +@override +String toString() { + return 'ParseImageFolderParam(folderPath: $folderPath, imagePathsInput: $imagePathsInput)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseImageFolderParamCopyWith<$Res> { + factory $ParseImageFolderParamCopyWith(ParseImageFolderParam value, $Res Function(ParseImageFolderParam) _then) = _$ParseImageFolderParamCopyWithImpl; +@useResult +$Res call({ + String? folderPath, List? imagePathsInput +}); + + + + +} +/// @nodoc +class _$ParseImageFolderParamCopyWithImpl<$Res> + implements $ParseImageFolderParamCopyWith<$Res> { + _$ParseImageFolderParamCopyWithImpl(this._self, this._then); + + final ParseImageFolderParam _self; + final $Res Function(ParseImageFolderParam) _then; + +/// Create a copy of ParseImageFolderParam +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? folderPath = freezed,Object? imagePathsInput = freezed,}) { + return _then(_self.copyWith( +folderPath: freezed == folderPath ? _self.folderPath : folderPath // ignore: cast_nullable_to_non_nullable +as String?,imagePathsInput: freezed == imagePathsInput ? _self.imagePathsInput : imagePathsInput // ignore: cast_nullable_to_non_nullable +as List?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseImageFolderParam]. +extension ParseImageFolderParamPatterns on ParseImageFolderParam { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseImageFolderParam value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseImageFolderParam() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseImageFolderParam value) $default,){ +final _that = this; +switch (_that) { +case _ParseImageFolderParam(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseImageFolderParam value)? $default,){ +final _that = this; +switch (_that) { +case _ParseImageFolderParam() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String? folderPath, List? imagePathsInput)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseImageFolderParam() when $default != null: +return $default(_that.folderPath,_that.imagePathsInput);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String? folderPath, List? imagePathsInput) $default,) {final _that = this; +switch (_that) { +case _ParseImageFolderParam(): +return $default(_that.folderPath,_that.imagePathsInput);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? folderPath, List? imagePathsInput)? $default,) {final _that = this; +switch (_that) { +case _ParseImageFolderParam() when $default != null: +return $default(_that.folderPath,_that.imagePathsInput);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseImageFolderParam implements ParseImageFolderParam { + const _ParseImageFolderParam({this.folderPath, final List? imagePathsInput}): _imagePathsInput = imagePathsInput; + + +@override final String? folderPath; + final List? _imagePathsInput; +@override List? get imagePathsInput { + final value = _imagePathsInput; + if (value == null) return null; + if (_imagePathsInput is EqualUnmodifiableListView) return _imagePathsInput; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + + +/// Create a copy of ParseImageFolderParam +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseImageFolderParamCopyWith<_ParseImageFolderParam> get copyWith => __$ParseImageFolderParamCopyWithImpl<_ParseImageFolderParam>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseImageFolderParam&&(identical(other.folderPath, folderPath) || other.folderPath == folderPath)&&const DeepCollectionEquality().equals(other._imagePathsInput, _imagePathsInput)); +} + + +@override +int get hashCode => Object.hash(runtimeType,folderPath,const DeepCollectionEquality().hash(_imagePathsInput)); + +@override +String toString() { + return 'ParseImageFolderParam(folderPath: $folderPath, imagePathsInput: $imagePathsInput)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseImageFolderParamCopyWith<$Res> implements $ParseImageFolderParamCopyWith<$Res> { + factory _$ParseImageFolderParamCopyWith(_ParseImageFolderParam value, $Res Function(_ParseImageFolderParam) _then) = __$ParseImageFolderParamCopyWithImpl; +@override @useResult +$Res call({ + String? folderPath, List? imagePathsInput +}); + + + + +} +/// @nodoc +class __$ParseImageFolderParamCopyWithImpl<$Res> + implements _$ParseImageFolderParamCopyWith<$Res> { + __$ParseImageFolderParamCopyWithImpl(this._self, this._then); + + final _ParseImageFolderParam _self; + final $Res Function(_ParseImageFolderParam) _then; + +/// Create a copy of ParseImageFolderParam +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? folderPath = freezed,Object? imagePathsInput = freezed,}) { + return _then(_ParseImageFolderParam( +folderPath: freezed == folderPath ? _self.folderPath : folderPath // ignore: cast_nullable_to_non_nullable +as String?,imagePathsInput: freezed == imagePathsInput ? _self._imagePathsInput : imagePathsInput // ignore: cast_nullable_to_non_nullable +as List?, + )); +} + + +} + +/// @nodoc +mixin _$ParseImageFolderState { + + String get folderName; List get imagePaths; +/// Create a copy of ParseImageFolderState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseImageFolderStateCopyWith get copyWith => _$ParseImageFolderStateCopyWithImpl(this as ParseImageFolderState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseImageFolderState&&(identical(other.folderName, folderName) || other.folderName == folderName)&&const DeepCollectionEquality().equals(other.imagePaths, imagePaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,folderName,const DeepCollectionEquality().hash(imagePaths)); + +@override +String toString() { + return 'ParseImageFolderState(folderName: $folderName, imagePaths: $imagePaths)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseImageFolderStateCopyWith<$Res> { + factory $ParseImageFolderStateCopyWith(ParseImageFolderState value, $Res Function(ParseImageFolderState) _then) = _$ParseImageFolderStateCopyWithImpl; +@useResult +$Res call({ + String folderName, List imagePaths +}); + + + + +} +/// @nodoc +class _$ParseImageFolderStateCopyWithImpl<$Res> + implements $ParseImageFolderStateCopyWith<$Res> { + _$ParseImageFolderStateCopyWithImpl(this._self, this._then); + + final ParseImageFolderState _self; + final $Res Function(ParseImageFolderState) _then; + +/// Create a copy of ParseImageFolderState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? folderName = null,Object? imagePaths = null,}) { + return _then(_self.copyWith( +folderName: null == folderName ? _self.folderName : folderName // ignore: cast_nullable_to_non_nullable +as String,imagePaths: null == imagePaths ? _self.imagePaths : imagePaths // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseImageFolderState]. +extension ParseImageFolderStatePatterns on ParseImageFolderState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseImageFolderState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseImageFolderState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseImageFolderState value) $default,){ +final _that = this; +switch (_that) { +case _ParseImageFolderState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseImageFolderState value)? $default,){ +final _that = this; +switch (_that) { +case _ParseImageFolderState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String folderName, List imagePaths)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseImageFolderState() when $default != null: +return $default(_that.folderName,_that.imagePaths);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String folderName, List imagePaths) $default,) {final _that = this; +switch (_that) { +case _ParseImageFolderState(): +return $default(_that.folderName,_that.imagePaths);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String folderName, List imagePaths)? $default,) {final _that = this; +switch (_that) { +case _ParseImageFolderState() when $default != null: +return $default(_that.folderName,_that.imagePaths);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseImageFolderState implements ParseImageFolderState { + const _ParseImageFolderState({required this.folderName, required final List imagePaths}): _imagePaths = imagePaths; + + +@override final String folderName; + final List _imagePaths; +@override List get imagePaths { + if (_imagePaths is EqualUnmodifiableListView) return _imagePaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_imagePaths); +} + + +/// Create a copy of ParseImageFolderState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseImageFolderStateCopyWith<_ParseImageFolderState> get copyWith => __$ParseImageFolderStateCopyWithImpl<_ParseImageFolderState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseImageFolderState&&(identical(other.folderName, folderName) || other.folderName == folderName)&&const DeepCollectionEquality().equals(other._imagePaths, _imagePaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,folderName,const DeepCollectionEquality().hash(_imagePaths)); + +@override +String toString() { + return 'ParseImageFolderState(folderName: $folderName, imagePaths: $imagePaths)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseImageFolderStateCopyWith<$Res> implements $ParseImageFolderStateCopyWith<$Res> { + factory _$ParseImageFolderStateCopyWith(_ParseImageFolderState value, $Res Function(_ParseImageFolderState) _then) = __$ParseImageFolderStateCopyWithImpl; +@override @useResult +$Res call({ + String folderName, List imagePaths +}); + + + + +} +/// @nodoc +class __$ParseImageFolderStateCopyWithImpl<$Res> + implements _$ParseImageFolderStateCopyWith<$Res> { + __$ParseImageFolderStateCopyWithImpl(this._self, this._then); + + final _ParseImageFolderState _self; + final $Res Function(_ParseImageFolderState) _then; + +/// Create a copy of ParseImageFolderState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? folderName = null,Object? imagePaths = null,}) { + return _then(_ParseImageFolderState( +folderName: null == folderName ? _self.folderName : folderName // ignore: cast_nullable_to_non_nullable +as String,imagePaths: null == imagePaths ? _self._imagePaths : imagePaths // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc +mixin _$ParseImageFolderSaveBookParam { + + String get folderName; List get imagePaths; +/// Create a copy of ParseImageFolderSaveBookParam +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseImageFolderSaveBookParamCopyWith get copyWith => _$ParseImageFolderSaveBookParamCopyWithImpl(this as ParseImageFolderSaveBookParam, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseImageFolderSaveBookParam&&(identical(other.folderName, folderName) || other.folderName == folderName)&&const DeepCollectionEquality().equals(other.imagePaths, imagePaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,folderName,const DeepCollectionEquality().hash(imagePaths)); + +@override +String toString() { + return 'ParseImageFolderSaveBookParam(folderName: $folderName, imagePaths: $imagePaths)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseImageFolderSaveBookParamCopyWith<$Res> { + factory $ParseImageFolderSaveBookParamCopyWith(ParseImageFolderSaveBookParam value, $Res Function(ParseImageFolderSaveBookParam) _then) = _$ParseImageFolderSaveBookParamCopyWithImpl; +@useResult +$Res call({ + String folderName, List imagePaths +}); + + + + +} +/// @nodoc +class _$ParseImageFolderSaveBookParamCopyWithImpl<$Res> + implements $ParseImageFolderSaveBookParamCopyWith<$Res> { + _$ParseImageFolderSaveBookParamCopyWithImpl(this._self, this._then); + + final ParseImageFolderSaveBookParam _self; + final $Res Function(ParseImageFolderSaveBookParam) _then; + +/// Create a copy of ParseImageFolderSaveBookParam +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? folderName = null,Object? imagePaths = null,}) { + return _then(_self.copyWith( +folderName: null == folderName ? _self.folderName : folderName // ignore: cast_nullable_to_non_nullable +as String,imagePaths: null == imagePaths ? _self.imagePaths : imagePaths // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseImageFolderSaveBookParam]. +extension ParseImageFolderSaveBookParamPatterns on ParseImageFolderSaveBookParam { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseImageFolderSaveBookParam value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseImageFolderSaveBookParam() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseImageFolderSaveBookParam value) $default,){ +final _that = this; +switch (_that) { +case _ParseImageFolderSaveBookParam(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseImageFolderSaveBookParam value)? $default,){ +final _that = this; +switch (_that) { +case _ParseImageFolderSaveBookParam() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String folderName, List imagePaths)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseImageFolderSaveBookParam() when $default != null: +return $default(_that.folderName,_that.imagePaths);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String folderName, List imagePaths) $default,) {final _that = this; +switch (_that) { +case _ParseImageFolderSaveBookParam(): +return $default(_that.folderName,_that.imagePaths);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String folderName, List imagePaths)? $default,) {final _that = this; +switch (_that) { +case _ParseImageFolderSaveBookParam() when $default != null: +return $default(_that.folderName,_that.imagePaths);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseImageFolderSaveBookParam implements ParseImageFolderSaveBookParam { + const _ParseImageFolderSaveBookParam({required this.folderName, required final List imagePaths}): _imagePaths = imagePaths; + + +@override final String folderName; + final List _imagePaths; +@override List get imagePaths { + if (_imagePaths is EqualUnmodifiableListView) return _imagePaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_imagePaths); +} + + +/// Create a copy of ParseImageFolderSaveBookParam +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseImageFolderSaveBookParamCopyWith<_ParseImageFolderSaveBookParam> get copyWith => __$ParseImageFolderSaveBookParamCopyWithImpl<_ParseImageFolderSaveBookParam>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseImageFolderSaveBookParam&&(identical(other.folderName, folderName) || other.folderName == folderName)&&const DeepCollectionEquality().equals(other._imagePaths, _imagePaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,folderName,const DeepCollectionEquality().hash(_imagePaths)); + +@override +String toString() { + return 'ParseImageFolderSaveBookParam(folderName: $folderName, imagePaths: $imagePaths)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseImageFolderSaveBookParamCopyWith<$Res> implements $ParseImageFolderSaveBookParamCopyWith<$Res> { + factory _$ParseImageFolderSaveBookParamCopyWith(_ParseImageFolderSaveBookParam value, $Res Function(_ParseImageFolderSaveBookParam) _then) = __$ParseImageFolderSaveBookParamCopyWithImpl; +@override @useResult +$Res call({ + String folderName, List imagePaths +}); + + + + +} +/// @nodoc +class __$ParseImageFolderSaveBookParamCopyWithImpl<$Res> + implements _$ParseImageFolderSaveBookParamCopyWith<$Res> { + __$ParseImageFolderSaveBookParamCopyWithImpl(this._self, this._then); + + final _ParseImageFolderSaveBookParam _self; + final $Res Function(_ParseImageFolderSaveBookParam) _then; + +/// Create a copy of ParseImageFolderSaveBookParam +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? folderName = null,Object? imagePaths = null,}) { + return _then(_ParseImageFolderSaveBookParam( +folderName: null == folderName ? _self.folderName : folderName // ignore: cast_nullable_to_non_nullable +as String,imagePaths: null == imagePaths ? _self._imagePaths : imagePaths // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc +mixin _$ParseImageFolderSaveBookProgress { + + SaveStep get step; int get current; int get total; +/// Create a copy of ParseImageFolderSaveBookProgress +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseImageFolderSaveBookProgressCopyWith get copyWith => _$ParseImageFolderSaveBookProgressCopyWithImpl(this as ParseImageFolderSaveBookProgress, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseImageFolderSaveBookProgress&&(identical(other.step, step) || other.step == step)&&(identical(other.current, current) || other.current == current)&&(identical(other.total, total) || other.total == total)); +} + + +@override +int get hashCode => Object.hash(runtimeType,step,current,total); + +@override +String toString() { + return 'ParseImageFolderSaveBookProgress(step: $step, current: $current, total: $total)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseImageFolderSaveBookProgressCopyWith<$Res> { + factory $ParseImageFolderSaveBookProgressCopyWith(ParseImageFolderSaveBookProgress value, $Res Function(ParseImageFolderSaveBookProgress) _then) = _$ParseImageFolderSaveBookProgressCopyWithImpl; +@useResult +$Res call({ + SaveStep step, int current, int total +}); + + + + +} +/// @nodoc +class _$ParseImageFolderSaveBookProgressCopyWithImpl<$Res> + implements $ParseImageFolderSaveBookProgressCopyWith<$Res> { + _$ParseImageFolderSaveBookProgressCopyWithImpl(this._self, this._then); + + final ParseImageFolderSaveBookProgress _self; + final $Res Function(ParseImageFolderSaveBookProgress) _then; + +/// Create a copy of ParseImageFolderSaveBookProgress +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? step = null,Object? current = null,Object? total = null,}) { + return _then(_self.copyWith( +step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable +as SaveStep,current: null == current ? _self.current : current // ignore: cast_nullable_to_non_nullable +as int,total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable +as int, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseImageFolderSaveBookProgress]. +extension ParseImageFolderSaveBookProgressPatterns on ParseImageFolderSaveBookProgress { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseImageFolderSaveBookProgress value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseImageFolderSaveBookProgress() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseImageFolderSaveBookProgress value) $default,){ +final _that = this; +switch (_that) { +case _ParseImageFolderSaveBookProgress(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseImageFolderSaveBookProgress value)? $default,){ +final _that = this; +switch (_that) { +case _ParseImageFolderSaveBookProgress() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( SaveStep step, int current, int total)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseImageFolderSaveBookProgress() when $default != null: +return $default(_that.step,_that.current,_that.total);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( SaveStep step, int current, int total) $default,) {final _that = this; +switch (_that) { +case _ParseImageFolderSaveBookProgress(): +return $default(_that.step,_that.current,_that.total);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( SaveStep step, int current, int total)? $default,) {final _that = this; +switch (_that) { +case _ParseImageFolderSaveBookProgress() when $default != null: +return $default(_that.step,_that.current,_that.total);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseImageFolderSaveBookProgress extends ParseImageFolderSaveBookProgress { + const _ParseImageFolderSaveBookProgress({this.step = SaveStep.generateCover, this.current = 0, this.total = 0}): super._(); + + +@override@JsonKey() final SaveStep step; +@override@JsonKey() final int current; +@override@JsonKey() final int total; + +/// Create a copy of ParseImageFolderSaveBookProgress +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseImageFolderSaveBookProgressCopyWith<_ParseImageFolderSaveBookProgress> get copyWith => __$ParseImageFolderSaveBookProgressCopyWithImpl<_ParseImageFolderSaveBookProgress>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseImageFolderSaveBookProgress&&(identical(other.step, step) || other.step == step)&&(identical(other.current, current) || other.current == current)&&(identical(other.total, total) || other.total == total)); +} + + +@override +int get hashCode => Object.hash(runtimeType,step,current,total); + +@override +String toString() { + return 'ParseImageFolderSaveBookProgress(step: $step, current: $current, total: $total)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseImageFolderSaveBookProgressCopyWith<$Res> implements $ParseImageFolderSaveBookProgressCopyWith<$Res> { + factory _$ParseImageFolderSaveBookProgressCopyWith(_ParseImageFolderSaveBookProgress value, $Res Function(_ParseImageFolderSaveBookProgress) _then) = __$ParseImageFolderSaveBookProgressCopyWithImpl; +@override @useResult +$Res call({ + SaveStep step, int current, int total +}); + + + + +} +/// @nodoc +class __$ParseImageFolderSaveBookProgressCopyWithImpl<$Res> + implements _$ParseImageFolderSaveBookProgressCopyWith<$Res> { + __$ParseImageFolderSaveBookProgressCopyWithImpl(this._self, this._then); + + final _ParseImageFolderSaveBookProgress _self; + final $Res Function(_ParseImageFolderSaveBookProgress) _then; + +/// Create a copy of ParseImageFolderSaveBookProgress +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? step = null,Object? current = null,Object? total = null,}) { + return _then(_ParseImageFolderSaveBookProgress( +step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable +as SaveStep,current: null == current ? _self.current : current // ignore: cast_nullable_to_non_nullable +as int,total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + +// dart format on diff --git a/lib/feature/parse/ui/provider/parse_image_folder_provider.g.dart b/lib/feature/parse/ui/provider/parse_image_folder_provider.g.dart new file mode 100644 index 0000000..67b893c --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_image_folder_provider.g.dart @@ -0,0 +1,196 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'parse_image_folder_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ParseImageFolder) +final parseImageFolderProvider = ParseImageFolderFamily._(); + +final class ParseImageFolderProvider + extends $AsyncNotifierProvider { + ParseImageFolderProvider._({ + required ParseImageFolderFamily super.from, + required ParseImageFolderParam super.argument, + }) : super( + retry: null, + name: r'parseImageFolderProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$parseImageFolderHash(); + + @override + String toString() { + return r'parseImageFolderProvider' + '' + '($argument)'; + } + + @$internal + @override + ParseImageFolder create() => ParseImageFolder(); + + @override + bool operator ==(Object other) { + return other is ParseImageFolderProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$parseImageFolderHash() => r'c8d88c1656f7fc63a7b09d86de108d637ef3e6bf'; + +final class ParseImageFolderFamily extends $Family + with + $ClassFamilyOverride< + ParseImageFolder, + AsyncValue, + ParseImageFolderState, + FutureOr, + ParseImageFolderParam + > { + ParseImageFolderFamily._() + : super( + retry: null, + name: r'parseImageFolderProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + ParseImageFolderProvider call(ParseImageFolderParam param) => + ParseImageFolderProvider._(argument: param, from: this); + + @override + String toString() => r'parseImageFolderProvider'; +} + +abstract class _$ParseImageFolder + extends $AsyncNotifier { + late final _$args = ref.$arg as ParseImageFolderParam; + ParseImageFolderParam get param => _$args; + + FutureOr build(ParseImageFolderParam param); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref + as $Ref, ParseImageFolderState>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + AsyncValue, + ParseImageFolderState + >, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} + +@ProviderFor(ParseImageFolderSaveBook) +final parseImageFolderSaveBookProvider = ParseImageFolderSaveBookFamily._(); + +final class ParseImageFolderSaveBookProvider + extends $AsyncNotifierProvider { + ParseImageFolderSaveBookProvider._({ + required ParseImageFolderSaveBookFamily super.from, + required ParseImageFolderParam super.argument, + }) : super( + retry: null, + name: r'parseImageFolderSaveBookProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$parseImageFolderSaveBookHash(); + + @override + String toString() { + return r'parseImageFolderSaveBookProvider' + '' + '($argument)'; + } + + @$internal + @override + ParseImageFolderSaveBook create() => ParseImageFolderSaveBook(); + + @override + bool operator ==(Object other) { + return other is ParseImageFolderSaveBookProvider && + other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$parseImageFolderSaveBookHash() => + r'df6a4025c5d290269667cfd4e0ffb453ec0e4c1a'; + +final class ParseImageFolderSaveBookFamily extends $Family + with + $ClassFamilyOverride< + ParseImageFolderSaveBook, + AsyncValue, + void, + FutureOr, + ParseImageFolderParam + > { + ParseImageFolderSaveBookFamily._() + : super( + retry: null, + name: r'parseImageFolderSaveBookProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + ParseImageFolderSaveBookProvider call(ParseImageFolderParam param) => + ParseImageFolderSaveBookProvider._(argument: param, from: this); + + @override + String toString() => r'parseImageFolderSaveBookProvider'; +} + +abstract class _$ParseImageFolderSaveBook extends $AsyncNotifier { + late final _$args = ref.$arg as ParseImageFolderParam; + ParseImageFolderParam get param => _$args; + + FutureOr build(ParseImageFolderParam param); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} diff --git a/lib/feature/parse/ui/provider/parse_pdf_provider.dart b/lib/feature/parse/ui/provider/parse_pdf_provider.dart new file mode 100644 index 0000000..74cae94 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_pdf_provider.dart @@ -0,0 +1,144 @@ +import 'dart:io'; + +import 'package:dk_util/dk_util.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod/legacy.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tele_book/feature/book/model/dto/save_as_book_dto.dart'; +import 'package:tele_book/feature/book/repository/book_repository.dart'; +import 'package:tele_book/feature/parse/service/parse_pdf_service.dart'; + +part 'parse_pdf_provider.freezed.dart'; + +part 'parse_pdf_provider.g.dart'; + +@freezed +abstract class ParsePdfState with _$ParsePdfState { + const factory ParsePdfState({ + required String pdfName, + required List tempPaths, + }) = _ParsePdfProgressState; +} + +final parsePdfProgressProvider = + StateProvider.family<(int current, int total), String>( + (ref, pdfPath) => (0, 0), + ); + +@riverpod +class ParsePdf extends _$ParsePdf { + ParsePdfService get _service => ref.read(parsePdfServiceProvider); + + Future _requestStoragePermission() async { + if (!Platform.isAndroid) return true; + if (await Permission.manageExternalStorage.isGranted) return true; + final status = await Permission.manageExternalStorage.request(); + if (status.isGranted) return true; + if (await Permission.storage.isGranted) return true; + final storageStatus = await Permission.storage.request(); + return storageStatus.isGranted; + } + + @override + FutureOr build(String pdfPath) async { + final pdfName = pdfPath.split(RegExp(r'[\\/]')).last.replaceAll('.pdf', ''); + Future.microtask(() => _parsePdf(pdfPath)); + return ParsePdfState(tempPaths: const [], pdfName: pdfName); + } + + Future _parsePdf(String pdfPath) async { + final pdfName = pdfPath.split(RegExp(r'[\\/]')).last.replaceAll('.pdf', ''); + + ref.read(parsePdfProgressProvider(pdfPath).notifier).state = (0, 0); + + final hasPermission = await _requestStoragePermission(); + if (!ref.mounted) return; + if (!hasPermission) { + state = AsyncValue.error('需要存储权限才能解析 PDF', StackTrace.current); + return; + } + + state = AsyncValue.loading(); + final result = await _service.parsePdf( + pdfPath, + onProgress: (current, total) { + if (!ref.mounted) return; + ref.read(parsePdfProgressProvider(pdfPath).notifier).state = ( + current, + total, + ); + }, + ); + if (!ref.mounted) return; + result.fold( + onSuccess: (data) { + state = AsyncValue.data( + ParsePdfState(tempPaths: data, pdfName: pdfName), + ); + }, + onError: (error) { + state = AsyncValue.error(error.message, StackTrace.current); + }, + ); + } +} + +final parsePdfSaveBookProgressProvider = + StateProvider<(SaveStep step, int current, int total)>( + (_) => (SaveStep.generateCover, 0, 0), + ); + +String saveBookStepText((SaveStep step, int current, int total) progress) { + final (step, current, total) = progress; + return switch (step) { + SaveStep.generateCover => '生成封面图...', + SaveStep.generatePreview => '生成预览图... ($current/$total)', + SaveStep.saveOriginal => '保存原图... ($current/$total)', + SaveStep.saveDatabase => '保存中...', + }; +} + +@riverpod +class ParsePdfSaveBook extends _$ParsePdfSaveBook { + BookRepository get _repository => ref.read(bookRepositoryProvider); + + @override + FutureOr build() => null; + + + Future onSave(List tempPaths, String pdfName) async { + DKLog.i('开始保存解析结果为书籍,tempPaths: $tempPaths, pdfName: $pdfName'); + state = AsyncValue.loading(); + + ref.read(parsePdfSaveBookProgressProvider.notifier).state = ( + SaveStep.generateCover, + 0, + tempPaths.length, + ); + + DKLog.i('调用 repository.saveAsBook,开始文件复制和 DB 写入'); + final result = await _repository.saveAsBook( + SaveAsBookDto(title: pdfName, paths: tempPaths), + onStepProgress: (step, current, total) { + ref.read(parsePdfSaveBookProgressProvider.notifier).state = ( + step, + current, + total, + ); + }, + ); + + result.fold( + onSuccess: (_) { + DKLog.i('保存书籍成功'); + state = AsyncValue.data(null); + }, + onError: (e) { + DKLog.e('保存书籍失败,错误信息:${e.message}'); + state = AsyncValue.error(e.message, StackTrace.current); + }, + ); + } +} diff --git a/lib/feature/parse/ui/provider/parse_pdf_provider.freezed.dart b/lib/feature/parse/ui/provider/parse_pdf_provider.freezed.dart new file mode 100644 index 0000000..41bc883 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_pdf_provider.freezed.dart @@ -0,0 +1,280 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'parse_pdf_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ParsePdfState { + + String get pdfName; List get tempPaths; +/// Create a copy of ParsePdfState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParsePdfStateCopyWith get copyWith => _$ParsePdfStateCopyWithImpl(this as ParsePdfState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParsePdfState&&(identical(other.pdfName, pdfName) || other.pdfName == pdfName)&&const DeepCollectionEquality().equals(other.tempPaths, tempPaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,pdfName,const DeepCollectionEquality().hash(tempPaths)); + +@override +String toString() { + return 'ParsePdfState(pdfName: $pdfName, tempPaths: $tempPaths)'; +} + + +} + +/// @nodoc +abstract mixin class $ParsePdfStateCopyWith<$Res> { + factory $ParsePdfStateCopyWith(ParsePdfState value, $Res Function(ParsePdfState) _then) = _$ParsePdfStateCopyWithImpl; +@useResult +$Res call({ + String pdfName, List tempPaths +}); + + + + +} +/// @nodoc +class _$ParsePdfStateCopyWithImpl<$Res> + implements $ParsePdfStateCopyWith<$Res> { + _$ParsePdfStateCopyWithImpl(this._self, this._then); + + final ParsePdfState _self; + final $Res Function(ParsePdfState) _then; + +/// Create a copy of ParsePdfState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? pdfName = null,Object? tempPaths = null,}) { + return _then(_self.copyWith( +pdfName: null == pdfName ? _self.pdfName : pdfName // ignore: cast_nullable_to_non_nullable +as String,tempPaths: null == tempPaths ? _self.tempPaths : tempPaths // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParsePdfState]. +extension ParsePdfStatePatterns on ParsePdfState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParsePdfProgressState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParsePdfProgressState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParsePdfProgressState value) $default,){ +final _that = this; +switch (_that) { +case _ParsePdfProgressState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParsePdfProgressState value)? $default,){ +final _that = this; +switch (_that) { +case _ParsePdfProgressState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String pdfName, List tempPaths)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParsePdfProgressState() when $default != null: +return $default(_that.pdfName,_that.tempPaths);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String pdfName, List tempPaths) $default,) {final _that = this; +switch (_that) { +case _ParsePdfProgressState(): +return $default(_that.pdfName,_that.tempPaths);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String pdfName, List tempPaths)? $default,) {final _that = this; +switch (_that) { +case _ParsePdfProgressState() when $default != null: +return $default(_that.pdfName,_that.tempPaths);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParsePdfProgressState implements ParsePdfState { + const _ParsePdfProgressState({required this.pdfName, required final List tempPaths}): _tempPaths = tempPaths; + + +@override final String pdfName; + final List _tempPaths; +@override List get tempPaths { + if (_tempPaths is EqualUnmodifiableListView) return _tempPaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_tempPaths); +} + + +/// Create a copy of ParsePdfState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParsePdfProgressStateCopyWith<_ParsePdfProgressState> get copyWith => __$ParsePdfProgressStateCopyWithImpl<_ParsePdfProgressState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParsePdfProgressState&&(identical(other.pdfName, pdfName) || other.pdfName == pdfName)&&const DeepCollectionEquality().equals(other._tempPaths, _tempPaths)); +} + + +@override +int get hashCode => Object.hash(runtimeType,pdfName,const DeepCollectionEquality().hash(_tempPaths)); + +@override +String toString() { + return 'ParsePdfState(pdfName: $pdfName, tempPaths: $tempPaths)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParsePdfProgressStateCopyWith<$Res> implements $ParsePdfStateCopyWith<$Res> { + factory _$ParsePdfProgressStateCopyWith(_ParsePdfProgressState value, $Res Function(_ParsePdfProgressState) _then) = __$ParsePdfProgressStateCopyWithImpl; +@override @useResult +$Res call({ + String pdfName, List tempPaths +}); + + + + +} +/// @nodoc +class __$ParsePdfProgressStateCopyWithImpl<$Res> + implements _$ParsePdfProgressStateCopyWith<$Res> { + __$ParsePdfProgressStateCopyWithImpl(this._self, this._then); + + final _ParsePdfProgressState _self; + final $Res Function(_ParsePdfProgressState) _then; + +/// Create a copy of ParsePdfState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? pdfName = null,Object? tempPaths = null,}) { + return _then(_ParsePdfProgressState( +pdfName: null == pdfName ? _self.pdfName : pdfName // ignore: cast_nullable_to_non_nullable +as String,tempPaths: null == tempPaths ? _self._tempPaths : tempPaths // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +// dart format on diff --git a/lib/feature/parse/ui/provider/parse_pdf_provider.g.dart b/lib/feature/parse/ui/provider/parse_pdf_provider.g.dart new file mode 100644 index 0000000..14ffadb --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_pdf_provider.g.dart @@ -0,0 +1,143 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'parse_pdf_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ParsePdf) +final parsePdfProvider = ParsePdfFamily._(); + +final class ParsePdfProvider + extends $AsyncNotifierProvider { + ParsePdfProvider._({ + required ParsePdfFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'parsePdfProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$parsePdfHash(); + + @override + String toString() { + return r'parsePdfProvider' + '' + '($argument)'; + } + + @$internal + @override + ParsePdf create() => ParsePdf(); + + @override + bool operator ==(Object other) { + return other is ParsePdfProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$parsePdfHash() => r'e0b05363d6c097e4b8c3141968eb773d58e94b26'; + +final class ParsePdfFamily extends $Family + with + $ClassFamilyOverride< + ParsePdf, + AsyncValue, + ParsePdfState, + FutureOr, + String + > { + ParsePdfFamily._() + : super( + retry: null, + name: r'parsePdfProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + ParsePdfProvider call(String pdfPath) => + ParsePdfProvider._(argument: pdfPath, from: this); + + @override + String toString() => r'parsePdfProvider'; +} + +abstract class _$ParsePdf extends $AsyncNotifier { + late final _$args = ref.$arg as String; + String get pdfPath => _$args; + + FutureOr build(String pdfPath); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, ParsePdfState>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, ParsePdfState>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} + +@ProviderFor(ParsePdfSaveBook) +final parsePdfSaveBookProvider = ParsePdfSaveBookProvider._(); + +final class ParsePdfSaveBookProvider + extends $AsyncNotifierProvider { + ParsePdfSaveBookProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'parsePdfSaveBookProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$parsePdfSaveBookHash(); + + @$internal + @override + ParsePdfSaveBook create() => ParsePdfSaveBook(); +} + +String _$parsePdfSaveBookHash() => r'2bb199c158dc082cd536e8a9bc71eb25994461fc'; + +abstract class _$ParsePdfSaveBook extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/feature/parse/ui/provider/parse_web_provider.dart b/lib/feature/parse/ui/provider/parse_web_provider.dart new file mode 100644 index 0000000..dad98eb --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_web_provider.dart @@ -0,0 +1,83 @@ +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tele_book/feature/download/service/download_service.dart'; +import 'package:tele_book/feature/parse/service/parse_web_service.dart'; + +part 'parse_web_provider.g.dart'; + +part 'parse_web_provider.freezed.dart'; + +@freezed +abstract class ParseWebState with _$ParseWebState { + const factory ParseWebState({ + required String title, + required List urls, + required int progress, + }) = _ParseWebState; +} + +@riverpod +class ParseWeb extends _$ParseWeb { + ParseWebService get _parseWebService => ref.read(parseWebServiceProvider); + + DownloadService get _downloadService => ref.read(downloadServiceProvider); + + bool get isInit => _webViewController != null; + + InAppWebViewController? _webViewController; + + @override + ParseWebState build(String url) { + Future.microtask(() => _initialize(url)); + return const ParseWebState(title: '加载中...', urls: [], progress: 0); + } + + Future _initialize(String url) async { + if (!ref.mounted) return; + state = state.copyWith(progress: 0); + await Future.delayed(Duration.zero); + } + + void onLoadStart(InAppWebViewController controller) { + _webViewController = controller; + } + + void onTitleChanged(InAppWebViewController controller, String? title) { + state = state.copyWith(title: title ?? '未知标题'); + } + + Future onProgressChange( + InAppWebViewController controller, + int progress, + ) async { + _webViewController = controller; + final urls = await _parseWebService.extractImagesFromWebView( + onExtractImages: (js) async { + final result = await controller.evaluateJavascript(source: js); + return result?.toString(); + }, + ); + state = state.copyWith(urls: urls, progress: progress); + } + + Future parseWeb() async { + if (_webViewController == null) { + return; + } + + final urls = await _parseWebService.extractImagesFromWebView( + onExtractImages: (js) async { + final result = await _webViewController!.evaluateJavascript(source: js); + return result?.toString(); + }, + ); + state = state.copyWith(urls: urls); + } + + Future startDownload() async { + final current = state; + if (current.urls.isEmpty) return; + _downloadService.startDownload(current.urls, current.title); + } +} diff --git a/lib/feature/parse/ui/provider/parse_web_provider.freezed.dart b/lib/feature/parse/ui/provider/parse_web_provider.freezed.dart new file mode 100644 index 0000000..0ab9ec1 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_web_provider.freezed.dart @@ -0,0 +1,283 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'parse_web_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ParseWebState { + + String get title; List get urls; int get progress; +/// Create a copy of ParseWebState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParseWebStateCopyWith get copyWith => _$ParseWebStateCopyWithImpl(this as ParseWebState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParseWebState&&(identical(other.title, title) || other.title == title)&&const DeepCollectionEquality().equals(other.urls, urls)&&(identical(other.progress, progress) || other.progress == progress)); +} + + +@override +int get hashCode => Object.hash(runtimeType,title,const DeepCollectionEquality().hash(urls),progress); + +@override +String toString() { + return 'ParseWebState(title: $title, urls: $urls, progress: $progress)'; +} + + +} + +/// @nodoc +abstract mixin class $ParseWebStateCopyWith<$Res> { + factory $ParseWebStateCopyWith(ParseWebState value, $Res Function(ParseWebState) _then) = _$ParseWebStateCopyWithImpl; +@useResult +$Res call({ + String title, List urls, int progress +}); + + + + +} +/// @nodoc +class _$ParseWebStateCopyWithImpl<$Res> + implements $ParseWebStateCopyWith<$Res> { + _$ParseWebStateCopyWithImpl(this._self, this._then); + + final ParseWebState _self; + final $Res Function(ParseWebState) _then; + +/// Create a copy of ParseWebState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? title = null,Object? urls = null,Object? progress = null,}) { + return _then(_self.copyWith( +title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable +as String,urls: null == urls ? _self.urls : urls // ignore: cast_nullable_to_non_nullable +as List,progress: null == progress ? _self.progress : progress // ignore: cast_nullable_to_non_nullable +as int, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParseWebState]. +extension ParseWebStatePatterns on ParseWebState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParseWebState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParseWebState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParseWebState value) $default,){ +final _that = this; +switch (_that) { +case _ParseWebState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParseWebState value)? $default,){ +final _that = this; +switch (_that) { +case _ParseWebState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String title, List urls, int progress)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParseWebState() when $default != null: +return $default(_that.title,_that.urls,_that.progress);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String title, List urls, int progress) $default,) {final _that = this; +switch (_that) { +case _ParseWebState(): +return $default(_that.title,_that.urls,_that.progress);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String title, List urls, int progress)? $default,) {final _that = this; +switch (_that) { +case _ParseWebState() when $default != null: +return $default(_that.title,_that.urls,_that.progress);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ParseWebState implements ParseWebState { + const _ParseWebState({required this.title, required final List urls, required this.progress}): _urls = urls; + + +@override final String title; + final List _urls; +@override List get urls { + if (_urls is EqualUnmodifiableListView) return _urls; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_urls); +} + +@override final int progress; + +/// Create a copy of ParseWebState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParseWebStateCopyWith<_ParseWebState> get copyWith => __$ParseWebStateCopyWithImpl<_ParseWebState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParseWebState&&(identical(other.title, title) || other.title == title)&&const DeepCollectionEquality().equals(other._urls, _urls)&&(identical(other.progress, progress) || other.progress == progress)); +} + + +@override +int get hashCode => Object.hash(runtimeType,title,const DeepCollectionEquality().hash(_urls),progress); + +@override +String toString() { + return 'ParseWebState(title: $title, urls: $urls, progress: $progress)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParseWebStateCopyWith<$Res> implements $ParseWebStateCopyWith<$Res> { + factory _$ParseWebStateCopyWith(_ParseWebState value, $Res Function(_ParseWebState) _then) = __$ParseWebStateCopyWithImpl; +@override @useResult +$Res call({ + String title, List urls, int progress +}); + + + + +} +/// @nodoc +class __$ParseWebStateCopyWithImpl<$Res> + implements _$ParseWebStateCopyWith<$Res> { + __$ParseWebStateCopyWithImpl(this._self, this._then); + + final _ParseWebState _self; + final $Res Function(_ParseWebState) _then; + +/// Create a copy of ParseWebState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? title = null,Object? urls = null,Object? progress = null,}) { + return _then(_ParseWebState( +title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable +as String,urls: null == urls ? _self._urls : urls // ignore: cast_nullable_to_non_nullable +as List,progress: null == progress ? _self.progress : progress // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + +// dart format on diff --git a/lib/feature/parse/ui/provider/parse_web_provider.g.dart b/lib/feature/parse/ui/provider/parse_web_provider.g.dart new file mode 100644 index 0000000..373ce14 --- /dev/null +++ b/lib/feature/parse/ui/provider/parse_web_provider.g.dart @@ -0,0 +1,107 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'parse_web_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ParseWeb) +final parseWebProvider = ParseWebFamily._(); + +final class ParseWebProvider + extends $NotifierProvider { + ParseWebProvider._({ + required ParseWebFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'parseWebProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$parseWebHash(); + + @override + String toString() { + return r'parseWebProvider' + '' + '($argument)'; + } + + @$internal + @override + ParseWeb create() => ParseWeb(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ParseWebState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } + + @override + bool operator ==(Object other) { + return other is ParseWebProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$parseWebHash() => r'3f70fc70a1b6dbff9de825ecf6dd0f8d271c1a9a'; + +final class ParseWebFamily extends $Family + with + $ClassFamilyOverride< + ParseWeb, + ParseWebState, + ParseWebState, + ParseWebState, + String + > { + ParseWebFamily._() + : super( + retry: null, + name: r'parseWebProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + ParseWebProvider call(String url) => + ParseWebProvider._(argument: url, from: this); + + @override + String toString() => r'parseWebProvider'; +} + +abstract class _$ParseWeb extends $Notifier { + late final _$args = ref.$arg as String; + String get url => _$args; + + ParseWebState build(String url); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + ParseWebState, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} diff --git a/lib/feature/parse/ui/view/parse_archive_view.dart b/lib/feature/parse/ui/view/parse_archive_view.dart index c27d539..bcc3298 100644 --- a/lib/feature/parse/ui/view/parse_archive_view.dart +++ b/lib/feature/parse/ui/view/parse_archive_view.dart @@ -1,65 +1,175 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:tele_book/core/util/state_util.dart'; -import 'package:tele_book/feature/parse/ui/viewmodel/parse_archive_viewmodel.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tele_book/common/widget/error_widget.dart'; +import 'package:tele_book/core/route/app_route.dart'; +import 'package:tele_book/feature/parse/ui/provider/parse_archive_provider.dart'; -class ParseArchiveView extends StatelessWidget { +class ParseArchiveView extends ConsumerWidget { final String archivePath; const ParseArchiveView({super.key, required this.archivePath}); @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => ParseArchiveViewmodel( - archivePath: archivePath, - parseArchiveService: context.read(), - bookRepository: context.read(), - ), - child: _ParseArchiveContent(), + Widget build(BuildContext context, WidgetRef ref) { + final provider = parseArchiveProvider(archivePath); + final asyncState = ref.watch(provider); + final parseProgress = ref.watch(parseArchiveProgressProvider(archivePath)); + + final saveState = ref.watch(parseArchiveSaveBookProvider); + final saveNotifier = ref.watch(parseArchiveSaveBookProvider.notifier); + final saveProgress = ref.watch(parseArchiveSaveBookProgressProvider); + + ref.listen(parseArchiveSaveBookProvider, (previous, next) { + if (previous == null) return; + if (next.hasError) { + showFToast( + context: context, + title: Text("保存失败"), + description: Text(next.error.toString()), + ); + } else if (previous.isLoading && next.hasValue) { + showFToast(context: context, title: Text("保存成功")); + context.go(AppRoute.main); + } + }); + + return _ParseArchiveContent( + asyncState: asyncState, + parseProgress: parseProgress, + saveState: saveState, + saveProgress: saveProgress, + onSave: (data) { + saveNotifier.submit( + ParseArchiveSaveBookParam( + archiveName: data.archiveName, + tempPaths: data.tempPaths, + ), + ); + }, ); } } class _ParseArchiveContent extends StatelessWidget { + final AsyncValue asyncState; + final (int current, int total) parseProgress; + final AsyncValue saveState; + final ParseArchiveSaveBookProgress saveProgress; + final void Function(ParseArchiveState data) onSave; + + const _ParseArchiveContent({ + required this.asyncState, + required this.parseProgress, + required this.saveState, + required this.saveProgress, + required this.onSave, + }); + @override Widget build(BuildContext context) { - final vm = context.watch(); - return Scaffold( - appBar: AppBar(title: Text("解析压缩包")), - body: vm.parseState.when>( - success: (images) => GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - mainAxisSpacing: 4, - crossAxisSpacing: 4, + return FScaffold( + header: FHeader.nested( + title: const Text('解析压缩包'), + prefixes: [FHeaderAction.back(onPress: () => context.pop())], + ), + child: asyncState.when( + loading: () => Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const FCircularProgress(size: .xl), + const SizedBox(height: 12), + Text('正在解析 ${parseProgress.$1}/${parseProgress.$2}'), + ], ), - itemBuilder: (context, index) { - final image = images[index]; - return Image.file(File(image), fit: BoxFit.cover); - }, - itemCount: images.length, ), - ), - bottomNavigationBar: vm.parseState.isSuccess - ? Padding( - padding: EdgeInsets.all(16), - child: FilledButton( - onPressed: vm.saveToBookState.isLoading - ? null - : () => vm.saveToBook(context), - child: vm.saveToBookState.isLoading - ? SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : Text("保存到书架"), + error: (error, stack) => Center( + child: CustomErrorWidget( + errorMessage: error.toString(), + stackTrace: stack, + ), + ), + data: (state) => state.tempPaths.isEmpty + ? const Center(child: Text('未解析到图片')) + : Column( + children: [ + Expanded( + child: GridView.builder( + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 150, + mainAxisExtent: 200, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemBuilder: (context, index) { + final image = state.tempPaths[index]; + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Stack( + fit: StackFit.expand, + children: [ + Image.file( + File(image), + fit: BoxFit.cover, + cacheWidth: 300, + errorBuilder: (_, __, ___) => Container( + color: Colors.grey[200], + child: Icon( + Icons.broken_image, + color: Colors.grey[400], + ), + ), + ), + // 页码角标 + Positioned( + bottom: 4, + right: 4, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + ), + ), + ), + ), + ], + ), + ); + }, + itemCount: state.tempPaths.length, + ), + ), + asyncState.value?.tempPaths.isNotEmpty == true + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: FButton( + onPress: saveState.isLoading + ? null + : () => onSave(asyncState.value!), + child: saveState.isLoading + ? Text(saveProgress.stepText) + : const Text('保存到书架'), + ), + ) + : SizedBox.shrink(), + ], ), - ) - : null, + ), ); } } diff --git a/lib/feature/parse/ui/view/parse_batch_archive_view.dart b/lib/feature/parse/ui/view/parse_batch_archive_view.dart index dc84457..709334c 100644 --- a/lib/feature/parse/ui/view/parse_batch_archive_view.dart +++ b/lib/feature/parse/ui/view/parse_batch_archive_view.dart @@ -1,14 +1,18 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:provider/provider.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tele_book/common/widget/error_widget.dart'; +import 'package:tele_book/common/widget/f_sheet_content.dart'; +import 'package:tele_book/common/widget/f_text.dart'; import 'package:tele_book/common/widget/local_image_widget.dart'; -import 'package:tele_book/core/util/state_util.dart'; -import 'package:tele_book/feature/parse/model/parse_batch_archive_vo.dart'; -import 'package:tele_book/feature/parse/ui/viewmodel/parse_batch_archive_viewmodel.dart'; +import 'package:tele_book/core/route/app_route.dart'; +import 'package:tele_book/feature/parse/ui/provider/parse_batch_archive_provider.dart'; -class ParseBatchArchiveView extends StatelessWidget { +class ParseBatchArchiveView extends ConsumerStatefulWidget { final String? archiveDirPath; final List? archivePaths; @@ -19,119 +23,134 @@ class ParseBatchArchiveView extends StatelessWidget { }); @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => ParseBatchArchiveViewmodel( - archiveDirPath: archiveDirPath, - archivePaths: archivePaths, - parseArchiveService: context.read(), - bookRepository: context.read(), - ), - child: _ParseBatchArchiveContentView(), - ); - } + ConsumerState createState() => + _ParseBatchArchiveViewState(); } -class _ParseBatchArchiveContentView extends StatelessWidget { +class _ParseBatchArchiveViewState extends ConsumerState { + @override + void dispose() { + super.dispose(); + } + @override Widget build(BuildContext context) { - final viewmodel = context.watch(); - return Scaffold( - appBar: AppBar(title: const Text('批量解析'), elevation: 0), - body: viewmodel.parseBatchArchiveState.when>( - loading: () { - if (viewmodel.totalCount == 0 && viewmodel.completeCount == 0) { - return Center(child: CircularProgressIndicator()); + final provider = parseBatchArchiveProvider( + ParseBatchArchiveParam( + archiveDirPath: widget.archiveDirPath, + archivePaths: widget.archivePaths, + ), + ); + final asyncState = ref.watch(provider); + final parseProgress = ref.watch(parseBatchArchiveProgressProvider); + + ref.listen(parseBatchArchiveSaveBookProvider, (previous, next) { + if (previous == null) return; + if (next.hasError) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('保存失败:${next.error}'))); + } else if (previous.isLoading && next.hasValue) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('保存成功'))); + context.go(AppRoute.main); + } + }); + + return FScaffold( + header: FHeader.nested( + title: const Text('批量解析'), + prefixes: [FHeaderAction.back(onPress: () => context.pop())], + ), + child: asyncState.when( + loading: () => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FText.title( + context, + '正在处理:${parseProgress.completeCount}/${parseProgress.totalCount}', + ), + if (parseProgress.currentFileName.isNotEmpty) + FText.subTitle( + context, + '当前文件:${parseProgress.currentFileName}', + ), + if (parseProgress.currentFileProgressText.isNotEmpty) + FText.subTitle(context, parseProgress.currentFileProgressText), + ], + ), + ), + error: (error, stack) => Center( + child: CustomErrorWidget( + errorMessage: error.toString(), + stackTrace: stack, + ), + ), + data: (state) { + final data = state.parseBatchArchiveList; + if (data.isEmpty) { + return const Center(child: Text('暂无可解析内容')); } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + + return Builder( + builder: (innerContext) => FItemGroup( children: [ - CircularProgressIndicator(), - SizedBox(height: 8), - Text("正在处理:${viewmodel.completeCount}/${viewmodel.totalCount}"), - ], - ), - ); - }, - success: (data) { - return ListView.builder( - padding: EdgeInsets.all(16), - itemCount: data.length, - itemBuilder: (context, index) { - final archive = data[index]; - return Row( - children: [ - LocalImageWidget(imagePath: archive.tempPaths.first), - Expanded( - child: ListTile( - title: Text(archive.name), - subtitle: Text("图片数: ${archive.tempPaths.length}"), - onTap: () { - _buildArchiveImageList(context, archive.tempPaths); - }, + for (var archive in data) + .item( + title: Text(archive.name), + subtitle: Text('图片数: ${archive.tempPaths.length}'), + prefix: LocalImageWidget( + imagePath: archive.tempPaths.first, ), + suffix: const Icon(FLucideIcons.chevronRight), + onPress: () { + _buildImageList( + innerContext, + archive.name, + archive.tempPaths, + ); + }, ), - ], - ); - }, + ], + ), ); }, ), - bottomNavigationBar: viewmodel.parseBatchArchiveState.isSuccess - ? Padding( - padding: EdgeInsets.all(16), - child: FilledButton( - onPressed: viewmodel.saveBatchAsBookState.isLoading - ? null - : () => viewmodel.saveBatchAsBook(context), - child: viewmodel.saveBatchAsBookState.isLoading - ? Text( - "正在保存:${viewmodel.saveAsBookCount}/${viewmodel.parseBatchArchiveList.length}", - ) - : Text("保存到书架"), - ), - ) - : null, ); } - void _buildArchiveImageList(BuildContext context, List imageList) { - showModalBottomSheet( + void _buildImageList( + BuildContext context, + String title, + List imageList, + ) { + showFSheet( context: context, - isScrollControlled: true, // 允许高度超过半屏 - useSafeArea: true, - builder: (context) { - return DraggableScrollableSheet( - initialChildSize: 0.7, - minChildSize: 0.4, - maxChildSize: 1.0, - expand: false, - builder: (context, scrollController) { - return Column( + side: .btt, + mainAxisMaxRatio: null, + builder: (context) => DraggableScrollableSheet( + expand: false, + maxChildSize: 0.8, + builder: (context, scrollController) => ScrollConfiguration( + behavior: ScrollConfiguration.of( + context, + ).copyWith(dragDevices: {.touch, .mouse, .trackpad}), + child: FSheetContent( + side: .btt, + child: Column( + crossAxisAlignment: .start, + mainAxisSize: .min, children: [ - // 拖动把手 - Container( - margin: const EdgeInsets.symmetric(vertical: 8), - width: 40, - height: 4, - decoration: BoxDecoration( - color: Colors.grey[400], - borderRadius: BorderRadius.circular(2), - ), - ), - Text( - "包含图片(${imageList.length})", - style: Theme.of(context).textTheme.titleMedium, - ), + FSheetContent.title(context, title), + FSheetContent.subTitle(context, "包含图片(${imageList.length})"), const SizedBox(height: 8), - Expanded( child: MasonryGridView.count( controller: scrollController, crossAxisCount: 3, - // 列数 mainAxisSpacing: 4, crossAxisSpacing: 4, padding: const EdgeInsets.symmetric( @@ -141,19 +160,82 @@ class _ParseBatchArchiveContentView extends StatelessWidget { itemCount: imageList.length, itemBuilder: (context, index) { final path = imageList[index]; - // Image.file 会根据图片真实宽高自适应,形成瀑布流效果 return ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image.file(File(path), fit: BoxFit.cover), + borderRadius: BorderRadius.circular(8), + child: Stack( + children: [ + Image.file( + File(path), + fit: BoxFit.cover, + cacheWidth: 300, + ), + Positioned( + bottom: 4, + right: 4, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + ), + ), + ), + ), + ], + ), ); }, ), ), + Consumer( + builder: (context, ref, _) { + final provider = parseBatchArchiveProvider( + ParseBatchArchiveParam( + archiveDirPath: widget.archiveDirPath, + archivePaths: widget.archivePaths, + ), + ); + final asyncState = ref.watch(provider); + final saveState = ref.watch( + parseBatchArchiveSaveBookProvider, + ); + final saveProgress = ref.watch( + parseBatchArchiveSaveBookProgressProvider, + ); + final saveNotifier = ref.watch( + parseBatchArchiveSaveBookProvider.notifier, + ); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: FButton( + onPress: saveState.isLoading + ? null + : () => saveNotifier.saveBatchAsBook( + asyncState.value!.parseBatchArchiveList, + ), + child: saveState.isLoading + ? Text( + '正在保存:${saveProgress.current}/${saveProgress.total}', + ) + : const Text('保存到书架'), + ), + ); + }, + ), ], - ); - }, - ); - }, + ), + ), + ), + ), ); } } diff --git a/lib/feature/parse/ui/view/parse_batch_image_folder_view.dart b/lib/feature/parse/ui/view/parse_batch_image_folder_view.dart index 99eaac9..5aa1787 100644 --- a/lib/feature/parse/ui/view/parse_batch_image_folder_view.dart +++ b/lib/feature/parse/ui/view/parse_batch_image_folder_view.dart @@ -1,14 +1,18 @@ import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:provider/provider.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tele_book/common/widget/error_widget.dart'; +import 'package:tele_book/common/widget/f_sheet_content.dart'; import 'package:tele_book/common/widget/local_image_widget.dart'; -import 'package:tele_book/core/util/state_util.dart'; -import 'package:tele_book/feature/parse/model/parse_batch_archive_vo.dart'; -import 'package:tele_book/feature/parse/ui/viewmodel/parse_batch_image_folder_viewmodel.dart'; +import 'package:tele_book/core/route/app_route.dart'; +import 'package:tele_book/feature/parse/ui/provider/parse_batch_image_folder.dart'; -class ParseBatchImageFolderView extends StatelessWidget { +class ParseBatchImageFolderView extends ConsumerStatefulWidget { final String? parentDirPath; final List? imagePaths; @@ -19,113 +23,186 @@ class ParseBatchImageFolderView extends StatelessWidget { }); @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => ParseBatchImageFolderViewmodel( - parentDirPath: parentDirPath, - imagePaths: imagePaths, - parseArchiveService: context.read(), - bookRepository: context.read(), - ), - child: const _ParseBatchImageFolderContentView(), - ); - } + ConsumerState createState() => + _ParseBatchImageFolderViewState(); } -class _ParseBatchImageFolderContentView extends StatelessWidget { - const _ParseBatchImageFolderContentView(); +class _ParseBatchImageFolderViewState + extends ConsumerState { + @override + void dispose() { + super.dispose(); + } @override Widget build(BuildContext context) { - final viewmodel = context.watch(); - return Scaffold( - appBar: AppBar(title: const Text('批量解析文件夹'), elevation: 0), - body: viewmodel.parseBatchFolderState.when>( + final param = ParseBatchImageFolderParam( + parentDirPath: widget.parentDirPath, + imagePaths: widget.imagePaths, + ); + + final parseProvider = parseBatchImageFolderProvider(param); + final saveProvider = saveBatchAsBookProvider(param); + final parseAsync = ref.watch(parseProvider); + final parseNotifier = ref.read(parseProvider.notifier); + final saveState = ref.watch(saveProvider); + final saveNotifier = ref.read(saveProvider.notifier); + + ref.listen(saveBatchAsBookProvider(param).select((s) => s.submitState), ( + previous, + next, + ) { + if (previous == null) return; + if (next.hasError) { + showFToast( + context: context, + title: Text("保存失败"), + description: Text(e.toString()), + ); + } else if (previous.isLoading && + next.isLoading == false && + next.error == null) { + showFToast(context: context, title: Text("保存成功!")); + context.go(AppRoute.main); + } + }); + + return FScaffold( + header: FHeader.nested( + title: const Text('批量解析文件夹'), + prefixes: [FHeaderAction.back(onPress: () => context.pop())], + suffixes: [ + if (parseAsync.value?.isParsing == false) + FHeaderAction( + icon: const Icon(FLucideIcons.refreshCw), + onPress: () => parseNotifier.refresh(), + ), + ], + ), + child: parseAsync.when( + error: (error, stack) => Center( + child: CustomErrorWidget( + errorMessage: error.toString(), + stackTrace: stack, + ), + ), loading: () { - if (viewmodel.totalCount == 0 && viewmodel.completeCount == 0) { - return const Center(child: CircularProgressIndicator()); + final current = parseAsync.value; + if (current != null && current.totalCount > 0) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const FCircularProgress(size: .xl), + const SizedBox(height: 8), + Text("正在处理:${current.completeCount}/${current.totalCount}"), + if (current.currentFileName.isNotEmpty) + Text("当前文件:${current.currentFileName}"), + ], + ), + ); } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 8), - Text("正在处理:${viewmodel.completeCount}/${viewmodel.totalCount}"), - ], - ), - ); + return const Center(child: FCircularProgress(size: .xl)); }, - success: (data) { - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: data.length, - itemBuilder: (context, index) { - final folder = data[index]; - return Row( + data: (state) { + if (state.isParsing) { + if (state.totalCount == 0 && state.completeCount == 0) { + return const Center(child: FCircularProgress(size: .xl)); + } + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - LocalImageWidget(imagePath: folder.tempPaths.first), - Expanded( - child: ListTile( - title: Text(folder.name), - subtitle: Text("图片数: ${folder.tempPaths.length}"), - onTap: () { - _buildImageList(context, folder.tempPaths); - }, - ), - ), + const FCircularProgress(size: .xl), + const SizedBox(height: 8), + Text("正在处理:${state.completeCount}/${state.totalCount}"), + if (state.currentFileName.isNotEmpty) + Text("当前文件:${state.currentFileName}"), + if (state.currentFileProgressText.isNotEmpty) + Text(state.currentFileProgressText), ], - ); - }, + ), + ); + } + + if (state.parseBatchFolderList.isEmpty) { + return const Center(child: Text('未解析到可用图片文件夹')); + } + + return Column( + children: [ + Expanded( + child: FItemGroup( + children: [ + for (var folder in state.parseBatchFolderList) + .item( + title: Text(folder.name), + subtitle: Text("图片数: ${folder.tempPaths.length}"), + prefix: LocalImageWidget( + imagePath: folder.tempPaths.first, + ), + suffix: const Icon(FLucideIcons.chevronRight), + onPress: () { + _buildImageList( + context, + folder.name, + folder.tempPaths, + ); + }, + ), + ], + ), + ), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: FButton( + onPress: saveState.submitState.isLoading + ? null + : () => saveNotifier.submit( + parseAsync.value!.parseBatchFolderList, + ), + child: saveState.submitState.isLoading + ? Text( + "正在保存:${saveState.saveAsBookCount}/${saveState.totalCount}", + ) + : const Text("保存到书架"), + ), + ), + ], ); }, ), - bottomNavigationBar: viewmodel.parseBatchFolderState.isSuccess - ? Padding( - padding: const EdgeInsets.all(16), - child: FilledButton( - onPressed: viewmodel.saveBatchAsBookState.isLoading - ? null - : () => viewmodel.saveBatchAsBook(context), - child: viewmodel.saveBatchAsBookState.isLoading - ? Text( - "正在保存:${viewmodel.saveAsBookCount}/${viewmodel.parseBatchFolderList.length}", - ) - : const Text("保存到书架"), - ), - ) - : null, ); } - void _buildImageList(BuildContext context, List imageList) { - showModalBottomSheet( + void _buildImageList( + BuildContext context, + String title, + List imageList, + ) { + showFSheet( context: context, - isScrollControlled: true, - useSafeArea: true, - builder: (context) { - return DraggableScrollableSheet( - initialChildSize: 0.7, - minChildSize: 0.4, - maxChildSize: 1.0, - expand: false, - builder: (context, scrollController) { - return Column( + side: .btt, + mainAxisMaxRatio: null, + builder: (context) => DraggableScrollableSheet( + expand: false, + maxChildSize: 0.8, + builder: (context, scrollController) => ScrollConfiguration( + behavior: ScrollConfiguration.of( + context, + ).copyWith(dragDevices: {.touch, .mouse, .trackpad}), + child: FSheetContent( + side: .btt, + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, children: [ - Container( - margin: const EdgeInsets.symmetric(vertical: 8), - width: 40, - height: 4, - decoration: BoxDecoration( - color: Colors.grey[400], - borderRadius: BorderRadius.circular(2), - ), - ), - Text( - "包含图片(${imageList.length})", - style: Theme.of(context).textTheme.titleMedium, - ), + FSheetContent.title(context, title), + FSheetContent.subTitle(context, "包含图片(${imageList.length})"), + const SizedBox(height: 8), Expanded( child: MasonryGridView.count( @@ -141,18 +218,46 @@ class _ParseBatchImageFolderContentView extends StatelessWidget { itemBuilder: (context, index) { final path = imageList[index]; return ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image.file(File(path), fit: BoxFit.cover), + borderRadius: BorderRadius.circular(8), + child: Stack( + children: [ + Image.file( + File(path), + fit: BoxFit.cover, + cacheWidth: 300, + ), + Positioned( + bottom: 4, + right: 4, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + ), + ), + ), + ), + ], + ), ); }, ), ), ], - ); - }, - ); - }, + ), + ), + ), + ), ); } } - diff --git a/lib/feature/parse/ui/view/parse_batch_pdf_view.dart b/lib/feature/parse/ui/view/parse_batch_pdf_view.dart index b67a441..8745607 100644 --- a/lib/feature/parse/ui/view/parse_batch_pdf_view.dart +++ b/lib/feature/parse/ui/view/parse_batch_pdf_view.dart @@ -1,146 +1,213 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:provider/provider.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tele_book/common/widget/f_sheet_content.dart'; import 'package:tele_book/common/widget/local_image_widget.dart'; -import 'package:tele_book/core/util/state_util.dart'; import 'package:tele_book/feature/parse/model/parse_batch_archive_vo.dart'; -import 'package:tele_book/feature/parse/ui/viewmodel/parse_batch_pdf_viewmodel.dart'; -import 'dart:io'; +import 'package:tele_book/feature/parse/ui/provider/parse_batch_pdf_provider.dart'; -class ParseBatchPdfView extends StatelessWidget { +import '../../../../core/route/app_route.dart'; + +class ParseBatchPdfView extends ConsumerStatefulWidget { final String? pdfDirPath; final List? pdfPaths; - const ParseBatchPdfView({ - super.key, - this.pdfDirPath, - this.pdfPaths, - }); + const ParseBatchPdfView({super.key, this.pdfDirPath, this.pdfPaths}); @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => ParseBatchPdfViewmodel( - pdfDirPath: pdfDirPath, - pdfPaths: pdfPaths, - parsePdfService: context.read(), - bookRepository: context.read(), - ), - child: const _ParseBatchPdfContent(), + ConsumerState createState() => _ParseBatchPdfViewState(); +} + +class _ParseBatchPdfViewState extends ConsumerState { + late final ParseBatchPdfParam _param; + + @override + void initState() { + super.initState(); + _param = ParseBatchPdfParam( + pdfDirPath: widget.pdfDirPath ?? '', + pdfPaths: widget.pdfPaths, ); } -} -class _ParseBatchPdfContent extends StatelessWidget { - const _ParseBatchPdfContent(); + + @override + void dispose() { + super.dispose(); + } @override Widget build(BuildContext context) { - final vm = context.watch(); + final asyncState = ref.watch(parseBatchPdfProvider(_param)); + final parseProgress = ref.watch(parseBatchProgressProvider); + + final saveBatchBookState = ref.watch(parseBatchPdfSaveBookProvider); + final saveBatchBookNotifier = ref.watch( + parseBatchPdfSaveBookProvider.notifier, + ); + final saveBookProgress = ref.watch(parseBatchPdfSaveBookProgressProvider); + + ref.listen(parseBatchPdfSaveBookProvider, (previous, next) { + if (previous == null) return; + if (next.hasError) { + showFToast( + context: context, + title: Text("保存失败"), + description: Text(next.error.toString()), + ); + } else if (previous.isLoading && + next.isLoading == false && + next.error == null) { + showFToast(context: context, title: Text("保存成功!")); + context.go(AppRoute.main); + } + }); - return Scaffold( - appBar: AppBar(title: const Text('批量解析 PDF'), elevation: 0), - body: vm.parseBatchState.when>( - loading: () { - if (vm.totalCount == 0 && vm.completeCount == 0) { - return const Center(child: CircularProgressIndicator()); + return FScaffold( + header: FHeader.nested( + title: const Text('批量解析 PDF'), + prefixes: [FHeaderAction.back(onPress: () => context.pop())], + ), + child: asyncState.when( + loading: () => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const FCircularProgress(size: .xl), + const SizedBox(height: 8), + Text( + '正在处理:${parseProgress.completeCount} / ${parseProgress.totalCount}', + ), + if (parseProgress.currentFileName.isNotEmpty) + Text('当前文件:${parseProgress.currentFileName}'), + ], + ), + ), + error: (error, stack) => Center(child: Text(error.toString())), + data: (state) { + if (state.parseBatchList.isEmpty) { + return const Center(child: Text('暂无可解析内容')); } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 8), - Text('正在处理:${vm.completeCount} / ${vm.totalCount}'), - ], - ), + + return Column( + children: [ + Expanded( + child: FItemGroup( + children: [ + for (var item in state.parseBatchList) + .item( + title: Text(item.name), + subtitle: Text('页数:${item.tempPaths.length}'), + prefix: LocalImageWidget( + imagePath: item.tempPaths.first, + ), + suffix: const Icon(FLucideIcons.chevronRight), + onPress: () { + _showPagePreview(context, item); + }, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: FButton( + onPress: saveBatchBookState.isLoading == true + ? null + : () => saveBatchBookNotifier.saveBatchAsBook( + asyncState.value!.parseBatchList, + ), + child: saveBatchBookState.isLoading == true + ? Text( + '正在保存:${saveBookProgress.current} / ${saveBookProgress.total}', + ) + : const Text('保存到书架'), + ), + ), + ], ); }, - success: (data) => ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: data.length, - itemBuilder: (context, index) { - final item = data[index]; - return Row( - children: [ - LocalImageWidget(imagePath: item.tempPaths.first), - Expanded( - child: ListTile( - title: Text(item.name), - subtitle: Text('页数:${item.tempPaths.length}'), - onTap: () => _showPagePreview(context, item), - ), - ), - ], - ); - }, - ), ), - bottomNavigationBar: vm.parseBatchState.isSuccess - ? Padding( - padding: const EdgeInsets.all(16), - child: FilledButton( - onPressed: vm.saveBatchAsBookState.isLoading - ? null - : () => vm.saveBatchAsBook(context), - child: vm.saveBatchAsBookState.isLoading - ? Text( - '正在保存:${vm.saveAsBookCount} / ${vm.parseBatchList.length}', - ) - : const Text('保存到书架'), - ), - ) - : null, ); } void _showPagePreview(BuildContext context, ParseBatchArchiveVo item) { - showModalBottomSheet( + showFSheet( context: context, - isScrollControlled: true, - useSafeArea: true, + side: .btt, + mainAxisMaxRatio: null, builder: (context) => DraggableScrollableSheet( - initialChildSize: 0.7, - minChildSize: 0.4, - maxChildSize: 1.0, expand: false, - builder: (context, scrollController) => Column( - children: [ - Container( - margin: const EdgeInsets.symmetric(vertical: 8), - width: 40, - height: 4, - decoration: BoxDecoration( - color: Colors.grey[400], - borderRadius: BorderRadius.circular(2), - ), - ), - Text( - '${item.name}(${item.tempPaths.length} 页)', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - Expanded( - child: MasonryGridView.count( - controller: scrollController, - crossAxisCount: 3, - mainAxisSpacing: 4, - crossAxisSpacing: 4, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - itemCount: item.tempPaths.length, - itemBuilder: (context, index) => ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image.file( - File(item.tempPaths[index]), - fit: BoxFit.cover, + maxChildSize: 0.8, + builder: (context, scrollController) => ScrollConfiguration( + behavior: ScrollConfiguration.of( + context, + ).copyWith(dragDevices: {.touch, .mouse, .trackpad}), + child: FSheetContent( + side: .btt, + child: Column( + crossAxisAlignment: .start, + children: [ + FSheetContent.title(context, item.name), + FSheetContent.subTitle(context, "${item.tempPaths.length} 页"), + const SizedBox(height: 8), + Expanded( + child: MasonryGridView.count( + controller: scrollController, + crossAxisCount: 3, + mainAxisSpacing: 4, + crossAxisSpacing: 4, + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + itemCount: item.tempPaths.length, + itemBuilder: (context, index) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Stack( + children: [ + Image.file( + File(item.tempPaths[index]), + fit: BoxFit.cover, + cacheWidth: 300, + ), + Positioned( + bottom: 4, + right: 4, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + ), + ), + ), + ), + ], + ), + ); + }, ), ), - ), + ], ), - ], + ), ), ), ); } } - diff --git a/lib/feature/parse/ui/view/parse_form_view.dart b/lib/feature/parse/ui/view/parse_form_view.dart index 28f3921..d216f7e 100644 --- a/lib/feature/parse/ui/view/parse_form_view.dart +++ b/lib/feature/parse/ui/view/parse_form_view.dart @@ -1,237 +1,238 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:tele_book/feature/parse/ui/viewmodel/parse_form_viewmodel.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tele_book/feature/parse/ui/provider/parse_form_provider.dart'; -class ParseFormView extends StatelessWidget { +class ParseFormView extends ConsumerWidget { const ParseFormView({super.key}); @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (_) => ParseFormViewmodel(), - child: const _ParseFormContent(), - ); - } -} - -class _ParseFormContent extends StatefulWidget { - const _ParseFormContent(); - - @override - State<_ParseFormContent> createState() => _ParseFormContentState(); -} - -class _ParseFormContentState extends State<_ParseFormContent> { - @override - Widget build(BuildContext context) { - final vm = context.watch(); + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(parseFormProvider); + final notifier = ref.read(parseFormProvider.notifier); - return Scaffold( - appBar: AppBar(title: const Text("解析表单")), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - LayoutBuilder( - builder: (context, constraints) { - return DropdownMenu( - width: constraints.maxWidth, - initialSelection: vm.type, - decorationBuilder: (context, state) { - return const InputDecoration( - labelText: "选择解析来源", - prefixIcon: Icon(Icons.source), - border: OutlineInputBorder(), - ); - }, - menuStyle: MenuStyle( - padding: WidgetStateProperty.all(EdgeInsets.zero), - ), - dropdownMenuEntries: const [ - DropdownMenuEntry( - value: ParseFormType.web, - label: "网页", - leadingIcon: Icon(Icons.web), - ), - DropdownMenuEntry( - value: ParseFormType.archive, - label: "压缩包", - leadingIcon: Icon(Icons.archive), - ), - DropdownMenuEntry( - value: ParseFormType.batchArchive, - label: "批量压缩包", - leadingIcon: Icon(Icons.batch_prediction), - ), - DropdownMenuEntry( - value: ParseFormType.imageFolder, - label: "文件夹", - leadingIcon: Icon(Icons.photo_library), - ), - DropdownMenuEntry( - value: ParseFormType.batchImageFolder, - label: "批量文件夹", - leadingIcon: Icon(Icons.folder_copy), - ), - DropdownMenuEntry( - value: ParseFormType.pdf, - label: "PDF", - leadingIcon: Icon(Icons.picture_as_pdf), - ), - DropdownMenuEntry( - value: ParseFormType.batchPdf, - label: "批量 PDF", - leadingIcon: Icon(Icons.folder_special), - ), - ], - onSelected: (value) { - vm.setType(value); - }, - ); + return FScaffold( + header: FHeader.nested( + title: const Text("解析表单"), + prefixes: [ + FHeaderAction.back( + onPress: () { + context.pop(); + }, + ), + ], + ), + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .stretch, + children: [ + Text( + "导入书籍", + style: context.theme.typography.display.xl2.copyWith( + fontWeight: .w600, + color: context.theme.colors.foreground, + height: 1.5, + ), + ), + SizedBox(height: 2), + Text( + "请选择导入方式,然后输入导入网站/文件路径/文件夹路径", + style: context.theme.typography.body.sm.copyWith( + color: context.theme.colors.mutedForeground, + ), + ), + SizedBox(height: 16), + FSelect.rich( + control: FSelectControl.managed( + initial: ParseFormType.web, + onChange: (value) { + notifier.setType(value); }, ), - const SizedBox(height: 16), - _buildSubForm(context, vm.type), - const Spacer(), - FilledButton( - onPressed: () { - vm.onParse(context); + hint: "请选择导入方式", + label: Text("导入方式"), + format: (s) => s.description, + children: [ + .item( + value: ParseFormType.web, + title: Text("网页"), + prefix: Icon(Icons.web), + ), + .item( + value: ParseFormType.archive, + title: Text("压缩包"), + prefix: Icon(Icons.archive), + ), + .item( + value: ParseFormType.batchArchive, + title: Text("批量压缩包"), + prefix: Icon(Icons.batch_prediction), + ), + .item( + value: ParseFormType.imageFolder, + title: Text("文件夹"), + prefix: Icon(Icons.photo_library), + ), + .item( + value: ParseFormType.batchImageFolder, + title: Text("批量文件夹"), + prefix: Icon(Icons.folder_copy), + ), + .item( + value: ParseFormType.pdf, + title: Text("PDF"), + prefix: Icon(Icons.picture_as_pdf), + ), + .item( + value: ParseFormType.batchPdf, + title: Text("批量 PDF"), + prefix: Icon(Icons.folder_special), + ), + ], + ), + const SizedBox(height: 16), + _buildSubForm(context, notifier, state.type), + const SizedBox(height: 16), + Spacer(), + Padding( + padding: .symmetric(vertical: 16), + child: FButton( + onPress: () { + notifier.onParse(context); }, child: const Text("解析"), ), - const SizedBox(height: 16), - ], - ), + ), + ], ), ); } - Widget _buildSubForm(BuildContext context, ParseFormType type) { - final vm = context.read(); + Widget _buildSubForm( + BuildContext context, + ParseForm notifier, + ParseFormType type, + ) { switch (type) { case ParseFormType.web: - return _buildWebForm(context, vm); + return _buildWebForm(context, notifier); case ParseFormType.archive: - return _buildArchiveForm(context, vm); + return _buildArchiveForm(context, notifier); case ParseFormType.batchArchive: - return _buildBatchArchiveForm(context, vm); + return _buildBatchArchiveForm(context, notifier); case ParseFormType.imageFolder: - return _buildImageFolderForm(context, vm); + return _buildImageFolderForm(context, notifier); case ParseFormType.batchImageFolder: - return _buildBatchImageFolderForm(context, vm); + return _buildBatchImageFolderForm(context, notifier); case ParseFormType.pdf: - return _buildPdfForm(context, vm); + return _buildPdfForm(context, notifier); case ParseFormType.batchPdf: - return _buildBatchPdfForm(context, vm); + return _buildBatchPdfForm(context, notifier); } } - Widget _buildWebForm(BuildContext context, ParseFormViewmodel vm) { - return TextField( - controller: vm.urlController, - decoration: InputDecoration( - labelText: "输入文本", - prefixIcon: const Icon(Icons.web), - suffixIcon: IconButton( - onPressed: () => vm.getClipboardUrl(), - icon: const Icon(Icons.paste), - ), - border: const OutlineInputBorder(), + Widget _buildWebForm(BuildContext context, ParseForm notifier) { + return FTextFormField( + label: const Text('URL'), + hint: '请输入URL', + control: FTextFieldControl.managed(controller: notifier.urlController), + suffixBuilder: (context, style, variants) => FButton.icon( + style: style.obscureButtonStyle, + onPress: notifier.getClipboardUrl, + child: const Icon(FLucideIcons.clipboardPaste), ), ); } - Widget _buildArchiveForm(BuildContext context, ParseFormViewmodel vm) { - return TextField( - controller: vm.archivePathController, - decoration: InputDecoration( - labelText: "请选择压缩包文件", - prefixIcon: const Icon(Icons.archive), - suffixIcon: IconButton( - onPressed: () => vm.pickerArchive(context), - icon: const Icon(Icons.folder_open), - ), - border: const OutlineInputBorder(), + Widget _buildArchiveForm(BuildContext context, ParseForm notifier) { + return FTextFormField( + label: const Text('压缩包文件'), + hint: '请选择压缩包文件', + control: FTextFieldControl.managed( + controller: notifier.archivePathController, + ), + suffixBuilder: (context, style, variants) => FButton.icon( + style: style.obscureButtonStyle, + onPress: notifier.pickerArchive, + child: const Icon(FLucideIcons.folderOpen), ), ); } - Widget _buildBatchArchiveForm(BuildContext context, ParseFormViewmodel vm) { - return TextField( - controller: vm.batchArchivePathController, - decoration: InputDecoration( - labelText: Platform.isIOS ? "请选择一个或多个 ZIP 文件" : "请选择压缩包文件夹", - prefixIcon: const Icon(Icons.folder), - suffixIcon: IconButton( - onPressed: () => vm.pickerBatchArchive(context), - icon: const Icon(Icons.folder_open), - ), - border: const OutlineInputBorder(), + Widget _buildBatchArchiveForm(BuildContext context, ParseForm notifier) { + return FTextFormField( + label: const Text('批量压缩包'), + hint: Platform.isIOS ? '请选择一个或多个 ZIP 文件' : '请选择压缩包文件夹', + control: FTextFieldControl.managed( + controller: notifier.batchArchivePathController, + ), + suffixBuilder: (context, style, variants) => FButton.icon( + style: style.obscureButtonStyle, + onPress: notifier.pickerBatchArchive, + child: const Icon(FLucideIcons.folderOpen), ), ); } - Widget _buildImageFolderForm(BuildContext context, ParseFormViewmodel vm) { - return TextField( - controller: vm.imageFolderPathController, - decoration: InputDecoration( - labelText: Platform.isIOS ? "请选择一个或多个图片文件" : "请选择图片文件夹", - prefixIcon: const Icon(Icons.photo_library), - suffixIcon: IconButton( - onPressed: () => vm.pickerImageFolder(context), - icon: const Icon(Icons.folder_open), - ), - border: const OutlineInputBorder(), + Widget _buildImageFolderForm(BuildContext context, ParseForm notifier) { + return FTextFormField( + label: const Text('图片路径'), + hint: Platform.isIOS ? '请选择一个或多个图片文件' : '请选择图片文件夹', + control: FTextFieldControl.managed( + controller: notifier.imageFolderPathController, + ), + suffixBuilder: (context, style, variants) => FButton.icon( + style: style.obscureButtonStyle, + onPress: notifier.pickerImageFolder, + child: const Icon(FLucideIcons.folderOpen), ), ); } - Widget _buildBatchImageFolderForm(BuildContext context, ParseFormViewmodel vm) { - return TextField( - controller: vm.batchImageFolderPathController, - decoration: InputDecoration( - labelText: Platform.isIOS - ? "请选择多个图片文件(按文件夹分组)" - : "请选择批量图片文件夹父目录", - prefixIcon: const Icon(Icons.folder_copy), - suffixIcon: IconButton( - onPressed: () => vm.pickerBatchImageFolder(context), - icon: const Icon(Icons.folder_open), - ), - border: const OutlineInputBorder(), + Widget _buildBatchImageFolderForm(BuildContext context, ParseForm notifier) { + return FTextFormField( + label: const Text('批量图片路径'), + hint: Platform.isIOS ? '请选择多个图片文件(按文件夹分组)' : '请选择批量图片文件夹父目录', + control: FTextFieldControl.managed( + controller: notifier.batchImageFolderPathController, + ), + suffixBuilder: (context, style, variants) => FButton.icon( + style: style.obscureButtonStyle, + onPress: notifier.pickerBatchImageFolder, + child: const Icon(FLucideIcons.folderOpen), ), ); } - Widget _buildPdfForm(BuildContext context, ParseFormViewmodel vm) { - return TextField( - controller: vm.pdfPathController, - decoration: InputDecoration( - labelText: "请选择 PDF 文件", - prefixIcon: const Icon(Icons.picture_as_pdf), - suffixIcon: IconButton( - onPressed: () => vm.pickerPdf(context), - icon: const Icon(Icons.folder_open), - ), - border: const OutlineInputBorder(), + Widget _buildPdfForm(BuildContext context, ParseForm notifier) { + return FTextFormField( + label: const Text('PDF 文件'), + hint: '请选择 PDF 文件', + control: FTextFieldControl.managed( + controller: notifier.pdfPathController, + ), + suffixBuilder: (context, style, variants) => FButton.icon( + style: style.obscureButtonStyle, + onPress: notifier.pickerPdf, + child: const Icon(FLucideIcons.folderOpen), ), ); } - Widget _buildBatchPdfForm(BuildContext context, ParseFormViewmodel vm) { - return TextField( - controller: vm.batchPdfPathController, - decoration: InputDecoration( - labelText: Platform.isIOS ? "请选择一个或多个 PDF 文件" : "请选择包含 PDF 的文件夹", - prefixIcon: const Icon(Icons.folder_special), - suffixIcon: IconButton( - onPressed: () => vm.pickerBatchPdf(context), - icon: const Icon(Icons.folder_open), - ), - border: const OutlineInputBorder(), + Widget _buildBatchPdfForm(BuildContext context, ParseForm notifier) { + return FTextFormField( + label: const Text('批量 PDF'), + hint: Platform.isIOS ? '请选择一个或多个 PDF 文件' : '请选择包含 PDF 的文件夹', + control: FTextFieldControl.managed( + controller: notifier.batchPdfPathController, + ), + suffixBuilder: (context, style, variants) => FButton.icon( + style: style.obscureButtonStyle, + onPress: notifier.pickerBatchPdf, + child: const Icon(FLucideIcons.folderOpen), ), ); } diff --git a/lib/feature/parse/ui/view/parse_image_folder_view.dart b/lib/feature/parse/ui/view/parse_image_folder_view.dart index 6d33575..f87f681 100644 --- a/lib/feature/parse/ui/view/parse_image_folder_view.dart +++ b/lib/feature/parse/ui/view/parse_image_folder_view.dart @@ -1,74 +1,170 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:tele_book/core/util/state_util.dart'; -import 'package:tele_book/feature/parse/ui/viewmodel/parse_image_folder_viewmodel.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tele_book/common/widget/error_widget.dart'; +import 'package:tele_book/core/route/app_route.dart'; +import 'package:tele_book/feature/parse/ui/provider/parse_image_folder_provider.dart'; -class ParseImageFolderView extends StatelessWidget { +class ParseImageFolderView extends ConsumerWidget { final String? folderPath; final List? imagePaths; - const ParseImageFolderView({ - super.key, - this.folderPath, - this.imagePaths, - }); + const ParseImageFolderView({super.key, this.folderPath, this.imagePaths}); @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => ParseImageFolderViewmodel( - folderPath: folderPath, - imagePathsInput: imagePaths, - parseArchiveService: context.read(), - bookRepository: context.read(), - ), - child: const _ParseImageFolderContent(), + Widget build(BuildContext context, WidgetRef ref) { + final param = ParseImageFolderParam( + folderPath: folderPath, + imagePathsInput: imagePaths, + ); + final provider = parseImageFolderProvider(param); + final asyncState = ref.watch(provider); + + final saveState = ref.watch(parseImageFolderSaveBookProvider(param)); + final saveNotifier = ref.watch( + parseImageFolderSaveBookProvider(param).notifier, + ); + final saveProgress = ref.watch( + parseImageFolderSaveBookProgressProvider(param), + ); + + ref.listen(parseImageFolderSaveBookProvider(param), (previous, next) { + if (previous == null) return; + if (next.hasError) { + showFToast( + context: context, + title: Text("保存失败"), + description: Text(next.error.toString()), + ); + } else if (previous.isLoading && next.hasValue) { + showFToast(context: context, title: Text("保存成功")); + context.go(AppRoute.main); + } + }); + + return _ParseImageFolderContent( + asyncState: asyncState, + saveState: saveState, + saveProgress: saveProgress, + onSave: (data) { + saveNotifier.submit( + ParseImageFolderSaveBookParam( + folderName: data.folderName, + imagePaths: data.imagePaths, + ), + ); + }, ); } } class _ParseImageFolderContent extends StatelessWidget { - const _ParseImageFolderContent(); + final AsyncValue asyncState; + final AsyncValue saveState; + final ParseImageFolderSaveBookProgress saveProgress; + final void Function(ParseImageFolderState data) onSave; + + const _ParseImageFolderContent({ + required this.asyncState, + required this.saveState, + required this.saveProgress, + required this.onSave, + }); @override Widget build(BuildContext context) { - final vm = context.watch(); - return Scaffold( - appBar: AppBar(title: const Text("解析文件夹")), - body: vm.parseState.when>( - success: (images) => GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - mainAxisSpacing: 4, - crossAxisSpacing: 4, + return FScaffold( + header: FHeader.nested( + title: const Text('解析文件夹'), + prefixes: [FHeaderAction.back(onPress: () => context.pop())], + ), + child: asyncState.when( + loading: () => const Center(child: FCircularProgress(size: .xl)), + error: (error, stack) => Center( + child: CustomErrorWidget( + errorMessage: error.toString(), + stackTrace: stack, ), - itemBuilder: (context, index) { - final image = images[index]; - return Image.file(File(image), fit: BoxFit.cover); - }, - itemCount: images.length, ), - ), - bottomNavigationBar: vm.parseState.isSuccess - ? Padding( - padding: const EdgeInsets.all(16), - child: FilledButton( - onPressed: vm.saveToBookState.isLoading - ? null - : () => vm.saveToBook(context), - child: vm.saveToBookState.isLoading - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text("保存到书架"), + data: (state) => state.imagePaths.isEmpty + ? const Center(child: Text('未解析到图片')) + : Column( + children: [ + Expanded( + child: GridView.builder( + padding: const EdgeInsets.all(12), + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 150, + mainAxisExtent: 200, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemCount: state.imagePaths.length, + itemBuilder: (context, index) { + final image = state.imagePaths[index]; + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Stack( + fit: StackFit.expand, + children: [ + Image.file( + File(image), + fit: BoxFit.cover, + cacheWidth: 300, + errorBuilder: (_, __, ___) => Container( + color: Colors.grey[200], + child: Icon( + Icons.broken_image, + color: Colors.grey[400], + ), + ), + ), + // 页码角标 + Positioned( + bottom: 4, + right: 4, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: FButton( + onPress: saveState.isLoading + ? null + : () => onSave(asyncState.value!), + child: saveState.isLoading + ? Text(saveProgress.stepText) + : const Text("保存到书架"), + ), + ), + ], ), - ) - : null, + ), ); } } - diff --git a/lib/feature/parse/ui/view/parse_pdf_view.dart b/lib/feature/parse/ui/view/parse_pdf_view.dart index b5a068e..1ac9731 100644 --- a/lib/feature/parse/ui/view/parse_pdf_view.dart +++ b/lib/feature/parse/ui/view/parse_pdf_view.dart @@ -1,78 +1,137 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:tele_book/core/util/state_util.dart'; -import 'package:tele_book/feature/parse/ui/viewmodel/parse_pdf_viewmodel.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tele_book/core/route/app_route.dart'; +import 'package:tele_book/feature/parse/ui/provider/parse_pdf_provider.dart'; -class ParsePdfView extends StatelessWidget { +class ParsePdfView extends ConsumerWidget { final String pdfPath; const ParsePdfView({super.key, required this.pdfPath}); @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => ParsePdfViewmodel( - pdfPath: pdfPath, - parsePdfService: context.read(), - bookRepository: context.read(), - ), - child: const _ParsePdfContent(), - ); - } -} + Widget build(BuildContext context, WidgetRef ref) { + final asyncState = ref.watch(parsePdfProvider(pdfPath)); + final progress = ref.watch(parsePdfProgressProvider(pdfPath)); -class _ParsePdfContent extends StatelessWidget { - const _ParsePdfContent(); + final saveBookProgress = ref.watch(parsePdfSaveBookProgressProvider); + final saveBookState = ref.watch(parsePdfSaveBookProvider); - @override - Widget build(BuildContext context) { - final vm = context.watch(); + ref.listen(parsePdfSaveBookProvider, (previous, next) { + if (previous == null) return; + + if (next.hasError && !next.isLoading) { + showFToast( + context: context, + title: Text("保存失败"), + description: Text(next.error.toString()), + ); + } - return Scaffold( - appBar: AppBar(title: Text('解析 PDF:${vm.pdfName}')), - body: vm.parseState.when>( + if (previous.isLoading && next.hasValue) { + showFToast(context: context, title: Text('保存成功')); + context.go(AppRoute.main); + } + }); + + return FScaffold( + header: FHeader.nested( + title: Text('解析 PDF:${asyncState.value?.pdfName ?? ''}'), + prefixes: [FHeaderAction.back(onPress: () => context.pop())], + ), + child: asyncState.when( loading: () => Center( child: Column( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ - const CircularProgressIndicator(), - const SizedBox(height: 12), - if (vm.totalPages > 0) - Text('正在渲染:${vm.currentPage} / ${vm.totalPages} 页'), + const FCircularProgress(size: .xl), + const SizedBox(height: 16), + Text('正在解析 ${progress.$1}/${progress.$2}'), ], ), ), - success: (images) => GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - mainAxisSpacing: 4, - crossAxisSpacing: 4, - ), - itemCount: images.length, - itemBuilder: (context, index) => - Image.file(File(images[index]), fit: BoxFit.cover), - ), - ), - bottomNavigationBar: vm.parseState.isSuccess - ? Padding( + error: (error, stack) => Center(child: Text(error.toString())), + data: (state) => Column( + children: [ + Expanded( + child: GridView.builder( + padding: const EdgeInsets.all(12), + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 150, + mainAxisExtent: 200, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemCount: state.tempPaths.length, + itemBuilder: (context, index) { + final image = state.tempPaths[index]; + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Stack( + fit: StackFit.expand, + children: [ + Image.file( + File(image), + fit: BoxFit.cover, + cacheWidth: 300, + errorBuilder: (_, __, ___) => Container( + color: Colors.grey[200], + child: Icon( + Icons.broken_image, + color: Colors.grey[400], + ), + ), + ), + // 页码角标 + Positioned( + bottom: 4, + right: 4, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + Padding( padding: const EdgeInsets.all(16), - child: FilledButton( - onPressed: vm.saveToBookState.isLoading + child: FButton( + onPress: saveBookState.isLoading ? null - : () => vm.saveToBook(context), - child: vm.saveToBookState.isLoading - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) + : () => ref + .read(parsePdfSaveBookProvider.notifier) + .onSave( + asyncState.value!.tempPaths, + asyncState.value!.pdfName, + ), + child: saveBookState.isLoading + ? Text(saveBookStepText(saveBookProgress)) : const Text('保存到书架'), ), - ) - : null, + ), + ], + ), + ), ); } } - diff --git a/lib/feature/parse/ui/view/parse_web_view.dart b/lib/feature/parse/ui/view/parse_web_view.dart index a133b93..9424a9b 100644 --- a/lib/feature/parse/ui/view/parse_web_view.dart +++ b/lib/feature/parse/ui/view/parse_web_view.dart @@ -1,67 +1,68 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; +import 'package:go_router/go_router.dart'; import 'package:tele_book/common/widget/network_image_widget.dart'; -import 'package:tele_book/feature/parse/ui/viewmodel/parse_web_viewmodel.dart'; +import 'package:tele_book/core/route/app_route.dart'; +import 'package:tele_book/feature/main/provider/main_provider.dart'; +import 'package:tele_book/feature/parse/ui/provider/parse_web_provider.dart'; -class ParseWebView extends StatelessWidget { +class ParseWebView extends ConsumerStatefulWidget { final String url; const ParseWebView({super.key, required this.url}); @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => ParseWebViewmodel( - context.read(), - context.read(), - ), - child: _ParseWebContent(url: url), - ); - } + ConsumerState createState() => _ParseWebViewState(); } -class _ParseWebContent extends StatefulWidget { - final String url; - - const _ParseWebContent({super.key, required this.url}); - +class _ParseWebViewState extends ConsumerState { @override - State<_ParseWebContent> createState() => __ParseWebContentState(); -} + void dispose() { + super.dispose(); + } -class __ParseWebContentState extends State<_ParseWebContent> { @override Widget build(BuildContext context) { - final vm = context.watch(); - return Scaffold( - appBar: AppBar(title: const Text("解析网页")), - floatingActionButton: FloatingActionButton( - child: Badge( - label: Text(vm.urls.length.toString()), - child: Icon(Icons.photo), - ), - onPressed: () { - showModalBottomSheet( - context: context, - builder: (context) => _buildBottomSheet(context, vm), - ); - }, + final state = ref.watch(parseWebProvider(widget.url)); + final notifier = ref.read(parseWebProvider(widget.url).notifier); + + return FScaffold( + header: FHeader.nested( + title: Text(state.title), + prefixes: [FHeaderAction.back(onPress: () => context.pop())], + suffixes: [ + Builder( + builder: (innerContext) => FHeaderAction( + icon: FBadge(child: Text('${state.urls.length}')), + onPress: () { + _showBottomSheet( + context: innerContext, + url: widget.url, + ); + }, + ), + ), + ], ), - body: Column( + child: Column( children: [ - LinearProgressIndicator(value: vm.progress / 100), + FDeterminateProgress(value: (state.progress) / 100), Expanded( child: InAppWebView( initialUrlRequest: URLRequest(url: WebUri(widget.url)), onLoadStart: (controller, url) { - vm.onLoadStart(controller); + notifier.onLoadStart(controller); }, onTitleChanged: (controller, title) { - vm.onTitleChanged(controller, title); + notifier.onTitleChanged(controller, title); }, onProgressChanged: (controller, progress) { - vm.onProgressChange(controller, progress); + notifier.onProgressChange(controller, progress); }, ), ), @@ -70,62 +71,106 @@ class __ParseWebContentState extends State<_ParseWebContent> { ); } - Widget _buildBottomSheet(BuildContext context, ParseWebViewmodel vm) { - return Container( - padding: EdgeInsets.all(16), - height: MediaQuery.of(context).size.height * 0.7, - child: Column( - children: [ - Text("解析到的图片链接", style: Theme.of(context).textTheme.titleMedium), - SizedBox(height: 16), - Expanded( - child: ListView.builder( - itemCount: vm.urls.length, - itemBuilder: (context, index) { - final url = vm.urls[index]; - return Row( - children: [ - NetworkImageWidget(imageUrl: url), - Expanded( - child: ListTile( - title: Text( - url, - maxLines: 3, - overflow: TextOverflow.ellipsis, + void _showBottomSheet({required BuildContext context, required String url}) { + showFSheet( + context: context, + side: .btt, + builder: (context) => Consumer( + builder: (context, ref, _) { + final state = ref.watch(parseWebProvider(url)); + final notifier = ref.read(parseWebProvider(url).notifier); + + return Container( + decoration: BoxDecoration( + color: context.theme.colors.background, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + border: .symmetric( + horizontal: BorderSide(color: context.theme.colors.border), + ), + ), + child: Padding( + padding: .all(16), + child: Column( + crossAxisAlignment: .start, + children: [ + Row( + mainAxisAlignment: .spaceBetween, + children: [ + Text( + "解析到的图片链接", + style: context.theme.typography.body.xl.copyWith( + fontWeight: .w600, + color: context.theme.colors.foreground, + height: 1.5, ), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => _buildImagePreview(url), - ), - ); - }, ), + FButton.icon( + variant: .ghost, + onPress: notifier.isInit + ? () { + notifier.parseWeb(); + } + : null, + child: Icon(FLucideIcons.refreshCcw), + ), + ], + ), + SizedBox(height: 16), + Expanded( + child: FItemGroup.builder( + count: state.urls.length, + itemBuilder: (context, index) { + final url = state.urls[index]; + return FItem( + prefix: NetworkImageWidget(imageUrl: url), + title: Text( + url, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + onPress: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + _buildImagePreview(context, url), + ), + ); + }, + suffix: Icon(FLucideIcons.chevronRight), + ); + }, ), - ], - ); - }, + ), + FButton( + onPress: () { + if (state.urls.isEmpty) return; + ref.read(mainProvider.notifier).updateCurrentIndex(1); + context.go(AppRoute.main); + unawaited(notifier.startDownload()); + }, + child: const Text("下载"), + ), + ], + ), ), - ), - SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: () { - vm.startDownload(context); - }, - child: Text("下载"), - ), - ), - ], + ); + }, ), ); } - Widget _buildImagePreview(String url) { - return Scaffold( - appBar: AppBar(title: Text("图片预览")), - body: Center( + Widget _buildImagePreview(BuildContext parentContext, String url) { + return FScaffold( + header: FHeader.nested( + title: const Text("图片预览"), + prefixes: [ + FHeaderAction.back(onPress: () => Navigator.of(parentContext).pop()), + ], + ), + child: Center( child: Image.network( url, fit: BoxFit.contain, diff --git a/lib/feature/parse/ui/viewmodel/parse_archive_viewmodel.dart b/lib/feature/parse/ui/viewmodel/parse_archive_viewmodel.dart deleted file mode 100644 index ac0c186..0000000 --- a/lib/feature/parse/ui/viewmodel/parse_archive_viewmodel.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:tele_book/core/route/app_route.dart'; -import 'package:tele_book/core/util/state_util.dart'; -import 'package:tele_book/feature/book/model/dto/save_as_book_dto.dart'; -import 'package:tele_book/feature/book/repository/book_repository.dart'; -import 'package:tele_book/feature/parse/service/parse_archive_service.dart'; - -class ParseArchiveViewmodel extends ChangeNotifier { - final String archivePath; - final ParseArchiveService parseArchiveService; - final BookRepository bookRepository; - EventState parseState = IdleEventState(); - EventState saveToBookState = IdleEventState(); - List tempPaths = []; - String archiveName = ""; - - ParseArchiveViewmodel({ - required this.archivePath, - required this.parseArchiveService, - required this.bookRepository, - }) { - archiveName = archivePath.split(RegExp(r'[\\/]')).last; - paseArchive(); - } - - Future paseArchive() async { - parseState = LoadingEventState(); - notifyListeners(); - - final result = await parseArchiveService.parseArchive(archivePath); - - result.fold( - onSuccess: (data) { - tempPaths = data; - parseState = SuccessEventState(data); - notifyListeners(); - }, - onError: (error) { - parseState = ErrorEventState(error.message); - notifyListeners(); - }, - ); - } - - Future saveToBook(BuildContext context) async { - if (tempPaths.isEmpty || saveToBookState.isLoading) return; - saveToBookState = LoadingEventState(); - notifyListeners(); - try { - final result = await bookRepository.saveAsBook( - SaveAsBookDto(title: archiveName, paths: tempPaths), - ); - result.fold( - onSuccess: (data) { - saveToBookState = SuccessEventState(data); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text("保存成功"))); - notifyListeners(); - context.go(AppRoute.book); - }, - onError: (error) { - saveToBookState = ErrorEventState(error.message); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text("保存失败: ${error.message}"))); - notifyListeners(); - }, - ); - } catch (e) { - saveToBookState = ErrorEventState(e.toString()); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text("保存失败: $e"))); - notifyListeners(); - } - } -} diff --git a/lib/feature/parse/ui/viewmodel/parse_batch_archive_viewmodel.dart b/lib/feature/parse/ui/viewmodel/parse_batch_archive_viewmodel.dart deleted file mode 100644 index 5125548..0000000 --- a/lib/feature/parse/ui/viewmodel/parse_batch_archive_viewmodel.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/cupertino.dart'; -import 'package:go_router/go_router.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:tele_book/core/route/app_route.dart'; -import 'package:tele_book/core/util/state_util.dart'; -import 'package:tele_book/feature/book/model/dto/save_as_book_dto.dart'; -import 'package:tele_book/feature/book/repository/book_repository.dart'; -import 'package:tele_book/feature/parse/model/parse_batch_archive_vo.dart'; -import 'package:tele_book/feature/parse/service/parse_archive_service.dart'; - -class ParseBatchArchiveViewmodel extends ChangeNotifier { - final String? archiveDirPath; - final List? archivePaths; - final ParseArchiveService parseArchiveService; - final BookRepository bookRepository; - List parseBatchArchiveList = []; - int completeCount = 0; - int totalCount = 0; - int saveAsBookCount = 0; - EventState parseBatchArchiveState = IdleEventState(); - EventState saveBatchAsBookState = IdleEventState(); - - ParseBatchArchiveViewmodel({ - this.archiveDirPath, - this.archivePaths, - required this.parseArchiveService, - required this.bookRepository, - }) { - parseBatchArchive(); - } - - /// 请求外部存储权限,返回是否已获得 - Future _requestStoragePermission() async { - if (!Platform.isAndroid) return true; - - // Android 11+(API 30+)需要 MANAGE_EXTERNAL_STORAGE 才能读取外部目录 - if (await Permission.manageExternalStorage.isGranted) return true; - final status = await Permission.manageExternalStorage.request(); - if (status.isGranted) return true; - - // 降级:尝试普通 storage 权限(Android 10 及以下) - if (await Permission.storage.isGranted) return true; - final storageStatus = await Permission.storage.request(); - return storageStatus.isGranted; - } - - Future parseBatchArchive() async { - parseBatchArchiveState = LoadingEventState(); - notifyListeners(); - - // 先请求权限,未授权则直接返回错误状态 - final hasPermission = await _requestStoragePermission(); - if (!hasPermission) { - parseBatchArchiveState = ErrorEventState( - "需要「所有文件访问权限」才能读取外部目录,请在系统设置中授权后重试。", - ); - notifyListeners(); - return; - } - - final result = archivePaths != null && archivePaths!.isNotEmpty - ? await parseArchiveService.parseBatchArchivesFromPaths( - archivePaths!, - (total) { - totalCount = total; - notifyListeners(); - }, - (count) { - completeCount = count; - notifyListeners(); - }, - ) - : await parseArchiveService.parseBatchArchives( - archiveDirPath ?? '', - (total) { - totalCount = total; - notifyListeners(); - }, - (count) { - completeCount = count; - notifyListeners(); - }, - ); - - result.fold( - onSuccess: (data) { - if (data.isEmpty) { - parseBatchArchiveState = EmptyEventState(); - notifyListeners(); - return; - } - - parseBatchArchiveList = data; - parseBatchArchiveState = SuccessEventState(data); - notifyListeners(); - }, - onError: (error) { - parseBatchArchiveState = ErrorEventState(error.message); - notifyListeners(); - }, - ); - } - - Future saveBatchAsBook(BuildContext context) async { - saveBatchAsBookState = LoadingEventState(); - notifyListeners(); - - final dos = parseBatchArchiveList - .map((e) => SaveAsBookDto(title: e.name, paths: e.tempPaths)) - .toList(); - - final result = await bookRepository.saveBatchAsBooks(dos, (count) { - saveAsBookCount = count; - }); - - result.fold( - onSuccess: (data) { - saveBatchAsBookState = SuccessEventState(data); - notifyListeners(); - saveAsBookCount = 0; - context.go(AppRoute.book); - }, - onError: (error) { - saveBatchAsBookState = ErrorEventState(error.message); - notifyListeners(); - saveAsBookCount = 0; - }, - ); - } -} diff --git a/lib/feature/parse/ui/viewmodel/parse_batch_image_folder_viewmodel.dart b/lib/feature/parse/ui/viewmodel/parse_batch_image_folder_viewmodel.dart deleted file mode 100644 index 0282ab0..0000000 --- a/lib/feature/parse/ui/viewmodel/parse_batch_image_folder_viewmodel.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/cupertino.dart'; -import 'package:go_router/go_router.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:tele_book/core/route/app_route.dart'; -import 'package:tele_book/core/util/state_util.dart'; -import 'package:tele_book/feature/book/model/dto/save_as_book_dto.dart'; -import 'package:tele_book/feature/book/repository/book_repository.dart'; -import 'package:tele_book/feature/parse/model/parse_batch_archive_vo.dart'; -import 'package:tele_book/feature/parse/service/parse_archive_service.dart'; - -class ParseBatchImageFolderViewmodel extends ChangeNotifier { - final String? parentDirPath; - final List? imagePaths; - final ParseArchiveService parseArchiveService; - final BookRepository bookRepository; - - List parseBatchFolderList = []; - int completeCount = 0; - int totalCount = 0; - int saveAsBookCount = 0; - EventState parseBatchFolderState = IdleEventState(); - EventState saveBatchAsBookState = IdleEventState(); - - ParseBatchImageFolderViewmodel({ - this.parentDirPath, - this.imagePaths, - required this.parseArchiveService, - required this.bookRepository, - }) { - parseBatchFolders(); - } - - Future _requestStoragePermission() async { - if (!Platform.isAndroid) return true; - if (await Permission.manageExternalStorage.isGranted) return true; - final status = await Permission.manageExternalStorage.request(); - if (status.isGranted) return true; - - if (await Permission.storage.isGranted) return true; - final storageStatus = await Permission.storage.request(); - return storageStatus.isGranted; - } - - Future parseBatchFolders() async { - parseBatchFolderState = LoadingEventState(); - notifyListeners(); - - final hasPermission = await _requestStoragePermission(); - if (!hasPermission) { - parseBatchFolderState = ErrorEventState( - "需要「所有文件访问权限」才能读取外部目录,请在系统设置中授权后重试。", - ); - notifyListeners(); - return; - } - - final result = imagePaths != null && imagePaths!.isNotEmpty - ? await parseArchiveService.parseBatchImageFoldersFromPaths( - imagePaths!, - (total) { - totalCount = total; - notifyListeners(); - }, - (count) { - completeCount = count; - notifyListeners(); - }, - ) - : await parseArchiveService.parseBatchImageFolders( - parentDirPath ?? '', - (total) { - totalCount = total; - notifyListeners(); - }, - (count) { - completeCount = count; - notifyListeners(); - }, - ); - - result.fold( - onSuccess: (data) { - if (data.isEmpty) { - parseBatchFolderState = EmptyEventState(); - notifyListeners(); - return; - } - parseBatchFolderList = data; - parseBatchFolderState = SuccessEventState(data); - notifyListeners(); - }, - onError: (error) { - parseBatchFolderState = ErrorEventState(error.message); - notifyListeners(); - }, - ); - } - - Future saveBatchAsBook(BuildContext context) async { - saveBatchAsBookState = LoadingEventState(); - notifyListeners(); - - final dos = parseBatchFolderList - .map((e) => SaveAsBookDto(title: e.name, paths: e.tempPaths)) - .toList(); - - final result = await bookRepository.saveBatchAsBooks(dos, (count) { - saveAsBookCount = count; - notifyListeners(); - }); - - result.fold( - onSuccess: (_) { - saveBatchAsBookState = SuccessEventState(null); - saveAsBookCount = 0; - notifyListeners(); - context.go(AppRoute.book); - }, - onError: (error) { - saveBatchAsBookState = ErrorEventState(error.message); - saveAsBookCount = 0; - notifyListeners(); - }, - ); - } -} - diff --git a/lib/feature/parse/ui/viewmodel/parse_batch_pdf_viewmodel.dart b/lib/feature/parse/ui/viewmodel/parse_batch_pdf_viewmodel.dart deleted file mode 100644 index 44f6628..0000000 --- a/lib/feature/parse/ui/viewmodel/parse_batch_pdf_viewmodel.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:tele_book/core/route/app_route.dart'; -import 'package:tele_book/core/util/state_util.dart'; -import 'package:tele_book/feature/book/model/dto/save_as_book_dto.dart'; -import 'package:tele_book/feature/book/repository/book_repository.dart'; -import 'package:tele_book/feature/parse/model/parse_batch_archive_vo.dart'; -import 'package:tele_book/feature/parse/service/parse_pdf_service.dart'; - -class ParseBatchPdfViewmodel extends ChangeNotifier { - final String? pdfDirPath; - final List? pdfPaths; - final ParsePdfService parsePdfService; - final BookRepository bookRepository; - - List parseBatchList = []; - int completeCount = 0; - int totalCount = 0; - int saveAsBookCount = 0; - - EventState parseBatchState = const IdleEventState(); - EventState saveBatchAsBookState = const IdleEventState(); - - ParseBatchPdfViewmodel({ - this.pdfDirPath, - this.pdfPaths, - required this.parsePdfService, - required this.bookRepository, - }) { - _parseBatch(); - } - - Future _requestStoragePermission() async { - if (!Platform.isAndroid) return true; - if (await Permission.manageExternalStorage.isGranted) return true; - final status = await Permission.manageExternalStorage.request(); - if (status.isGranted) return true; - if (await Permission.storage.isGranted) return true; - final storageStatus = await Permission.storage.request(); - return storageStatus.isGranted; - } - - Future _parseBatch() async { - parseBatchState = const LoadingEventState(); - notifyListeners(); - - final hasPermission = await _requestStoragePermission(); - if (!hasPermission) { - parseBatchState = const ErrorEventState( - '需要「所有文件访问权限」才能读取外部目录,请在系统设置中授权后重试。', - ); - notifyListeners(); - return; - } - - final result = pdfPaths != null && pdfPaths!.isNotEmpty - ? await parsePdfService.parseBatchPdfsFromPaths( - pdfPaths!, - (total) { - totalCount = total; - notifyListeners(); - }, - (count) { - completeCount = count; - notifyListeners(); - }, - ) - : await parsePdfService.parseBatchPdfs( - pdfDirPath ?? '', - (total) { - totalCount = total; - notifyListeners(); - }, - (count) { - completeCount = count; - notifyListeners(); - }, - ); - - result.fold( - onSuccess: (data) { - if (data.isEmpty) { - parseBatchState = const EmptyEventState(); - notifyListeners(); - return; - } - parseBatchList = data; - parseBatchState = SuccessEventState(data); - notifyListeners(); - }, - onError: (error) { - parseBatchState = ErrorEventState(error.message); - notifyListeners(); - }, - ); - } - - Future saveBatchAsBook(BuildContext context) async { - saveBatchAsBookState = const LoadingEventState(); - notifyListeners(); - - final dos = parseBatchList - .map((e) => SaveAsBookDto(title: e.name, paths: e.tempPaths)) - .toList(); - - final result = await bookRepository.saveBatchAsBooks(dos, (count) { - saveAsBookCount = count; - }); - - result.fold( - onSuccess: (_) { - saveBatchAsBookState = const SuccessEventState(null); - saveAsBookCount = 0; - notifyListeners(); - context.go(AppRoute.book); - }, - onError: (error) { - saveBatchAsBookState = ErrorEventState(error.message); - saveAsBookCount = 0; - notifyListeners(); - }, - ); - } -} - diff --git a/lib/feature/parse/ui/viewmodel/parse_form_viewmodel.dart b/lib/feature/parse/ui/viewmodel/parse_form_viewmodel.dart deleted file mode 100644 index 8bf091a..0000000 --- a/lib/feature/parse/ui/viewmodel/parse_form_viewmodel.dart +++ /dev/null @@ -1,254 +0,0 @@ -import 'dart:io'; - -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:go_router/go_router.dart'; -import 'package:tele_book/core/route/app_route.dart'; - -class ParseFormViewmodel extends ChangeNotifier { - ParseFormType type = ParseFormType.web; - final TextEditingController urlController = TextEditingController(); - final TextEditingController archivePathController = TextEditingController(); - final TextEditingController batchArchivePathController = - TextEditingController(); - List batchArchivePaths = []; - final TextEditingController imageFolderPathController = - TextEditingController(); - List imagePaths = []; - final TextEditingController batchImageFolderPathController = - TextEditingController(); - List batchImagePaths = []; - final TextEditingController pdfPathController = TextEditingController(); - final TextEditingController batchPdfPathController = TextEditingController(); - List batchPdfPaths = []; - - void setType(ParseFormType? newType) { - if (newType != null) { - type = newType; - notifyListeners(); - } - } - - void onParse(BuildContext context) { - switch (type) { - case ParseFormType.web: - context.push(AppRoute.parseWeb, extra: urlController.text); - break; - case ParseFormType.archive: - context.push( - AppRoute.parseArchiveSingle, - extra: archivePathController.text, - ); - break; - case ParseFormType.batchArchive: - context.push( - AppRoute.parseArchiveBatch, - extra: batchArchivePaths.isNotEmpty - ? batchArchivePaths - : batchArchivePathController.text, - ); - break; - case ParseFormType.imageFolder: - context.push( - AppRoute.parseImageFolder, - extra: imagePaths.isNotEmpty - ? imagePaths - : imageFolderPathController.text, - ); - break; - case ParseFormType.batchImageFolder: - context.push( - AppRoute.parseBatchImageFolder, - extra: batchImagePaths.isNotEmpty - ? batchImagePaths - : batchImageFolderPathController.text, - ); - break; - case ParseFormType.pdf: - context.push(AppRoute.parsePdf, extra: pdfPathController.text); - break; - case ParseFormType.batchPdf: - context.push( - AppRoute.parseBatchPdf, - extra: batchPdfPaths.isNotEmpty - ? batchPdfPaths - : batchPdfPathController.text, - ); - break; - } - } - - Future getClipboardUrl() async { - final clipboardData = await Clipboard.getData('text/plain'); - final text = clipboardData?.text ?? ''; - if (Uri.tryParse(text)?.hasAbsolutePath == true) { - urlController.text = text; - notifyListeners(); - } - } - - Future pickerArchive(BuildContext context) async { - final result = await FilePicker.platform.pickFiles( - dialogTitle: "选择 TeleBook 导出的书籍归档文件", - type: FileType.custom, - allowedExtensions: ['zip'], - ); - if (result != null && result.files.single.path != null) { - final path = result.files.single.path!; - archivePathController.text = path; - notifyListeners(); - } - } - - Future pickerBatchArchive(BuildContext context) async { - if (Platform.isIOS) { - final result = await FilePicker.platform.pickFiles( - dialogTitle: "选择一个或多个 ZIP 压缩包", - type: FileType.custom, - allowedExtensions: ['zip'], - allowMultiple: true, - ); - if (result != null) { - final paths = result.paths.whereType().toList(); - batchArchivePaths = paths; - batchArchivePathController.text = paths.isEmpty - ? '' - : '已选择 ${paths.length} 个 ZIP 文件'; - notifyListeners(); - } - return; - } - - final result = await FilePicker.platform.getDirectoryPath( - dialogTitle: "选择 TeleBook 导出的书籍归档文件夹", - ); - if (result != null) { - batchArchivePaths = []; - batchArchivePathController.text = result; - notifyListeners(); - } - } - - Future pickerImageFolder(BuildContext context) async { - if (Platform.isIOS) { - final result = await FilePicker.platform.pickFiles( - dialogTitle: "选择图片文件", - type: FileType.custom, - allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'], - allowMultiple: true, - ); - if (result != null) { - final paths = result.paths.whereType().toList(); - imagePaths = paths; - imageFolderPathController.text = paths.isEmpty - ? '' - : '已选择 ${paths.length} 张图片'; - notifyListeners(); - } - return; - } - - final result = await FilePicker.platform.getDirectoryPath( - dialogTitle: "选择包含图片的文件夹", - ); - if (result != null) { - imagePaths = []; - imageFolderPathController.text = result; - notifyListeners(); - } - } - - Future pickerBatchImageFolder(BuildContext context) async { - if (Platform.isIOS) { - final result = await FilePicker.platform.pickFiles( - dialogTitle: "选择批量图片文件", - type: FileType.custom, - allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'], - allowMultiple: true, - ); - if (result != null) { - final paths = result.paths.whereType().toList(); - batchImagePaths = paths; - batchImageFolderPathController.text = paths.isEmpty - ? '' - : '已选择 ${paths.length} 张图片(按所在文件夹分组)'; - notifyListeners(); - } - return; - } - - final result = await FilePicker.platform.getDirectoryPath( - dialogTitle: "选择批量图片文件夹的父目录", - ); - if (result != null) { - batchImagePaths = []; - batchImageFolderPathController.text = result; - notifyListeners(); - } - } - - Future pickerPdf(BuildContext context) async { - final result = await FilePicker.platform.pickFiles( - dialogTitle: "选择 PDF 文件", - type: FileType.custom, - allowedExtensions: ['pdf'], - ); - if (result != null && result.files.single.path != null) { - pdfPathController.text = result.files.single.path!; - notifyListeners(); - } - } - - Future pickerBatchPdf(BuildContext context) async { - if (Platform.isIOS) { - final result = await FilePicker.platform.pickFiles( - dialogTitle: "选择一个或多个 PDF 文件", - type: FileType.custom, - allowedExtensions: ['pdf'], - allowMultiple: true, - ); - if (result != null) { - final paths = result.paths.whereType().toList(); - batchPdfPaths = paths; - batchPdfPathController.text = paths.isEmpty - ? '' - : '已选择 ${paths.length} 个 PDF 文件'; - notifyListeners(); - } - return; - } - - final result = await FilePicker.platform.getDirectoryPath( - dialogTitle: "选择包含 PDF 的文件夹", - ); - if (result != null) { - batchPdfPaths = []; - batchPdfPathController.text = result; - notifyListeners(); - } - } - - @override - void dispose() { - urlController.dispose(); - archivePathController.dispose(); - batchArchivePathController.dispose(); - imageFolderPathController.dispose(); - batchImageFolderPathController.dispose(); - pdfPathController.dispose(); - batchPdfPathController.dispose(); - super.dispose(); - } -} - -enum ParseFormType { - web, - archive, - batchArchive, - imageFolder, - batchImageFolder, - pdf, - batchPdf, -} diff --git a/lib/feature/parse/ui/viewmodel/parse_image_folder_viewmodel.dart b/lib/feature/parse/ui/viewmodel/parse_image_folder_viewmodel.dart deleted file mode 100644 index 33c1470..0000000 --- a/lib/feature/parse/ui/viewmodel/parse_image_folder_viewmodel.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:tele_book/core/route/app_route.dart'; -import 'package:tele_book/core/util/state_util.dart'; -import 'package:tele_book/feature/book/model/dto/save_as_book_dto.dart'; -import 'package:tele_book/feature/book/repository/book_repository.dart'; -import 'package:tele_book/feature/parse/service/parse_archive_service.dart'; - -class ParseImageFolderViewmodel extends ChangeNotifier { - final String? folderPath; - final List? imagePathsInput; - final ParseArchiveService parseArchiveService; - final BookRepository bookRepository; - - EventState parseState = IdleEventState(); - EventState saveToBookState = IdleEventState(); - List imagePaths = []; - String folderName = ""; - - ParseImageFolderViewmodel({ - this.folderPath, - this.imagePathsInput, - required this.parseArchiveService, - required this.bookRepository, - }) { - if (folderPath != null && folderPath!.isNotEmpty) { - folderName = folderPath!.split(RegExp(r'[\\/]')).last; - } else if (imagePathsInput != null && imagePathsInput!.isNotEmpty) { - final first = imagePathsInput!.first; - final parts = first.split(RegExp(r'[\\/]')); - folderName = parts.length > 1 ? parts[parts.length - 2] : '导入图片'; - } else { - folderName = '导入图片'; - } - parseImageFolder(); - } - - Future _requestStoragePermission() async { - if (!Platform.isAndroid) return true; - if (await Permission.manageExternalStorage.isGranted) return true; - final status = await Permission.manageExternalStorage.request(); - if (status.isGranted) return true; - - if (await Permission.storage.isGranted) return true; - final storageStatus = await Permission.storage.request(); - return storageStatus.isGranted; - } - - Future parseImageFolder() async { - parseState = LoadingEventState(); - notifyListeners(); - - final hasPermission = await _requestStoragePermission(); - if (!hasPermission) { - parseState = ErrorEventState("需要存储权限才能读取图片文件夹"); - notifyListeners(); - return; - } - - final result = imagePathsInput != null && imagePathsInput!.isNotEmpty - ? await parseArchiveService.parseImagePaths(imagePathsInput!) - : await parseArchiveService.parseImageFolder(folderPath ?? ''); - result.fold( - onSuccess: (data) { - imagePaths = data; - parseState = data.isEmpty ? EmptyEventState() : SuccessEventState(data); - notifyListeners(); - }, - onError: (error) { - parseState = ErrorEventState(error.message); - notifyListeners(); - }, - ); - } - - Future saveToBook(BuildContext context) async { - if (imagePaths.isEmpty || saveToBookState.isLoading) return; - saveToBookState = LoadingEventState(); - notifyListeners(); - - final result = await bookRepository.saveAsBook( - SaveAsBookDto(title: folderName, paths: imagePaths), - ); - result.fold( - onSuccess: (_) { - saveToBookState = SuccessEventState(null); - notifyListeners(); - context.go(AppRoute.book); - }, - onError: (error) { - saveToBookState = ErrorEventState(error.message); - notifyListeners(); - }, - ); - } -} - diff --git a/lib/feature/parse/ui/viewmodel/parse_pdf_viewmodel.dart b/lib/feature/parse/ui/viewmodel/parse_pdf_viewmodel.dart deleted file mode 100644 index 8ca1060..0000000 --- a/lib/feature/parse/ui/viewmodel/parse_pdf_viewmodel.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:tele_book/core/route/app_route.dart'; -import 'package:tele_book/core/util/state_util.dart'; -import 'package:tele_book/feature/book/model/dto/save_as_book_dto.dart'; -import 'package:tele_book/feature/book/repository/book_repository.dart'; -import 'package:tele_book/feature/parse/service/parse_pdf_service.dart'; - -class ParsePdfViewmodel extends ChangeNotifier { - final String pdfPath; - final ParsePdfService parsePdfService; - final BookRepository bookRepository; - - EventState parseState = const IdleEventState(); - EventState saveToBookState = const IdleEventState(); - - List tempPaths = []; - String pdfName = ''; - int currentPage = 0; - int totalPages = 0; - - ParsePdfViewmodel({ - required this.pdfPath, - required this.parsePdfService, - required this.bookRepository, - }) { - pdfName = pdfPath.split(RegExp(r'[\\/]')).last.replaceAll('.pdf', ''); - _parsePdf(); - } - - Future _parsePdf() async { - parseState = const LoadingEventState(); - notifyListeners(); - - final result = await parsePdfService.parsePdf( - pdfPath, - onProgress: (current, total) { - currentPage = current; - totalPages = total; - notifyListeners(); - }, - ); - - result.fold( - onSuccess: (data) { - tempPaths = data; - parseState = SuccessEventState(data); - notifyListeners(); - }, - onError: (error) { - parseState = ErrorEventState(error.message); - notifyListeners(); - }, - ); - } - - Future saveToBook(BuildContext context) async { - if (tempPaths.isEmpty || saveToBookState.isLoading) return; - saveToBookState = const LoadingEventState(); - notifyListeners(); - - try { - final result = await bookRepository.saveAsBook( - SaveAsBookDto(title: pdfName, paths: tempPaths), - ); - result.fold( - onSuccess: (_) { - saveToBookState = const SuccessEventState(null); - notifyListeners(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('保存成功')), - ); - context.go(AppRoute.book); - }, - onError: (error) { - saveToBookState = ErrorEventState(error.message); - notifyListeners(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('保存失败: ${error.message}')), - ); - }, - ); - } catch (e) { - saveToBookState = ErrorEventState(e.toString()); - notifyListeners(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('保存失败: $e')), - ); - } - } -} - diff --git a/lib/feature/parse/ui/viewmodel/parse_web_viewmodel.dart b/lib/feature/parse/ui/viewmodel/parse_web_viewmodel.dart deleted file mode 100644 index ab66cb4..0000000 --- a/lib/feature/parse/ui/viewmodel/parse_web_viewmodel.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:go_router/go_router.dart'; -import 'package:tele_book/core/route/app_route.dart'; -import 'package:tele_book/feature/download/service/download_service.dart'; -import 'package:tele_book/feature/parse/service/parse_web_service.dart'; - - -class ParseWebViewmodel extends ChangeNotifier { - final ParseWebService _parseWebService; - final DownloadService _downloadService; - - ParseWebViewmodel(this._parseWebService, this._downloadService); - - InAppWebViewController? webViewController; - InAppWebViewSettings settings = InAppWebViewSettings( - isInspectable: kDebugMode, - mediaPlaybackRequiresUserGesture: false, - allowsInlineMediaPlayback: true, - iframeAllow: "camera; microphone", - iframeAllowFullscreen: true, - ); - String title = "加载中..."; - List urls = []; - int progress = 0; - - void onLoadStart(InAppWebViewController controller) { - webViewController = controller; - notifyListeners(); - } - - void onTitleChanged(InAppWebViewController controller, String? title) { - this.title = title ?? "无标题"; - print("提取到的标题: $title"); - notifyListeners(); - } - - void onProgressChange(InAppWebViewController controller, int progress) async { - final urls = await _parseWebService.extractImagesFromWebView( - onExtractImages: (js) async { - final urls = await controller.evaluateJavascript(source: js); - return urls; - }, - ); - this.urls = urls; - this.progress = progress; - notifyListeners(); - } - - void startDownload(BuildContext context) { - if (urls.isEmpty) return; - _downloadService.startDownload(urls, title); - context.go(AppRoute.book); - } -} diff --git a/lib/main.dart b/lib/main.dart index 66064a0..1950036 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:forui/forui.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'package:tele_book/common/config/global_config.dart'; import 'package:tele_book/common/theme/app_theme.dart'; -import 'package:tele_book/core/di/app_di.dart'; import 'package:tele_book/core/route/app_route.dart'; void main() async { @@ -40,23 +40,28 @@ Future _init() async { await InAppWebViewController.setWebContentsDebuggingEnabled(kDebugMode); } - appProviders = MultiProvider( - providers: [...createAppDI()], + final (lightTheme, darkTheme) = + const { + .android, + .iOS, + .fuchsia, + }.contains(defaultTargetPlatform) + ? (FTheme.neutral.light.touch, FTheme.neutral.dark.touch) + : (FTheme.neutral.light.desktop, FTheme.neutral.dark.desktop); + + appProviders = ProviderScope( child: MaterialApp.router( - title: 'TeleBook', + title: 'tele_book', routerConfig: AppRoute.router, debugShowCheckedModeBanner: false, - theme: AppTheme.light, - darkTheme: AppTheme.dark, + supportedLocales: FLocalizations.supportedLocales, + localizationsDelegates: const [...FLocalizations.localizationsDelegates], + theme: lightTheme.toApproximateMaterialTheme(), + darkTheme: darkTheme.toApproximateMaterialTheme(), themeMode: ThemeMode.system, - builder: (context, child) => ResponsiveBreakpoints.builder( - child: child!, - breakpoints: [ - const Breakpoint(start: 0, end: 480, name: MOBILE), - const Breakpoint(start: 481, end: 800, name: TABLET), - const Breakpoint(start: 801, end: 1200, name: DESKTOP), - const Breakpoint(start: 1201, end: double.infinity, name: '4K'), - ], + builder: (context, child) => FTheme( + data: Theme.brightnessOf(context) == .light ? lightTheme : darkTheme, + child: FToaster(child: FTooltipGroup(child: child!)), ), ), ); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 4c0025f..08fbce7 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_inappwebview_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterInappwebviewLinuxPlugin"); + flutter_inappwebview_linux_plugin_register_with_registrar(flutter_inappwebview_linux_registrar); g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index ad279a8..f0a73fa 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,11 +3,13 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_inappwebview_linux sqlite3_flutter_libs url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0a7d3b8..e6f081b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import file_picker +import flutter_image_compress_macos import flutter_inappwebview_macos import nsd_macos import package_info_plus @@ -18,6 +19,7 @@ import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) NsdMacosPlugin.register(with: registry.registrar(forPlugin: "NsdMacosPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) diff --git a/macos/Podfile b/macos/Podfile deleted file mode 100644 index b52666a..0000000 --- a/macos/Podfile +++ /dev/null @@ -1,43 +0,0 @@ -platform :osx, '10.15' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_macos_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_macos_build_settings(target) - end -end diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 147bb0f..c2e4704 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -21,14 +21,15 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - 068057681EBC0164278138EE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52C3F6C80C354EAC194FF017 /* Pods_Runner.framework */; }; 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 47872D1CD946E0B05FF0865B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E2D04AE39B8CD9253FACF36F /* Pods_RunnerTests.framework */; }; + 4C0D964FFD8E0E1C15A516F5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 21424197D5842335C26ECC6E /* Pods_Runner.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + BDE97472BE39ADEB8998F11C /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E37E4236474FDB01D292621 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -62,12 +63,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0D5418905133AF2E183332ED /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 20FC8B8BD9C1C6F8EE0D561D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 21424197D5842335C26ECC6E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2890361C15CCC1C9FFF925E2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* wo_nas.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = wo_nas.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* tele_book.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = tele_book.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -79,15 +82,14 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 39F4CCAB4B2333991EE870B5 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 52C3F6C80C354EAC194FF017 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 61815FF484F50BF3C434C847 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - 751E53A3618EF980C2B80835 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 45B361BDE7B819C9E996548B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 5E37E4236474FDB01D292621 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 60179C70E3F0D97078C749B8 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 68E590BE6A61E5C0AC2C0D57 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - E2D04AE39B8CD9253FACF36F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F7C816C2674A757608F1B430 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - F86AB340ED7EBD2475AC8D37 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 97EAD1D63D707A24CD7F7B8C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -95,7 +97,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 47872D1CD946E0B05FF0865B /* Pods_RunnerTests.framework in Frameworks */, + BDE97472BE39ADEB8998F11C /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -103,13 +105,28 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 068057681EBC0164278138EE /* Pods_Runner.framework in Frameworks */, + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + 4C0D964FFD8E0E1C15A516F5 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0194D0BA33C64134F7153C62 /* Pods */ = { + isa = PBXGroup; + children = ( + 97EAD1D63D707A24CD7F7B8C /* Pods-Runner.debug.xcconfig */, + 2890361C15CCC1C9FFF925E2 /* Pods-Runner.release.xcconfig */, + 45B361BDE7B819C9E996548B /* Pods-Runner.profile.xcconfig */, + 20FC8B8BD9C1C6F8EE0D561D /* Pods-RunnerTests.debug.xcconfig */, + 60179C70E3F0D97078C749B8 /* Pods-RunnerTests.release.xcconfig */, + 68E590BE6A61E5C0AC2C0D57 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -137,14 +154,14 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, - 4A325688A39558AB9A5FDDF5 /* Pods */, + 0194D0BA33C64134F7153C62 /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* wo_nas.app */, + 33CC10ED2044A3C60003C045 /* tele_book.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; @@ -164,6 +181,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, @@ -185,25 +203,11 @@ path = Runner; sourceTree = ""; }; - 4A325688A39558AB9A5FDDF5 /* Pods */ = { - isa = PBXGroup; - children = ( - 751E53A3618EF980C2B80835 /* Pods-Runner.debug.xcconfig */, - 0D5418905133AF2E183332ED /* Pods-Runner.release.xcconfig */, - F86AB340ED7EBD2475AC8D37 /* Pods-Runner.profile.xcconfig */, - 39F4CCAB4B2333991EE870B5 /* Pods-RunnerTests.debug.xcconfig */, - 61815FF484F50BF3C434C847 /* Pods-RunnerTests.release.xcconfig */, - F7C816C2674A757608F1B430 /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( - 52C3F6C80C354EAC194FF017 /* Pods_Runner.framework */, - E2D04AE39B8CD9253FACF36F /* Pods_RunnerTests.framework */, + 21424197D5842335C26ECC6E /* Pods_Runner.framework */, + 5E37E4236474FDB01D292621 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -215,7 +219,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 0D01681597DA0B7381D9BEF4 /* [CP] Check Pods Manifest.lock */, + 2F681318101AFA9BE4B572C2 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -234,13 +238,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - D3606AACA5A32258415D76F6 /* [CP] Check Pods Manifest.lock */, + 2DF00E3BAFF6F7125C743DBE /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 0C11A0C5FB4735CAA6B427E9 /* [CP] Embed Pods Frameworks */, + 8F00F77955ECAE6FA17BB14F /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -248,8 +252,11 @@ 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* wo_nas.app */; + productReference = 33CC10ED2044A3C60003C045 /* tele_book.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -258,6 +265,7 @@ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; @@ -291,6 +299,9 @@ Base, ); mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -322,24 +333,29 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 0C11A0C5FB4735CAA6B427E9 /* [CP] Embed Pods Frameworks */ = { + 2DF00E3BAFF6F7125C743DBE /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 0D01681597DA0B7381D9BEF4 /* [CP] Check Pods Manifest.lock */ = { + 2F681318101AFA9BE4B572C2 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -399,26 +415,21 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - D3606AACA5A32258415D76F6 /* [CP] Check Pods Manifest.lock */ = { + 8F00F77955ECAE6FA17BB14F /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -472,46 +483,46 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 39F4CCAB4B2333991EE870B5 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 20FC8B8BD9C1C6F8EE0D561D /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.dorkytiger.woNas.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.example.teleBook.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/wo_nas.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/wo_nas"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/tele_book.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/tele_book"; }; name = Debug; }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 61815FF484F50BF3C434C847 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 60179C70E3F0D97078C749B8 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.dorkytiger.woNas.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.example.teleBook.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/wo_nas.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/wo_nas"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/tele_book.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/tele_book"; }; name = Release; }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F7C816C2674A757608F1B430 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = 68E590BE6A61E5C0AC2C0D57 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.dorkytiger.woNas.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.example.teleBook.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/wo_nas.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/wo_nas"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/tele_book.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/tele_book"; }; name = Profile; }; @@ -520,6 +531,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -543,9 +555,11 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -593,6 +607,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -616,9 +631,11 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -646,6 +663,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -669,9 +687,11 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -786,6 +806,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 0e0bb2d..df5fec8 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + @@ -31,7 +49,7 @@ @@ -66,7 +84,7 @@ @@ -83,7 +101,7 @@ diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index 7579872..a57b18f 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,10 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = wo_nas +PRODUCT_NAME = tele_book // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.dorkytiger.woNas +PRODUCT_BUNDLE_IDENTIFIER = com.example.teleBook // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2023 com.dorkytiger. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index 0eaccf1..dddb8a3 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -6,10 +6,6 @@ com.apple.security.cs.allow-jit - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - com.apple.security.network.server diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index a046386..852fa1a 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -4,9 +4,5 @@ com.apple.security.app-sandbox - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift index 5418c9f..61f3bd1 100644 --- a/macos/RunnerTests/RunnerTests.swift +++ b/macos/RunnerTests/RunnerTests.swift @@ -1,5 +1,5 @@ -import FlutterMacOS import Cocoa +import FlutterMacOS import XCTest class RunnerTests: XCTestCase { diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index c57ecc4..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,1410 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" - url: "https://pub.flutter-io.cn" - source: hosted - version: "93.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b - url: "https://pub.flutter-io.cn" - source: hosted - version: "10.0.1" - archive: - dependency: "direct main" - description: - name: archive - sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.0.9" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.13.1" - background_downloader: - dependency: "direct main" - description: - name: background_downloader - sha256: "4cb23d9ad4f5060944f38164e7b90d4bf99b57b2472a3bd4676e59b2db4afd06" - url: "https://pub.flutter-io.cn" - source: hosted - version: "9.5.4" - barcode: - dependency: transitive - description: - name: barcode - sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.9" - bidi: - dependency: transitive - description: - name: bidi - sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.13" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.2" - build: - dependency: transitive - description: - name: build - sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.0.5" - build_config: - dependency: transitive - description: - name: build_config - sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.0" - build_daemon: - dependency: transitive - description: - name: build_daemon - sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.1.1" - build_runner: - dependency: "direct dev" - description: - name: build_runner - sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.13.1" - built_collection: - dependency: transitive - description: - name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.flutter-io.cn" - source: hosted - version: "5.1.1" - built_value: - dependency: transitive - description: - name: built_value - sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af" - url: "https://pub.flutter-io.cn" - source: hosted - version: "8.12.5" - characters: - dependency: transitive - description: - name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.4.1" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.4.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.4" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.4.2" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.0" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.11.1" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.1.2" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.3.5+2" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.7" - csslib: - dependency: transitive - description: - name: csslib - sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.2" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.9" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.1.7" - dbus: - dependency: transitive - description: - name: dbus - sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.7.12" - dio: - dependency: "direct main" - description: - name: dio - sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c - url: "https://pub.flutter-io.cn" - source: hosted - version: "5.9.2" - dio_web_adapter: - dependency: transitive - description: - name: dio_web_adapter - sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.2" - dk_util: - dependency: "direct main" - description: - name: dk_util - sha256: e7b67ab113a0ac83411559f37401c3b4d81cc16b0510b637b2bcaa2a133dd597 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.0" - drift: - dependency: "direct main" - description: - name: drift - sha256: "970cd188fddb111b26ea6a9b07a62bf5c2432d74147b8122c67044ae3b97e99e" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.31.0" - drift_dev: - dependency: "direct dev" - description: - name: drift_dev - sha256: "917184b2fb867b70a548a83bf0d36268423b38d39968c06cce4905683da49587" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.31.0" - drift_flutter: - dependency: "direct main" - description: - name: drift_flutter - sha256: c07120854742a0cae2f7501a0da02493addde550db6641d284983c08762e60a7 - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.8" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.0" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.flutter-io.cn" - source: hosted - version: "7.0.1" - file_picker: - dependency: "direct main" - description: - name: file_picker - sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" - url: "https://pub.flutter-io.cn" - source: hosted - version: "10.3.10" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.1" - flex_color_scheme: - dependency: "direct main" - description: - name: flex_color_scheme - sha256: ab854146f201d2d62cc251fd525ef023b84182c4a0bfe4ae4c18ffc505b412d3 - url: "https://pub.flutter-io.cn" - source: hosted - version: "8.4.0" - flex_seed_scheme: - dependency: transitive - description: - name: flex_seed_scheme - sha256: a3183753bbcfc3af106224bff3ab3e1844b73f58062136b7499919f49f3667e7 - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.0.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_inappwebview: - dependency: "direct main" - description: - name: flutter_inappwebview - sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.1.5" - flutter_inappwebview_android: - dependency: transitive - description: - name: flutter_inappwebview_android - sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.3" - flutter_inappwebview_internal_annotations: - dependency: transitive - description: - name: flutter_inappwebview_internal_annotations - sha256: e30fba942e3debea7b7e6cdd4f0f59ce89dd403a9865193e3221293b6d1544c6 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.0" - flutter_inappwebview_ios: - dependency: transitive - description: - name: flutter_inappwebview_ios - sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.2" - flutter_inappwebview_macos: - dependency: transitive - description: - name: flutter_inappwebview_macos - sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.2" - flutter_inappwebview_platform_interface: - dependency: transitive - description: - name: flutter_inappwebview_platform_interface - sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.0+1" - flutter_inappwebview_web: - dependency: transitive - description: - name: flutter_inappwebview_web - sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.2" - flutter_inappwebview_windows: - dependency: transitive - description: - name: flutter_inappwebview_windows - sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.6.0" - flutter_launcher_icons: - dependency: "direct main" - description: - name: flutter_launcher_icons - sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.14.4" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.0.0" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.34" - flutter_reorderable_grid_view: - dependency: "direct main" - description: - name: flutter_reorderable_grid_view - sha256: "4b7752425dba3f5dec03dd8d11c63027592535c7b7ece65e2c9573a2ea310206" - url: "https://pub.flutter-io.cn" - source: hosted - version: "5.6.0" - flutter_staggered_grid_view: - dependency: "direct main" - description: - name: flutter_staggered_grid_view - sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.7.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - get: - dependency: transitive - description: - name: get - sha256: "5ed34a7925b85336e15d472cc4cfe7d9ebf4ab8e8b9f688585bf6b50f4c3d79a" - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.7.3" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.3" - go_router: - dependency: "direct main" - description: - name: go_router - sha256: "5540e4a3f416dd4a93458257b908eb88353cbd0fb5b0a3d1bd7d849ba1e88735" - url: "https://pub.flutter-io.cn" - source: hosted - version: "17.2.1" - graphs: - dependency: transitive - description: - name: graphs - sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.3.2" - hooks: - dependency: transitive - description: - name: hooks - sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.3" - html: - dependency: "direct main" - description: - name: html - sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.15.6" - http: - dependency: "direct main" - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.6.0" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.2.2" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.1.2" - image: - dependency: transitive - description: - name: image - sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.8.0" - intl: - dependency: "direct main" - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.20.2" - io: - dependency: transitive - description: - name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.5" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.11.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.flutter-io.cn" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.1.0" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.12.19" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.13.0" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.17.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.17.6" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.0" - nsd: - dependency: transitive - description: - name: nsd - sha256: "1611a5c9f61d56ff2973e1488ae04112103e5203b4a7a1fb594b48cfb366fc14" - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.1.0" - nsd_android: - dependency: transitive - description: - name: nsd_android - sha256: "96d2d451c5db0319c37b1b2a38f2d55eb56ae54c0b0d3144c03c19c97436de4a" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.0" - nsd_ios: - dependency: transitive - description: - name: nsd_ios - sha256: "562fffe753543a65190344d3acd0ed80d96c571ac1a05bf10780c5584b5055ac" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.1" - nsd_macos: - dependency: transitive - description: - name: nsd_macos - sha256: "47cd355d84009befe02710c72bf1c1999b24d5f3fb3a3d914f8b77bcaca42542" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.1" - nsd_platform_interface: - dependency: transitive - description: - name: nsd_platform_interface - sha256: b1a5ace6f01ea2ce37f373e52c3b7af4fd7c11de2582ddcc89f4fc00615d9dff - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.0" - nsd_windows: - dependency: transitive - description: - name: nsd_windows - sha256: "68b4a256b0be258dbbad0ae789f2e8838d0935a353dac86b17c14c1a05df4ecd" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.1" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" - url: "https://pub.flutter-io.cn" - source: hosted - version: "9.3.0" - open_filex: - dependency: "direct main" - description: - name: open_filex - sha256: "9976da61b6a72302cf3b1efbce259200cd40232643a467aac7370addf94d6900" - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.7.0" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.0" - package_info_plus: - dependency: transitive - description: - name: package_info_plus - sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d - url: "https://pub.flutter-io.cn" - source: hosted - version: "9.0.0" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.2.1" - path: - dependency: "direct main" - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.9.1" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.0" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.23" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.3.0" - pdf: - dependency: "direct main" - description: - name: pdf - sha256: e47a275b267873d5944ad5f5ff0dcc7ac2e36c02b3046a0ffac9b72fd362c44b - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.12.0" - pdfium_dart: - dependency: transitive - description: - name: pdfium_dart - sha256: "58ad7325da54fd6b36860b0dcda8193b26214a3e8494036404bb3954c7795b07" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.0" - pdfium_flutter: - dependency: transitive - description: - name: pdfium_flutter - sha256: "05b15269ddeb81f0c1e4748c9f9fd61bc2109ae5e193962456ec15d885332af3" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.0" - pdfrx: - dependency: "direct main" - description: - name: pdfrx - sha256: ba1003bc45454de82ba4911791561533108cfb5ebecb54d8d06489ff6b7f8ebc - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.3.2" - pdfrx_engine: - dependency: transitive - description: - name: pdfrx_engine - sha256: bbb71414a7828128e87a017c97f3c392e379b9e171beaaa37aa225c597ca4f6e - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.4.1" - permission_handler: - dependency: "direct main" - description: - name: permission_handler - sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 - url: "https://pub.flutter-io.cn" - source: hosted - version: "12.0.1" - permission_handler_android: - dependency: transitive - description: - name: permission_handler_android - sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" - url: "https://pub.flutter-io.cn" - source: hosted - version: "13.0.1" - permission_handler_apple: - dependency: transitive - description: - name: permission_handler_apple - sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 - url: "https://pub.flutter-io.cn" - source: hosted - version: "9.4.7" - permission_handler_html: - dependency: transitive - description: - name: permission_handler_html - sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.1.3+5" - permission_handler_platform_interface: - dependency: transitive - description: - name: permission_handler_platform_interface - sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.3.0" - permission_handler_windows: - dependency: transitive - description: - name: permission_handler_windows - sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.1" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.flutter-io.cn" - source: hosted - version: "7.0.2" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.8" - pool: - dependency: transitive - description: - name: pool - sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.5.2" - posix: - dependency: transitive - description: - name: posix - sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.5.0" - provider: - dependency: "direct main" - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.1.5+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.0" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.5.0" - qr: - dependency: transitive - description: - name: qr - sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.2" - recase: - dependency: transitive - description: - name: recase - sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.1.0" - record_use: - dependency: transitive - description: - name: record_use - sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.6.0" - responsive_framework: - dependency: "direct main" - description: - name: responsive_framework - sha256: a8e1c13d4ba980c60cbf6fa1e9907cd60662bf2585184d7c96ca46c43de91552 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.5.1" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.28.0" - share_plus: - dependency: "direct main" - description: - name: share_plus - sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" - url: "https://pub.flutter-io.cn" - source: hosted - version: "12.0.1" - share_plus_platform_interface: - dependency: transitive - description: - name: share_plus_platform_interface - sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.1.0" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.5.5" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.4.23" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.5.6" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.4.2" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.4.1" - shelf: - dependency: transitive - description: - name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.4.2" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.0" - shimmer: - dependency: "direct main" - description: - name: shimmer - sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_gen: - dependency: transitive - description: - name: source_gen - sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd" - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.2.2" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.10.2" - sqlite3: - dependency: transitive - description: - name: sqlite3 - sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.9.4" - sqlite3_flutter_libs: - dependency: transitive - description: - name: sqlite3_flutter_libs - sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.5.42" - sqlparser: - dependency: transitive - description: - name: sqlparser - sha256: "337e9997f7141ffdd054259128553c348635fa318f7ca492f07a4ab76f850d19" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.43.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.4" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.1" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.4.1" - synchronized: - dependency: "direct main" - description: - name: synchronized - sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.4.0+1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.7.10" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.4.0" - url_launcher: - dependency: transitive - description: - name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.3.2" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.3.29" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.4.1" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.2.2" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.2.5" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.4.2" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.1.5" - uuid: - dependency: "direct main" - description: - name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.5.3" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.flutter-io.cn" - source: hosted - version: "15.0.2" - wakelock_plus: - dependency: "direct main" - description: - name: wakelock_plus - sha256: "8b12256f616346910c519a35606fb69b1fe0737c06b6a447c6df43888b097f39" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.5.1" - wakelock_plus_platform_interface: - dependency: transitive - description: - name: wakelock_plus_platform_interface - sha256: "24b84143787220a403491c2e5de0877fbbb87baf3f0b18a2a988973863db4b03" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.4.0" - watcher: - dependency: transitive - description: - name: watcher - sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.2.1" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.1" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.3" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.13.1" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: "0f7fcd2c86bf36bdcf94881f7941ce0cbc4f8d104b9fdcd5fcbef90e2199db76" - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.10.15" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.15.1" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: d7219cfabc6f5fc2032e0fa980ec36d71f308a35a823395af1abc34d9a2ede83 - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.24.2" - win32: - dependency: transitive - description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.flutter-io.cn" - source: hosted - version: "5.15.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.0" - xml: - dependency: transitive - description: - name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.6.1" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.11.0 <4.0.0" - flutter: ">=3.41.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4f8401a..6285ad4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: tele_book -version: 3.1.4+3 +version: 3.1.4+4 publish_to: none description: Simple way that download the telegraph Book. environment: @@ -25,7 +25,7 @@ dependencies: http: ^1.3.0 wakelock_plus: ^1.2.10 webview_flutter: ^4.13.0 - flutter_inappwebview: ^6.1.5 + flutter_inappwebview: ^6.2.0-beta.3 open_filex: ^4.4.0 share_plus: ^12.0.1 dk_util: ^1.0.0 @@ -33,20 +33,28 @@ dependencies: flutter_reorderable_grid_view: ^5.5.2 flutter_staggered_grid_view: ^0.7.0 responsive_framework: ^1.5.1 - provider: ^6.1.5+1 + riverpod: ^3.2.1 go_router: ^17.2.1 uuid: ^4.5.3 synchronized: ^3.4.0+1 shimmer: ^3.0.0 - pdf: ^3.11.0 - pdfrx: ^2.3.2 + pdf: ^3.12.0 + pdfrx: ^2.4.3 + flutter_riverpod: ^3.3.1 + riverpod_annotation: ^4.0.2 + freezed_annotation: ^3.1.0 + forui: ^0.24.3 + flutter_image_compress: ^2.4.0 dev_dependencies: - build_runner: ^2.4.15 + build_runner: ^2.15.0 drift_dev: ^2.26.0 flutter_lints: ^6.0.0 flutter_test: sdk: flutter + riverpod_generator: ^4.0.3 + riverpod_lint: ^3.1.3 + freezed: ^3.2.5 flutter: uses-material-design: true diff --git a/res/change-log.md b/res/change-log.md index 60db9c7..e442a28 100644 --- a/res/change-log.md +++ b/res/change-log.md @@ -34,3 +34,13 @@ - **优化** 内存泄漏防护 +# 2026.05.27 + +## 3.1.5 - riverpod 重构 + +### 🛠️ 主要更新 +- **重构** 使用 Riverpod 替代 Provider 进行状态管理 +- **优化** 代码结构,提升可维护性和扩展性 +- **增强** 组件解耦,提升性能和响应速度 +- **修复** 相关状态管理的潜在bug,提升稳定性 + diff --git a/tools/install_nuget.ps1 b/tools/install_nuget.ps1 index 4f31559..283bcb4 100644 --- a/tools/install_nuget.ps1 +++ b/tools/install_nuget.ps1 @@ -19,7 +19,7 @@ if (Test-Path $targetPath) { Write-Host "" Write-Host "请将该目录加入系统 PATH,或在系统环境变量中设置 NUGET_EXECUTABLE 指向完整路径:" Write-Host " 例如(PowerShell 临时):$env:NUGET_EXECUTABLE = '$targetPath'" - Write-Host " 或永久在 系统 属性 -> 环境变量 中添加:D:\\StudioProjects\\TeleBook\\tools\\nuget" + Write-Host " 或永久在 系统 属性 -> 环境变量 中添加:D:\\StudioProjects\\tele_book\\tools\\nuget" Write-Host "" Write-Host "然后重新运行 Flutter 构建(例如:flutter build windows 或 flutter run -d windows)。" } else { diff --git a/version.properties b/version.properties index 5cac1a8..89aa294 100644 --- a/version.properties +++ b/version.properties @@ -1,3 +1,3 @@ -VERSION_NAME=3.1.4 -VERSION_CODE=3 +VERSION_NAME=3.1.6 +VERSION_CODE=1 diff --git a/web/index.html b/web/index.html index acf30c9..43a30d8 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,7 @@ - + - + - wo_nas + tele_book - - - - - + + diff --git a/web/manifest.json b/web/manifest.json index 941a902..654758a 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,6 +1,6 @@ { - "name": "wo_nas", - "short_name": "wo_nas", + "name": "tele_book", + "short_name": "tele_book", "start_url": ".", "display": "standalone", "background_color": "#hexcode", diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index df20df8..0ee9434 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -1,23 +1,15 @@ # Project-level configuration. cmake_minimum_required(VERSION 3.14) -project(TeleBook LANGUAGES CXX) - -# Suppress CMP0175 warning from third-party plugins (e.g. flutter_inappwebview_windows) -if(POLICY CMP0175) - cmake_policy(SET CMP0175 OLD) -endif() +project(tele_book LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "TeleBook") +set(BINARY_NAME "tele_book") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(VERSION 3.14...3.25) -# 保证 CMake 允许 add_custom_command(TARGET) 使用 DEPENDS,消除 flutter 插件的警告 -cmake_policy(SET CMP0175 NEW) - # Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) @@ -69,12 +61,13 @@ include(flutter/generated_plugins.cmake) # === Installation === # Support files are copied into place next to the executable, so that it can # run in place. This is done instead of making a separate bundle (as on Linux) -# so that building and running form within Visual Studio will work. +# so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") # Make the "install" step default, as it's required to run. set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) - -set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") @@ -94,14 +87,14 @@ if(PLUGIN_BUNDLED_LIBRARIES) COMPONENT Runtime) endif() -# Copy the native assets provided by the build.dart form all packages. +# Copy the native assets provided by the build.dart from all packages. set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") install(DIRECTORY "${NATIVE_ASSETS_DIR}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) # Fully re-copy the assets directory on each build to avoid having stale files -# form a previous install. +# from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") @@ -113,21 +106,3 @@ install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) - -# 检查 NuGet 可执行文件(允许在项目 tools/nuget 目录或系统 PATH 中查找) -find_program(NUGET_EXECUTABLE - NAMES nuget.exe nuget - HINTS - "${CMAKE_SOURCE_DIR}/tools/nuget" - PATHS - ENV PATH -) - -if(NOT NUGET_EXECUTABLE) - message(FATAL_ERROR - "\nNuGet 未找到!flutter_inappwebview_windows 插件需要 NuGet。\n\ -请运行 D:\\StudioProjects\\TeleBook\\tools\\install_nuget.ps1 以下载 nuget.exe,\n\ -或按照 https://inappwebview.dev/docs/intro#setup-windows 进行手动安装。\n\n\ -下载后请将 nuget.exe 所在目录加入系统 PATH,或在环境变量中设置 NUGET_EXECUTABLE 指向 nuget.exe 的完整路径。\n" - ) -endif() diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt index c663404..903f489 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -85,7 +85,7 @@ add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list form the +# since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 165cb71..0546ea5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index d6b6ba4..9fc1eab 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -6,7 +6,7 @@ #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // -// Generated form the TEXTINCLUDE 2 resource. +// Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" @@ -89,11 +89,11 @@ BEGIN BEGIN BLOCK "040904e4" BEGIN - VALUE "CompanyName", "com.dorkytiger" "\0" + VALUE "CompanyName", "com.example" "\0" VALUE "FileDescription", "tele_book" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "tele_book" "\0" - VALUE "LegalCopyright", "Copyright (C) 2023 com.dorkytiger. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" VALUE "OriginalFilename", "tele_book.exe" "\0" VALUE "ProductName", "tele_book" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" @@ -113,7 +113,7 @@ END #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // -// Generated form the TEXTINCLUDE 3 resource. +// Generated from the TEXTINCLUDE 3 resource. // diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest index a42ea76..153653e 100644 --- a/windows/runner/runner.exe.manifest +++ b/windows/runner/runner.exe.manifest @@ -9,12 +9,6 @@ - - - - - - diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp index b2b0873..3cb7146 100644 --- a/windows/runner/utils.cpp +++ b/windows/runner/utils.cpp @@ -45,13 +45,17 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } + // First, find the length of the string with a safe upper bound (CWE-126). + // UNICODE_STRING_MAX_CHARS (32767) is the maximum length of a UNICODE_STRING. + int input_length = static_cast(wcsnlen(utf16_string, UNICODE_STRING_MAX_CHARS)); + // Now use that bounded length to determine the required buffer size. + // When an explicit length is passed, WideCharToMultiByte does not include + // the null terminator in its returned size. int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr) - -1; // remove the trailing null character - int input_length = (int)wcslen(utf16_string); + input_length, nullptr, 0, nullptr, nullptr); std::string utf8_string; - if (target_length <= 0 || target_length > utf8_string.max_size()) { + if (target_length == 0 || static_cast(target_length) > utf8_string.max_size()) { return utf8_string; } utf8_string.resize(target_length); diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp index e516426..60608d0 100644 --- a/windows/runner/win32_window.cpp +++ b/windows/runner/win32_window.cpp @@ -37,7 +37,7 @@ int Scale(int source, double scale_factor) { return static_cast(source * scale_factor); } -// Dynamically loads the |EnableNonClientDpiScaling| form the User32 module. +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. // This API is only needed for PerMonitor V1 awareness mode. void EnableFullDpiSupportIfAvailable(HWND hwnd) { HMODULE user32_module = LoadLibraryA("User32.dll"); diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h index 0e02598..e901dde 100644 --- a/windows/runner/win32_window.h +++ b/windows/runner/win32_window.h @@ -8,7 +8,7 @@ #include // A class abstraction for a high DPI-aware Win32 Window. Intended to be -// inherited form by classes that wish to specialize with custom +// inherited from by classes that wish to specialize with custom // rendering and input handling class Win32Window { public: