Repository: ComicSparks/pikapika Branch: master Commit: def75a101a65 Files: 319 Total size: 1.3 MB Directory structure: gitextract_rinrtcf6/ ├── .fvmrc ├── .github/ │ └── workflows/ │ ├── Package.core.yml │ ├── Package.yml │ ├── Release.core.yml │ └── Release.yml ├── .gitignore ├── .metadata ├── README-zh_CN.md ├── README.md ├── analysis_options.yaml ├── android/ │ ├── .gitignore │ ├── app/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── debug/ │ │ │ └── AndroidManifest.xml │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── opensource/ │ │ │ │ └── pic2acg/ │ │ │ │ └── MainActivity.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-v21/ │ │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ └── ic_launcher.xml │ │ │ ├── values/ │ │ │ │ └── styles.xml │ │ │ └── values-night/ │ │ │ └── styles.xml │ │ └── profile/ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ └── settings.gradle ├── ci/ │ ├── cmd/ │ │ ├── check_asset/ │ │ │ └── main.go │ │ ├── check_asset_core/ │ │ │ └── main.go │ │ ├── check_release/ │ │ │ └── main.go │ │ ├── send_to_community/ │ │ │ └── main.go │ │ ├── upload_asset/ │ │ │ └── main.go │ │ └── upload_asset_core/ │ │ └── main.go │ ├── commons/ │ │ ├── funcs.go │ │ └── types.go │ ├── go.mod │ ├── go.sum │ ├── linux_font.yaml │ ├── version.code.txt │ └── version.info.txt ├── ios/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── LaunchImage.imageset/ │ │ │ ├── Contents.json │ │ │ └── README.md │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ └── Runner.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings ├── lib/ │ ├── assets/ │ │ └── translations/ │ │ ├── en-US.json │ │ ├── ja-JP.json │ │ ├── ko-KR.json │ │ ├── zh-CN.json │ │ └── zh-TW.json │ ├── basic/ │ │ ├── Channels.dart │ │ ├── Common.dart │ │ ├── Cross.dart │ │ ├── Entities.dart │ │ ├── Method.dart │ │ ├── Navigator.dart │ │ ├── config/ │ │ │ ├── Address.dart │ │ │ ├── AndroidDisplayMode.dart │ │ │ ├── AndroidSecureFlag.dart │ │ │ ├── AppOrientation.dart │ │ │ ├── Authentication.dart │ │ │ ├── AutoClean.dart │ │ │ ├── AutoDeleteDownloadOnUnfavorite.dart │ │ │ ├── AutoDownloadOnFavorite.dart │ │ │ ├── AutoFullScreen.dart │ │ │ ├── AutoFullScreenOnForward.dart │ │ │ ├── CategoriesColumnCount.dart │ │ │ ├── CategoriesSort.dart │ │ │ ├── ChooserRoot.dart │ │ │ ├── ContentFailedReloadAction.dart │ │ │ ├── CopyFullName.dart │ │ │ ├── CopyFullNameTemplate.dart │ │ │ ├── CopySkipConfirm.dart │ │ │ ├── DisableAutoDownloadOnMobile.dart │ │ │ ├── DownloadAndExportPath.dart │ │ │ ├── DownloadCachePath.dart │ │ │ ├── DownloadThreadCount.dart │ │ │ ├── DragRegionLock.dart │ │ │ ├── EBookScrolling.dart │ │ │ ├── EBookScrollingRange.dart │ │ │ ├── EBookScrollingTrigger.dart │ │ │ ├── ExportPath.dart │ │ │ ├── ExportRename.dart │ │ │ ├── FullScreenAction.dart │ │ │ ├── FullScreenUI.dart │ │ │ ├── GalleryPreloadCount.dart │ │ │ ├── GestureSpeed.dart │ │ │ ├── HiddenFdIcon.dart │ │ │ ├── HiddenSearchPersion.dart │ │ │ ├── HiddenSubIcon.dart │ │ │ ├── HiddenViewed.dart │ │ │ ├── HiddenWords.dart │ │ │ ├── HideOnlineFavorite.dart │ │ │ ├── IconLoading.dart │ │ │ ├── IgnoreInfoHistory.dart │ │ │ ├── IgnoreUpgradeConfirm.dart │ │ │ ├── ImageAddress.dart │ │ │ ├── ImageFilter.dart │ │ │ ├── ImportNotice.dart │ │ │ ├── IsPro.dart │ │ │ ├── KeyboardController.dart │ │ │ ├── ListLayout.dart │ │ │ ├── LocalHistorySync.dart │ │ │ ├── NoAnimation.dart │ │ │ ├── PagerAction.dart │ │ │ ├── Platform.dart │ │ │ ├── Proxy.dart │ │ │ ├── Quality.dart │ │ │ ├── ReaderBackgroundColor.dart │ │ │ ├── ReaderDirection.dart │ │ │ ├── ReaderScrollByScreenPercentage.dart │ │ │ ├── ReaderSliderPosition.dart │ │ │ ├── ReaderTwoPageDirection.dart │ │ │ ├── ReaderType.dart │ │ │ ├── ReaderZoomScale.dart │ │ │ ├── RecommendLinks.dart │ │ │ ├── ShadowCategories.dart │ │ │ ├── ShadowCategoriesEvent.dart │ │ │ ├── ShadowCategoriesMode.dart │ │ │ ├── ShowCommentAtDownload.dart │ │ │ ├── StartupPic.dart │ │ │ ├── Themes.dart │ │ │ ├── ThreeKeepRight.dart │ │ │ ├── TimeOffsetHour.dart │ │ │ ├── TimeoutLock.dart │ │ │ ├── UseApiLoadImage.dart │ │ │ ├── UsingRightClickPop.dart │ │ │ ├── Version.dart │ │ │ ├── VolumeController.dart │ │ │ ├── VolumeNextChapter.dart │ │ │ ├── WebDav.dart │ │ │ ├── WebToonScrollMode.dart │ │ │ ├── WillPopNotice.dart │ │ │ ├── i18n.dart │ │ │ └── passed.dart │ │ ├── connect.dart │ │ ├── define.dart │ │ ├── enum/ │ │ │ ├── ErrorTypes.dart │ │ │ └── Sort.dart │ │ └── store/ │ │ └── Categories.dart │ ├── i18.dart │ ├── i18b.dart │ ├── main.dart │ ├── main_desktop.dart │ └── screens/ │ ├── AboutScreen.dart │ ├── AccessKeyReplaceScreen.dart │ ├── AccountScreen.dart │ ├── AppScreen.dart │ ├── CategoriesScreen.dart │ ├── CategoriesSortScreen.dart │ ├── CleanScreen.dart │ ├── CloseAppScreen.dart │ ├── ComicCollectionsScreen.dart │ ├── ComicInfoScreen.dart │ ├── ComicReaderScreen.dart │ ├── ComicSubscribesScreen.dart │ ├── ComicsScreen.dart │ ├── CommentScreen.dart │ ├── DesktopAuthenticationScreen.dart │ ├── DownloadConfirmScreen.dart │ ├── DownloadExportGroupScreen.dart │ ├── DownloadExportToFileScreen.dart │ ├── DownloadExportToSocketScreen.dart │ ├── DownloadExportingGroupScreen.dart │ ├── DownloadImportScreen.dart │ ├── DownloadInfoScreen.dart │ ├── DownloadListScreen.dart │ ├── DownloadOnlyImportScreen.dart │ ├── DownloadReaderScreen.dart │ ├── FavouritePaperScreen.dart │ ├── FilePhotoViewScreen.dart │ ├── ForgotPasswordScreen.dart │ ├── GameDownloadScreen.dart │ ├── GameInfoScreen.dart │ ├── GamesScreen.dart │ ├── HiddenWordsScreen.dart │ ├── ImportFromOffScreen.dart │ ├── InitScreen.dart │ ├── LocalFavoriteScreen.dart │ ├── MigrateScreen.dart │ ├── ModifyPasswordScreen.dart │ ├── NetworkSettingsScreen.dart │ ├── PkzArchiveScreen.dart │ ├── PkzComicInfoScreen.dart │ ├── PkzReaderScreen.dart │ ├── ProScreen.dart │ ├── RandomComicsScreen.dart │ ├── RankingsScreen.dart │ ├── RegisterScreen.dart │ ├── SearchAuthorScreen.dart │ ├── SearchScreen.dart │ ├── SettingsScreen.dart │ ├── SpaceScreen.dart │ ├── ThemeScreen.dart │ ├── ViewLogsScreen.dart │ ├── WebServerScreen.dart │ ├── calculator_screen.dart │ └── components/ │ ├── Avatar.dart │ ├── Badge.dart │ ├── BottomSheetInput.dart │ ├── ComicDescriptionCard.dart │ ├── ComicInfoCard.dart │ ├── ComicList.dart │ ├── ComicListBuilder.dart │ ├── ComicPager.dart │ ├── ComicTagsCard.dart │ ├── CommentItem.dart │ ├── CommentList.dart │ ├── CommentMainType.dart │ ├── Common.dart │ ├── CommonData.dart │ ├── ContentBuilder.dart │ ├── ContentError.dart │ ├── ContentLoading.dart │ ├── ContentMessage.dart │ ├── ContinueReadButton.dart │ ├── DesktopCropper.dart │ ├── DownloadComicsScreen.dart │ ├── DownloadInfoCard.dart │ ├── FitButton.dart │ ├── GameTitleCard.dart │ ├── GoDownloadSelect.dart │ ├── ImageReader.dart │ ├── Images.dart │ ├── ItemBuilder.dart │ ├── LinkToComicInfo.dart │ ├── ListView.dart │ ├── MouseAndTouchScrollBehavior.dart │ ├── NetworkSetting.dart │ ├── PkzComicInfoCard.dart │ ├── PkzImages.dart │ ├── RecommendLinksPanel.dart │ ├── Recommendation.dart │ ├── RightClickPop.dart │ ├── TimeoutLock.dart │ ├── UserProfileCard.dart │ ├── flutter_search_bar.dart │ └── gesture_zoom_box.dart ├── linux/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter/ │ │ ├── CMakeLists.txt │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ ├── main.cc │ ├── my_application.cc │ └── my_application.h ├── macos/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Podfile │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── MainMenu.xib │ │ ├── Configs/ │ │ │ ├── AppInfo.xcconfig │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ └── Runner.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ └── IDEWorkspaceChecks.plist ├── pubspec.yaml ├── scripts/ │ ├── README.md │ ├── bind-android-arm64.sh │ ├── bind-android-debug.sh │ ├── bind-ios-arm64.sh │ ├── bind-ios.sh │ ├── build-apk-arm.sh │ ├── build-apk-arm64.sh │ ├── build-apk-x64.sh │ ├── build-apk-x86.sh │ ├── build-ipa.sh │ ├── build-linux.sh │ ├── build-macos-dmg.sh │ ├── json_compairer.py │ ├── sign-apk-github-actions.sh │ ├── thin-payload.sh │ └── version.sh ├── test/ │ └── widget_test.dart └── windows/ ├── .gitignore ├── CMakeLists.txt ├── flutter/ │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake └── runner/ ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── run_loop.cpp ├── run_loop.h ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .fvmrc ================================================ { "flutter": "3.13.9" } ================================================ FILE: .github/workflows/Package.core.yml ================================================ name: Package-core on: workflow_dispatch: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GO_VERSION: "1.24" NDK_VERSION: "23.1.7779620" jobs: ci-pass: name: CI is green runs-on: ubuntu-latest needs: - build_release_assets steps: - run: exit 0 build_release_assets: name: Build and upload assets strategy: fail-fast: false matrix: config: - target: ios host: macos-latest - target: android host: ubuntu-latest runs-on: ${{ matrix.config.host }} env: TARGET: ${{ matrix.config.target }} steps: - name: Setup golang uses: actions/setup-go@v2 with: go-version: ${{ env.GO_VERSION }} - name: Cache go modules (Linux) if: matrix.config.host == 'ubuntu-latest' uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ matrix.config.host }}-go- - name: Cache go modules (macOS) if: matrix.config.host == 'macos-latest' uses: actions/cache@v3 with: path: | ~/Library/Caches/go-build ~/go/pkg/mod key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ matrix.config.host }}-go- - name: Checkout uses: actions/checkout@v3 with: ref: ${{ env.BRANCH }} - name: Checkout core uses: actions/checkout@v3 with: repository: 'niuhuan/pikapika-go-core' token: ${{ secrets.CORE_TOKEN }} path: 'go' - name: Install go mobile (mobile) if: matrix.config.target == 'ios' || matrix.config.target == 'android' run: | go install golang.org/x/mobile/cmd/gomobile@latest - name: Build (ios) if: matrix.config.target == 'ios' run: | sh scripts/bind-ios.sh - name: Setup java (Android) if: matrix.config.target == 'android' uses: actions/setup-java@v3 with: java-version: '11' distribution: 'temurin' - name: Setup android tools (Android) if: matrix.config.target == 'android' uses: android-actions/setup-android@v3 with: cmdline-tools-version: 8512546 packages: 'platform-tools platforms;android-32 build-tools;30.0.2 ndk;${{ env.NDK_VERSION}}' - name: Build (android) if: matrix.config.target == 'android' run: | export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.NDK_VERSION }} bash scripts/bind-android-debug.sh - name: Upload Asset (All) uses: actions/upload-artifact@v4 with: name: ${{ matrix.config.target }}-${{ github.run_number }} path: 'go/mobile/lib' retention-days: 3 ================================================ FILE: .github/workflows/Package.yml ================================================ name: Build on: workflow_dispatch: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ANDROID_NDK_VERSION: "23.1.7779620" GO_MOBILE_VERSION: v0.0.0-20241213221354-a87c1cf6cf46 # v0.0.0-20220722155234-aaac322e2105 jobs: ci-pass: name: CI is green runs-on: ubuntu-latest needs: - build_release_assets steps: - run: exit 0 build_release_assets: name: Build release assets strategy: fail-fast: false matrix: sources: - branch: master config: - target: windows host: windows-latest flutter_version: '2.10.3' go_version: '1.23' - target: macos host: macos-latest flutter_version: '2.10.3' go_version: '1.23' - target: linux host: ubuntu-latest flutter_version: '2.10.3' go_version: '1.23' - target: ios host: macos-latest flutter_version: '3.13.9' go_version: '1.24' - target: android-arm32 host: ubuntu-latest flutter_version: '3.13.9' go_version: '1.24' java: '11' - target: android-arm64 host: ubuntu-latest flutter_version: '3.13.9' go_version: '1.24' java: '11' - target: android-x86_64 host: ubuntu-latest flutter_version: '3.13.9' go_version: '1.24' java: '11' runs-on: ${{ matrix.config.host }} env: TARGET: ${{ matrix.config.target }} FLUTTER_VERSION: ${{ matrix.config.flutter_version }} BRANCH: ${{ matrix.sources.branch }} go_version: ${{ matrix.config.go_version }} steps: # Setup golang env and cache go module - name: Setup golang uses: actions/setup-go@v2 with: go-version: ${{ env.go_version }} - name: Cache go modules (Windows) if: matrix.config.host == 'windows-latest' uses: actions/cache@v3 with: path: | ~\AppData\Local\go-build ~\go\pkg\mod key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ matrix.config.host }}-go- - name: Cache go modules (Linux) if: matrix.config.host == 'ubuntu-latest' uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ matrix.config.host }}-go- - name: Cache go modules (macOS) if: matrix.config.host == 'macos-latest' uses: actions/cache@v3 with: path: | ~/Library/Caches/go-build ~/go/pkg/mod key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ matrix.config.host }}-go- # checkout - name: Checkout uses: actions/checkout@v3 with: ref: ${{ env.BRANCH }} # check_access - id: check_asset name: Check asset run: | echo "::set-output name=skip_build::false" # - name: Check core if: steps.check_asset.outputs.skip_build != 'true' uses: actions/checkout@v3 with: repository: 'niuhuan/pikapika-go-core' token: ${{ secrets.CORE_TOKEN }} path: 'go' - name: Setup flutter if: steps.check_asset.outputs.skip_build != 'true' uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.FLUTTER_VERSION }} architecture: x64 - name: Cache Flutter dependencies (Linux/Android) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' || matrix.config.target == 'linux' ) uses: actions/cache@v3 with: path: /opt/hostedtoolcache/flutter key: ${{ runner.os }}-flutter - name: Cache Flutter dependencies (Mac host) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'ios' || matrix.config.target == 'macos' ) uses: actions/cache@v3 with: path: /Users/runner/hostedtoolcache/flutter key: ${{ runner.os }}-flutter - name: Cache Gradle dependencies (Android) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) uses: actions/cache@v3 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- - name: Setup java (Android) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) && startsWith(matrix.config.flutter_version, '3.24.2') == false uses: actions/setup-java@v3 with: java-version: ${{ matrix.config.java }} distribution: 'temurin' - name: Setup java (Android) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) && startsWith(matrix.config.flutter_version, '3.24.2') uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - name: Setup android tools (Android) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) uses: android-actions/setup-android@v3 with: cmdline-tools-version: 8512546 packages: 'platform-tools platforms;android-32 build-tools;30.0.2 ndk;${{ env.ANDROID_NDK_VERSION }}' - name: Setup msys2 (Windows) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'windows' uses: msys2/setup-msys2@v2 with: install: gcc make - name: Install dependencies (Linux) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'linux' env: ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' run: | curl -JOL https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage chmod a+x appimagetool-x86_64.AppImage mkdir -p ${GITHUB_WORKSPACE}/bin mv appimagetool-x86_64.AppImage ${GITHUB_WORKSPACE}/bin/appimagetool echo ::add-path::${GITHUB_WORKSPACE}/bin sudo apt-get update sudo apt-get install -y libgl1-mesa-dev xorg-dev libfuse2 locate - name: Install hover (desktop) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'linux' || matrix.config.target == 'windows' || matrix.config.target == 'macos') run: | go install github.com/go-flutter-desktop/hover@latest - name: Install go mobile (mobile) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'ios' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-arm32' || matrix.config.target == 'android-x86_64' ) run: | go install golang.org/x/mobile/cmd/gomobile@${{ env.GO_MOBILE_VERSION }} - name: Set-Version (All) if: steps.check_asset.outputs.skip_build != 'true' run: | cd ci cp version.code.txt ../lib/assets/version.txt - name: Upgrade deps version (flutter2 non-mac) if: steps.check_asset.outputs.skip_build != 'true' && startsWith(matrix.config.host, 'macos-') == false && startsWith(matrix.config.flutter_version, '2') run: | sed -i "s/another_xlider: ^1.0.1+2/another_xlider: 1.0.1+2/g" pubspec.yaml sed -i "s/flutter_styled_toast: ^2.0.0/flutter_styled_toast: 2.0.0/g" pubspec.yaml sed -i "s/filesystem_picker: ^3.0.0-beta.1/filesystem_picker: 2.0.0/g" pubspec.yaml sed -i "s/file_picker: 5.2.5/file_picker: 4.6.1/g" pubspec.yaml sed -i "s/multi_select_flutter: ^4.0.0/multi_select_flutter: 4.1.2/g" pubspec.yaml sed -i "s/modal_bottom_sheet: ^3.0.0-pre/modal_bottom_sheet: 2.0.1/g" pubspec.yaml sed -i "s/Icons.energy_savings_leaf/Icons.ad_units/g" lib/screens/SettingsScreen.dart sed -i "s/gradle-7.5-bin.zip/gradle-6.7.1-all.zip/g" android/gradle/wrapper/gradle-wrapper.properties sed -i "s/com.android.tools.build:gradle:7.2.0/com.android.tools.build:gradle:4.1.0/g" android/build.gradle sed -i "s/1.7.10/1.3.50/g" android/app/build.gradle sed -i "s/fontFamilyFallback/\/\/fontFamilyFallback/g" lib/basic/config/Themes.dart sed -i "s/easy_localization: ^3.0.7+1/easy_localization: ^3.0.0/g" pubspec.yaml sed -i "s/thumbVisibility: true/isAlwaysShown: true/g" lib/basic/config/ShadowCategories.dart flutter pub get - name: Upgrade deps version (flutter2 mac) if: steps.check_asset.outputs.skip_build != 'true' && startsWith(matrix.config.host, 'macos-') && startsWith(matrix.config.flutter_version, '2') run: | brew install gnu-sed gsed -i "s/another_xlider: ^1.0.1+2/another_xlider: 1.0.1+2/g" pubspec.yaml gsed -i "s/flutter_styled_toast: ^2.0.0/flutter_styled_toast: 2.0.0/g" pubspec.yaml gsed -i "s/filesystem_picker: ^3.0.0-beta.1/filesystem_picker: 2.0.0/g" pubspec.yaml gsed -i "s/file_picker: 5.2.5/file_picker: 4.6.1/g" pubspec.yaml gsed -i "s/multi_select_flutter: ^4.0.0/multi_select_flutter: 4.1.2/g" pubspec.yaml gsed -i "s/modal_bottom_sheet: ^3.0.0-pre/modal_bottom_sheet: 2.0.1/g" pubspec.yaml gsed -i "s/Icons.energy_savings_leaf/Icons.ad_units/g" lib/screens/SettingsScreen.dart gsed -i "s/fontFamilyFallback/\/\/fontFamilyFallback/g" lib/basic/config/Themes.dart gsed -i "s/easy_localization: ^3.0.7+1/easy_localization: ^3.0.0/g" pubspec.yaml gsed -i "s/thumbVisibility: true/isAlwaysShown: true/g" lib/basic/config/ShadowCategories.dart flutter pub get - name: Build (windows) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'windows' run: | hover build windows curl -JOL https://github.com/ComicSparks/build-tools/releases/download/storage/Resource_Hacker_5.1.8.zip Expand-Archive .\Resource_Hacker_5.1.8.zip cmd /c "Resource_Hacker_5.1.8\ResourceHacker.exe" -open go\build\outputs\windows-release\pikapika.exe -save go\build\outputs\windows-release\pikapika.exe -action addskip -res go/assets/icon.ico -mask ICONGROUP,MAINICON,0 cd go\build\outputs\windows-release DEL flutter_engine.pdb DEL flutter_engine.exp DEL flutter_engine.lib Compress-Archive * ../../../../build/build.zip - name: Build (macos) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'macos' run: | hover build darwin-dmg mv go/build/outputs/darwin-dmg-release/*.dmg build/build.dmg - name: Build (linux) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'linux' run: | curl -JOL https://github.com/junmer/source-han-serif-ttf/raw/master/SubsetTTF/CN/SourceHanSerifCN-Regular.ttf mkdir -p fonts mv SourceHanSerifCN-Regular.ttf fonts/Roboto.ttf cat ci/linux_font.yaml >> pubspec.yaml hover build linux-appimage mv go/build/outputs/linux-appimage-release/*.AppImage build/build.AppImage - name: Append application-identifier (ios) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'ios' run: | /usr/libexec/PlistBuddy -c 'Add :application-identifier string opensource.pikapika' ios/Runner/Info.plist - name: Build (ios) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'ios' run: | sh scripts/build-ipa.sh - name: Build (android-arm32) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-arm32' run: | export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.ANDROID_NDK_VERSION }} sh scripts/build-apk-arm.sh - name: Build (android-arm64) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-arm64' run: | export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.ANDROID_NDK_VERSION }} sh scripts/build-apk-arm64.sh - name: Build (android-x86_64) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-x86_64' run: | export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.ANDROID_NDK_VERSION }} sh scripts/build-apk-x64.sh - name: Sign APK (Android) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) env: KEY_FILE_BASE64: ${{ secrets.KEY_FILE_BASE64 }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: | sh scripts/sign-apk-github-actions.sh - if: steps.need_build.outputs.skip_build != 'true' && matrix.config.target == 'ios' name: 'Upload Artifact (iOS)' uses: actions/upload-artifact@v4 with: name: 'nosign.ipa' path: 'build/nosign.ipa' retention-days: 3 - if: steps.need_build.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) name: 'Upload Artifact (Android)' uses: actions/upload-artifact@v4 with: name: 'app-release-${{ matrix.config.target }}.apk' path: 'build/app/outputs/flutter-apk/app-release.apk' retention-days: 3 - if: steps.need_build.outputs.skip_build != 'true' && matrix.config.target == 'linux' name: 'Upload Artifact (Linux)' uses: actions/upload-artifact@v4 with: name: 'build.AppImage' path: 'build/build.AppImage' retention-days: 3 - if: steps.need_build.outputs.skip_build != 'true' && matrix.config.target == 'macos' name: 'Upload Artifact (MacOS)' uses: actions/upload-artifact@v4 with: name: 'build.dmg' path: 'build/build.dmg' ================================================ FILE: .github/workflows/Release.core.yml ================================================ name: Release-core permissions: contents: write on: workflow_dispatch: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GO_VERSION: "1.24" GO_MOBILE_VERSION: v0.0.0-20241213221354-a87c1cf6cf46 jobs: ci-pass: name: CI is green runs-on: ubuntu-latest needs: - build_release_assets steps: - run: exit 0 build_release_assets: name: Build and upload assets strategy: fail-fast: false matrix: config: - target: ios host: macos-latest - target: android host: ubuntu-latest runs-on: ${{ matrix.config.host }} env: TARGET: ${{ matrix.config.target }} steps: - name: Setup golang uses: actions/setup-go@v2 with: go-version: ${{ env.GO_VERSION }} - name: Cache go modules (Linux) if: matrix.config.host == 'ubuntu-latest' uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ matrix.config.host }}-go- - name: Cache go modules (macOS) if: matrix.config.host == 'macos-latest' uses: actions/cache@v3 with: path: | ~/Library/Caches/go-build ~/go/pkg/mod key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ matrix.config.host }}-go- - name: Checkout uses: actions/checkout@v3 with: ref: ${{ env.BRANCH }} - id: check_asset name: Check asset run: | cd ci go run ./cmd/check_asset_core - name: Checkout core if: steps.check_asset.outputs.skip_build != 'true' uses: actions/checkout@v3 with: repository: 'niuhuan/pikapika-go-core' token: ${{ secrets.CORE_TOKEN }} path: 'go' - if: steps.check_asset.outputs.skip_build != 'true' name: Install go mobile (mobile) run: | go install golang.org/x/mobile/cmd/gomobile@${{ env.GO_MOBILE_VERSION }} - name: Build (ios) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'ios' run: | sh scripts/bind-ios.sh - name: Setup java (Android) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android' uses: actions/setup-java@v3 with: java-version: '11' distribution: 'temurin' - name: Setup android tools (Android) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android' uses: android-actions/setup-android@v3 with: cmdline-tools-version: 8512546 packages: 'platform-tools platforms;android-32 build-tools;30.0.2 ndk;23.1.7779620' - name: Build (android) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android' run: | sh scripts/bind-android-debug.sh - name: Upload Asset (All) if: steps.check_asset.outputs.skip_build != 'true' run: | zip -r core.zip go/mobile/lib cd ci go run ./cmd/upload_asset_core ================================================ FILE: .github/workflows/Release.yml ================================================ name: Release permissions: contents: write on: workflow_dispatch: inputs: skip_community_notification: description: 'skip_community_notification' type: boolean required: false default: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ANDROID_NDK_VERSION: "23.1.7779620" GO_MOBILE_VERSION: v0.0.0-20241213221354-a87c1cf6cf46 jobs: ci-pass: name: CI is green runs-on: ubuntu-latest needs: - check_release - build_release_assets - send_to_community steps: - run: exit 0 check_release: name: Check release runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: repository: ${{ github.event.inputs.repo }} ref: 'master' - uses: actions/setup-go@v2 with: go-version: ${{ env.go_version }} - name: Cache go modules uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ubuntu-latest-go-${{ hashFiles('**/go.sum') }} restore-keys: | ubuntu-latest-go- - name: Check release run: | cd ci go run ./cmd/check_release build_release_assets: name: Build release assets needs: - check_release strategy: fail-fast: false matrix: sources: - branch: master config: - target: windows host: windows-latest flutter_version: '2.10.3' go_version: '1.23' - target: macos host: macos-latest flutter_version: '2.10.3' go_version: '1.23' - target: linux host: ubuntu-latest flutter_version: '2.10.3' go_version: '1.23' - target: ios host: macos-14 flutter_version: '3.13.9' go_version: '1.24' - target: android-arm32 host: ubuntu-latest flutter_version: '3.13.9' go_version: '1.24' java: '11' - target: android-arm64 host: ubuntu-latest flutter_version: '3.13.9' go_version: '1.24' java: '11' - target: android-x86_64 host: ubuntu-latest flutter_version: '3.13.9' go_version: '1.24' java: '11' runs-on: ${{ matrix.config.host }} env: TARGET: ${{ matrix.config.target }} FLUTTER_VERSION: ${{ matrix.config.flutter_version }} BRANCH: ${{ matrix.sources.branch }} go_version: ${{ matrix.config.go_version }} steps: # Setup golang env and cache go module - name: Setup golang uses: actions/setup-go@v2 with: go-version: ${{ env.go_version }} - name: Cache go modules (Windows) if: matrix.config.host == 'windows-latest' uses: actions/cache@v3 with: path: | ~\AppData\Local\go-build ~\go\pkg\mod key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ matrix.config.host }}-go- - name: Cache go modules (Linux) if: matrix.config.host == 'ubuntu-latest' uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ matrix.config.host }}-go- - name: Cache go modules (macOS) if: startsWith(matrix.config.host, 'macos-') uses: actions/cache@v3 with: path: | ~/Library/Caches/go-build ~/go/pkg/mod key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ matrix.config.host }}-go- # checkout - name: Checkout uses: actions/checkout@v3 with: ref: ${{ env.BRANCH }} # check_access - id: check_asset name: Check asset run: | cd ci go run ./cmd/check_asset # - name: Check core if: steps.check_asset.outputs.skip_build != 'true' uses: actions/checkout@v3 with: repository: 'niuhuan/pikapika-go-core' token: ${{ secrets.CORE_TOKEN }} path: 'go' - name: Setup flutter if: steps.check_asset.outputs.skip_build != 'true' uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.FLUTTER_VERSION }} architecture: x64 - name: Check core if: steps.check_asset.outputs.skip_build != 'true' uses: actions/checkout@v3 with: repository: 'niuhuan/pikapika-go-core' token: ${{ secrets.CORE_TOKEN }} path: 'go' - name: Cache Flutter dependencies (Linux/Android) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' || matrix.config.target == 'linux' ) uses: actions/cache@v3 with: path: /opt/hostedtoolcache/flutter key: ${{ runner.os }}-flutter - name: Cache Flutter dependencies (Mac host) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'ios' || matrix.config.target == 'macos' ) uses: actions/cache@v3 with: path: /Users/runner/hostedtoolcache/flutter key: ${{ runner.os }}-flutter - name: Cache Gradle dependencies (Android) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) uses: actions/cache@v3 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- - name: Setup java (Android) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) && startsWith(matrix.config.flutter_version, '3.24.2') == false uses: actions/setup-java@v3 with: java-version: ${{ matrix.config.java }} distribution: 'temurin' - name: Setup java (Android) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) && startsWith(matrix.config.flutter_version, '3.24.2') uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - name: Setup android tools (Android) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) uses: android-actions/setup-android@v3 with: cmdline-tools-version: 8512546 packages: 'platform-tools platforms;android-32 build-tools;30.0.2 ndk;${{ env.ANDROID_NDK_VERSION }}' - name: Setup msys2 (Windows) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'windows' uses: msys2/setup-msys2@v2 with: install: gcc make - name: Install dependencies (Linux) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'linux' env: ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' run: | curl -JOL https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage chmod a+x appimagetool-x86_64.AppImage mkdir -p ${GITHUB_WORKSPACE}/bin mv appimagetool-x86_64.AppImage ${GITHUB_WORKSPACE}/bin/appimagetool echo ::add-path::${GITHUB_WORKSPACE}/bin sudo apt-get update sudo apt-get install -y libgl1-mesa-dev xorg-dev libfuse2 locate - name: Install hover (desktop) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'linux' || matrix.config.target == 'windows' || matrix.config.target == 'macos') run: | go install github.com/go-flutter-desktop/hover@latest - name: Install go mobile (mobile) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'ios' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-arm32' || matrix.config.target == 'android-x86_64' ) run: | go install golang.org/x/mobile/cmd/gomobile@${{ env.GO_MOBILE_VERSION }} - name: Set-Version (All) if: steps.check_asset.outputs.skip_build != 'true' run: | cd ci cp version.code.txt ../lib/assets/version.txt - name: Upgrade deps version (flutter2 non-mac) if: steps.check_asset.outputs.skip_build != 'true' && startsWith(matrix.config.host, 'macos-') == false && startsWith(matrix.config.flutter_version, '2') run: | sed -i "s/another_xlider: ^1.0.1+2/another_xlider: 1.0.1+2/g" pubspec.yaml sed -i "s/flutter_styled_toast: ^2.0.0/flutter_styled_toast: 2.0.0/g" pubspec.yaml sed -i "s/filesystem_picker: ^3.0.0-beta.1/filesystem_picker: 2.0.0/g" pubspec.yaml sed -i "s/file_picker: 5.2.5/file_picker: 4.6.1/g" pubspec.yaml sed -i "s/multi_select_flutter: ^4.0.0/multi_select_flutter: 4.1.2/g" pubspec.yaml sed -i "s/modal_bottom_sheet: ^3.0.0-pre/modal_bottom_sheet: 2.0.1/g" pubspec.yaml sed -i "s/Icons.energy_savings_leaf/Icons.ad_units/g" lib/screens/SettingsScreen.dart sed -i "s/gradle-7.5-bin.zip/gradle-6.7.1-all.zip/g" android/gradle/wrapper/gradle-wrapper.properties sed -i "s/com.android.tools.build:gradle:7.2.0/com.android.tools.build:gradle:4.1.0/g" android/build.gradle sed -i "s/1.7.10/1.3.50/g" android/app/build.gradle sed -i "s/fontFamilyFallback/\/\/fontFamilyFallback/g" lib/basic/config/Themes.dart sed -i "s/easy_localization: ^3.0.7+1/easy_localization: ^3.0.0/g" pubspec.yaml sed -i "s/thumbVisibility: true/isAlwaysShown: true/g" lib/basic/config/ShadowCategories.dart flutter pub get - name: Upgrade deps version (flutter2 mac) if: steps.check_asset.outputs.skip_build != 'true' && startsWith(matrix.config.host, 'macos-') && startsWith(matrix.config.flutter_version, '2') run: | brew install gnu-sed gsed -i "s/another_xlider: ^1.0.1+2/another_xlider: 1.0.1+2/g" pubspec.yaml gsed -i "s/flutter_styled_toast: ^2.0.0/flutter_styled_toast: 2.0.0/g" pubspec.yaml gsed -i "s/filesystem_picker: ^3.0.0-beta.1/filesystem_picker: 2.0.0/g" pubspec.yaml gsed -i "s/file_picker: 5.2.5/file_picker: 4.6.1/g" pubspec.yaml gsed -i "s/multi_select_flutter: ^4.0.0/multi_select_flutter: 4.1.2/g" pubspec.yaml gsed -i "s/modal_bottom_sheet: ^3.0.0-pre/modal_bottom_sheet: 2.0.1/g" pubspec.yaml gsed -i "s/Icons.energy_savings_leaf/Icons.ad_units/g" lib/screens/SettingsScreen.dart gsed -i "s/fontFamilyFallback/\/\/fontFamilyFallback/g" lib/basic/config/Themes.dart gsed -i "s/easy_localization: ^3.0.7+1/easy_localization: ^3.0.0/g" pubspec.yaml gsed -i "s/thumbVisibility: true/isAlwaysShown: true/g" lib/basic/config/ShadowCategories.dart flutter pub get - name: Build (windows) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'windows' run: | hover build windows curl -JOL https://github.com/ComicSparks/build-tools/releases/download/storage/Resource_Hacker_5.1.8.zip Expand-Archive .\Resource_Hacker_5.1.8.zip cmd /c "Resource_Hacker_5.1.8\ResourceHacker.exe" -open go\build\outputs\windows-release\pikapika.exe -save go\build\outputs\windows-release\pikapika.exe -action addskip -res go/assets/icon.ico -mask ICONGROUP,MAINICON,0 cd go\build\outputs\windows-release DEL flutter_engine.pdb DEL flutter_engine.exp DEL flutter_engine.lib Compress-Archive * ../../../../build/build.zip - name: Build (macos) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'macos' run: | hover build darwin-dmg mv go/build/outputs/darwin-dmg-release/*.dmg build/build.dmg - name: Build (linux) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'linux' run: | curl -JOL https://github.com/junmer/source-han-serif-ttf/raw/master/SubsetTTF/CN/SourceHanSerifCN-Regular.ttf mkdir -p fonts mv SourceHanSerifCN-Regular.ttf fonts/Roboto.ttf cat ci/linux_font.yaml >> pubspec.yaml hover build linux-appimage mv go/build/outputs/linux-appimage-release/*.AppImage build/build.AppImage - name: Append application-identifier (ios) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'ios' run: | /usr/libexec/PlistBuddy -c 'Add :application-identifier string opensource.pikapika' ios/Runner/Info.plist - name: Build (ios) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'ios' run: | sh scripts/build-ipa.sh - name: Build (android-arm32) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-arm32' run: | export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.ANDROID_NDK_VERSION }} sh scripts/build-apk-arm.sh - name: Build (android-arm64) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-arm64' run: | export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.ANDROID_NDK_VERSION }} sh scripts/build-apk-arm64.sh - name: Build (android-x86_64) if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-x86_64' run: | export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.ANDROID_NDK_VERSION }} sh scripts/build-apk-x64.sh - name: Sign APK (Android) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) env: KEY_FILE_BASE64: ${{ secrets.KEY_FILE_BASE64 }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: | sh scripts/sign-apk-github-actions.sh - name: Upload Asset (All) if: steps.check_asset.outputs.skip_build != 'true' run: | cd ci go run ./cmd/upload_asset send_to_community: if: github.event.inputs.skip_community_notification != 'true' needs: - check_release - build_release_assets name: Send message to community runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: repository: ${{ github.event.inputs.repo }} ref: 'master' - uses: actions/setup-go@v2 with: go-version: ${{ env.go_version }} - name: Cache go modules uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Send to community env: TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }} TG_CHAT_IDS: ${{ secrets.TG_CHAT_IDS }} DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} DISCORD_CHAT_IDS: ${{ secrets.DISCORD_CHAT_IDS }} run: | cd ci go run ./cmd/send_to_community ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ # Web related lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release # PROJECT /go/mobile/lib/*.aar /go/mobile/lib/*.jar /go/mobile/lib/*.framework/ /go/mobile/lib/*.xcframework/ /go/vendor/ ios/build/ # IDE *.iml .vscode/ # APP /lib/assets/version.txt go.work # FVM Version Cache .fvm/ tmp/ .tmp/ ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: f4abaa0735eba4dfd8f33f73363911d63931fe03 channel: stable project_type: app ================================================ FILE: README-zh_CN.md ================================================

Pikapika [![license](https://img.shields.io/github/license/ComicSparks/pikapika)](https://raw.githubusercontent.com/ComicSparks/pikapika/master/LICENSE) [![releases](https://img.shields.io/github/v/release/ComicSparks/pikapika)](https://github.com/ComicSparks/pikapika/releases)

- 美观易用且无广告的漫画客户端, 能运行在Windows/MacOS/Linux/Android/IOS中。 - 此APP内容存在限制级别内容(例如 露骨/血腥/暴力/吸毒),18岁以下的用户需在监护人陪同下使用,并请您在遵守当地法律法规。 - 您的star和issue是对开发者的莫大鼓励, 可以源仓库下载最新的源码/安装包, 表示支持/提出建议。 - 源仓库地址 [https://github.com/ComicSparks/pikapika](https://github.com/ComicSparks/pikapika) ## 界面 / 功能 ![阅读器](images/reader.png) ### 分流 VPN->代理->分流, 这三个功能如果同时设置, 您会在您手机的VPN上访问代理, 使用代理请求分流服务器。 ### 漫画分类/搜索 ![分类](images/categories_screen.png) ![列表](images/comic_list.png) ### 漫画阅读/下载/导入/导出 您可以导出任意已经完成的下载到zip, 从另外一台设备导入。 导出的zip解压后可以直接使用其中的HTML进行阅读 ![导出下载](images/exporting.png) ![HTML预览](images/exporting2.png) ### 游戏 ![games](images/games.png) ![game](images/game.png) ## 特性 - [x] 用户 - [x] 登录 / 注册 / 获取个人信息 / 自动打卡 - [x] 修改密码 / 修改签名 / 修改头像 - [x] 漫画 - [x] 分类 / 搜索 / 随机本子 / 看此本子的也在看 / 排行榜 - [x] 在分类中搜索 / 按 "分类 / 标签 / 创建人 / 汉化组" 检索 - [x] 漫画详情 / 章节 / 看图 / 将图片保存到相册 - [x] 收藏 / 喜欢 - [x] 获取评论 / 评论 / 评论回复 (社区评论后无法删除, 请谨慎使用) - [x] 更新提示 - [x] 游戏 - [x] 列表 / 详情 / 无广告下载 - [x] 下载 - [x] 导入导出 / 无线共享 / 移动设备与PC设备传输 - [x] 导出到加密的归档文件 / 直接观看加密的归档文件 - [ ] 聊天室 - [x] 缓存 / 自动清理 - [x] 设备支持 - [x] 移动端 - [x] 文件关联 - [x] 自定义超链接 - [x] 安卓 - [x] 高刷新频率屏幕适配 (90/120/144... Hz) - [x] 安卓10以上随系统进入深色/夜间模式 ## 其他说明 数据资料存储位置 - ios/android : 程序自身数据目录中, 删除就会清理 - windows : 程序同一目录中data文件夹下 - macos : ~/Library/Application\ Support/pikapika - linux : ~/.pikapika ## 技术架构 ### 多平台适配 这个应用程序使用golang和dart(flutter)作为主要语言, 可以兼容Windows, linux, MacOS, Android, IOS 使用了不同的框架桥接到桌面和移动平台上 - go-flutter => Windows / MacOS / Linux - gomobile => Android / IOS ![平台](images/platforms.png) ### 构建环境 (桌面端/移动端) - [golang](https://golang.org/) (1.17/1.18) - [flutter](https://flutter.dev/) (2.10.3/3.7.3) ## 请您遵守使用规则 软件副本分发以及代码使用规则 - 本软件的代码在未经允许的情况下可以自用但不允许释放任何releases, 个人或企业不可用于商业用途, 不可上架任何商店。 - 不要在任何其他 **二次元软件** 的 **聊天社区** 或 **开发社区** 内, 发布有关本软件的链接或信息, 对于观点不同产生的分歧作者不站队任何立场。 - 不要发送本软件安装包到 **任何社区内** , 不要将APK/IPA/ZIP/DMG发送包括任何聊天软件内的群聊功能。 请使用Github中提供的Releases页面的链接。 - 对本仓库的fork需要保留本仓库的链接, 以引导用户在主要仓库进行讨论。 责任声明 - 作者仅分享编程技术, 不分发软件, 不对分发软件承担任何后果。 因传播载造成的法律问题或纠纷, 需行为人自行承担, 请您遵守当地法以及副本接受方(社区或人)所在地区的法律。 ================================================ FILE: README.md ================================================

Pikapika [![license](https://img.shields.io/github/license/ComicSparks/pikapika)](https://raw.githubusercontent.com/ComicSparks/pikapika/master/LICENSE) [![releases](https://img.shields.io/github/v/release/ComicSparks/pikapika)](https://github.com/ComicSparks/pikapika/releases)

- A visually appealing, easy-to-use, ad-free manga client that runs on Windows/MacOS/Linux/Android/iOS. - This app contains restricted content (such as explicit, gory, violent, drug-related scenes). Users under 18 should use it under parental supervision, and please comply with local laws and regulations. - Your stars and issues are a great encouragement to the developers. You can download the latest source code/installation package from the source repository to show support/offer suggestions. - Source Repository URL is [https://github.com/ComicSparks/pikapika](https://github.com/ComicSparks/pikapika) ## Interface / Functions ![reader](images/reader.png) ### Traffic Diversion VPN -> Proxy -> Traffic Diversion: If these three functions are set simultaneously, you will access the proxy through the VPN on your phone, using the proxy to request the traffic diversion server. ### Comic categories/search Search comics in categories ### Comic reader/download/imports/exports You can export any completed downloads to a zip file and import it from another device. After extracting the exported zip, you can directly use the HTML files within for reading. ### Games You can download games without ads. ## Features - [x] Users - [x] Login / Register / Get personal information / Auto check-in - [x] Change password / Signature / Avatar - [x] Comics - [x] Categories / Search / Random comic / Also reading this comic / Rankings - [x] Search in categories / Search by "category / tag / creator / translation group" - [x] Comic details / Chapters / View images / Save images to the album - [x] Favorites / Likes - [x] Get comments / Comment / Comment reply (comments in the community cannot be deleted, please use with caution) - [x] Update notification - [x] Games - [x] List / Details / Ad-free download - [x] Downloads - [x] Import/export / Wireless sharing / Transfer between mobile and PC devices - [x] Export to encrypted archive file / Directly view encrypted archive file - [ ] Chat room - [x] Cache / Auto clean - [x] Device support - [x] Mobile - [x] File association - [x] Custom hyperlinks - [x] Android - [x] High refresh rate screen adaptation (90/120/144... Hz) - [x] Android 10 and above automatically switch to dark/night mode with the system ## Other tips Data storage location - ios/android: In the program's own data directory, deleting it will clear it - windows: In the data folder in the same directory as the program - macos: ~/Library/Application\ Support/pikapika - linux: ~/.pikapika ## Technology Stack ### Multi-platform adaptation This application uses golang and dart (flutter) as the main languages and is compatible with Windows, Linux, MacOS, Android, and iOS. Different frameworks are used to bridge to desktop and mobile platforms - go-flutter => Windows / MacOS / Linux - gomobile => Android / iOS ![platforms](images/platforms.png) ### Build environment (Desktop/Mobile) - [golang](https://golang.org/) (1.17/1.18) - [flutter](https://flutter.dev/) (2.10.3/3.7.3) ## Please follow the usage rules. Software copy distribution and code usage rules - The code of this software can be used for personal use without permission, but no releases are allowed, and it cannot be used for commercial purposes by individuals or companies, nor can it be put on any store. - Do not post links or information about this software in any **two-dimensional** chat community or development community. The author does not take any stance on any views that differ. - Do not send the software installation package to **any community**, and do not send APK/IPA/ZIP/DMG to any group chat function in any chat software. Please use the link provided on the Releases page in Github. - Forks of this repository must retain the link to this repository to guide users to discuss in the main repository. Disclaimer - The author only shares programming technology and does not distribute software. The person who spreads the content is responsible for any legal issues or disputes caused by the spread. Please comply with local laws and the laws of the recipient (community or individual) region. ================================================ FILE: analysis_options.yaml ================================================ include: package:flutter_lints/flutter.yaml linter: rules: avoid_print: false unnecessary_this: false file_names: false constant_identifier_names: false no_logic_in_create_state: false ================================================ FILE: android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties ================================================ FILE: android/app/build.gradle ================================================ def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion 33 // flutter.compileSdkVersion // ndkVersion flutter.ndkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } 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 "opensource.pic2acg" // 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 21 // flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } 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.debug } } } flutter { source '../..' } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' implementation fileTree(dir: "../../go/mobile/lib", include: ["*.jar", "*.aar"]) } ================================================ FILE: android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/kotlin/opensource/pic2acg/MainActivity.kt ================================================ package opensource.pic2acg import android.content.ContentValues import android.graphics.Bitmap import android.graphics.BitmapFactory import android.hardware.biometrics.BiometricPrompt import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.os.* import android.provider.MediaStore import android.util.Log import android.view.Display import android.view.KeyEvent import android.view.WindowManager import androidx.annotation.NonNull import androidx.annotation.RequiresApi import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.sync.Mutex import mobile.Mobile import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.nio.file.Files import java.util.concurrent.Executors import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit class MainActivity : FlutterActivity() { // 为什么换成换成线程池而不继续使用携程 : 下载图片速度慢会占满携程造成拥堵, 接口无法请求 private val pool = Executors.newCachedThreadPool { runnable -> Thread(runnable).also { it.isDaemon = true } } private val uiThreadHandler = Handler(Looper.getMainLooper()) private val scope = CoroutineScope(newSingleThreadContext("worker-scope")) private val notImplementedToken = Any() private fun MethodChannel.Result.withCoroutine(exec: () -> Any?) { pool.submit { try { val data = exec() uiThreadHandler.post { when (data) { notImplementedToken -> { notImplemented() } is Unit, null -> { success(null) } else -> { success(data) } } } } catch (e: Exception) { Log.e("Method", "Exception", e) uiThreadHandler.post { error("", e.message, "") } } } } @RequiresApi(Build.VERSION_CODES.KITKAT) override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) Mobile.initApplication(androidDataLocal()) // Method Channel MethodChannel( flutterEngine.dartExecutor.binaryMessenger, "method" ).setMethodCallHandler { call, result -> result.withCoroutine { when (call.method) { "flatInvoke" -> { Mobile.flatInvoke( call.argument("method")!!, call.argument("params")!! ) } "androidSaveFileToImage" -> { saveImage(call.argument("path")!!) } "androidGetModes" -> { modes() } "androidSetMode" -> { setMode(call.argument("mode")!!) } "androidGetVersion" -> Build.VERSION.SDK_INT // 现在的文件储存路径, 默认路径返回空字符串 "" "dataLocal" -> androidDataLocal() // 迁移到那个地方, 如果是空字符串则迁移会默认位置 "migrate" -> androidMigrate(call.argument("path")!!) // 获取可以迁移数据地址 "androidGetExtendDirs" -> androidGetExtendDirs() "androidSecureFlag" -> androidSecureFlag(call.argument("flag")!!) "verifyAuthentication" -> auth() "androidStorageRoot" -> storageRoot() "androidDefaultExportsDir" -> androidDefaultExportsDir().absolutePath "androidMkdirs" -> androidMkdirs( call.arguments() ?: throw Exception("need arg") ) else -> { notImplementedToken } } } } MethodChannel( flutterEngine.dartExecutor.binaryMessenger, "network" ).setMethodCallHandler { call, result -> result.withCoroutine { when (call.method) { "getNetworkType" -> getNetworkType() "getIsMobile" -> isMobileNetwork() else -> notImplementedToken } } } // val eventMutex = Mutex() var eventSink: EventChannel.EventSink? = null EventChannel(flutterEngine.dartExecutor.binaryMessenger, "flatEvent") .setStreamHandler(object : EventChannel.StreamHandler { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { events?.let { events -> scope.launch { eventMutex.lock() eventSink = events eventMutex.unlock() } } } override fun onCancel(arguments: Any?) { scope.launch { eventMutex.lock() eventSink = null eventMutex.unlock() } } }) Mobile.eventNotify { message -> scope.launch { eventMutex.lock() try { eventSink?.let { uiThreadHandler.post { it.success(message) } } } finally { eventMutex.unlock() } } } // EventChannel(flutterEngine.dartExecutor.binaryMessenger, "volume_button") .setStreamHandler(volumeStreamHandler) } private fun androidDataLocal(): String { val localFile = File(context!!.filesDir.absolutePath, "data.local") if (localFile.exists()) { val path = String(FileInputStream(localFile).use { it.readBytes() }) if (File(path).isDirectory) { return path } } return context!!.filesDir.absolutePath } private fun androidGetExtendDirs(): String { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { val result = context!!.getExternalFilesDirs("")?.toMutableList()?.also { it.add(context!!.filesDir.absoluteFile) }?.joinToString("|") if (result != null) { return result } } throw Exception("System version too low") } private fun androidMigrate(path: String) { val current = androidDataLocal() if (current == path) { return } // 删除位置配置文件 if (File(current, "data.local").exists()) { File(current, "data.local").delete() } // 目标位置文件夹不存在就创建,存在则清理 val target = File(path) if (!target.exists()) { target.mkdirs() } target.listFiles().forEach { delete(it) } // 移动所有文件夹 File(current).listFiles().forEach { move(it, File(target, it.name)) } val localFile = File(context!!.filesDir.absolutePath, "data.local") if (path == context!!.filesDir.absolutePath) { localFile.delete() } else { FileOutputStream(localFile).use { it.write(path.toByteArray()) } } } private fun delete(f: File) { f.delete() } private fun move(f: File, t: File) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (f.isDirectory) { Files.createDirectories(t.toPath()) f.listFiles().forEach { move(it, File(t, it.name)) } Files.delete(f.toPath()) } else { Files.move(f.toPath(), t.toPath()) } } else { if (f.isDirectory) { t.mkdirs() f.listFiles().forEach { move(it, File(t, it.name)) } f.delete() } else { FileOutputStream(t).use { o -> FileInputStream(f).use { i -> o.write(i.readBytes()) } } f.delete() } } } // save_image private fun saveImage(path: String) { BitmapFactory.decodeFile(path)?.let { bitmap -> val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, System.currentTimeMillis().toString()) put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //this one put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES) put(MediaStore.MediaColumns.IS_PENDING, 1) } } contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) ?.let { uri -> contentResolver.openOutputStream(uri)?.use { fos -> bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //this one contentValues.clear() contentValues.put(MediaStore.Video.Media.IS_PENDING, 0) contentResolver.update(uri, contentValues, null, null) } } } } // fps mods private fun mixDisplay(): Display? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { display?.let { return it } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { windowManager.defaultDisplay?.let { return it } } return null } private fun modes(): List { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mixDisplay()?.let { display -> return display.supportedModes.map { mode -> mode.toString() } } } return ArrayList() } private fun setMode(string: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mixDisplay()?.let { display -> if (string == "") { uiThreadHandler.post { window.attributes = window.attributes.also { attr -> attr.preferredDisplayModeId = 0 } } return } return display.supportedModes.forEach { mode -> if (mode.toString() == string) { uiThreadHandler.post { window.attributes = window.attributes.also { attr -> attr.preferredDisplayModeId = mode.modeId } } return } } } } } // volume_buttons private var volumeEvents: EventChannel.EventSink? = null private val volumeStreamHandler = object : EventChannel.StreamHandler { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { volumeEvents = events } override fun onCancel(arguments: Any?) { volumeEvents = null } } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { volumeEvents?.let { if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { uiThreadHandler.post { it.success("DOWN") } return true } if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { uiThreadHandler.post { it.success("UP") } return true } } return super.onKeyDown(keyCode, event) } private fun androidSecureFlag(flag: Boolean) { uiThreadHandler.post { if (flag) { window.setFlags( WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE ) } else { window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } } } // withCoroutine -> queue private fun auth(): Boolean { var queue = LinkedBlockingQueue() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { var mBiometricPrompt = BiometricPrompt.Builder(this) .setTitle("验证身份") .setDescription("需要验证您的身份") .setNegativeButton( "取消", mainExecutor ) { _, _ -> queue.add(false) } .build() var mCancellationSignal = CancellationSignal() mCancellationSignal.setOnCancelListener { queue.add(false) } var mAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { super.onAuthenticationError(errorCode, errString) queue.add(false) } override fun onAuthenticationFailed() { super.onAuthenticationFailed() queue.add(false) } override fun onAuthenticationSucceeded(result1: BiometricPrompt.AuthenticationResult?) { super.onAuthenticationSucceeded(result1) queue.add(true) } } mBiometricPrompt.authenticate( mCancellationSignal, mainExecutor, mAuthenticationCallback ) } else { queue.add(false) } return queue.poll(5, TimeUnit.MINUTES) ?: false } fun storageRoot(): String { return Environment.getExternalStorageDirectory().absolutePath } private fun downloadsDir(): File { return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) ?: throw java.lang.IllegalStateException() } private fun defaultAppDir(): File { return File(downloadsDir(), "pic2acg") } private fun androidDefaultExportsDir(): File { return File(defaultAppDir(), "exports") } private fun androidMkdirs(path: String) { val dir = File(path) if (!dir.exists()) { dir.mkdirs() } } private fun getNetworkType(): String { val ctx = context ?: return "none" val cm = ctx.getSystemService(ConnectivityManager::class.java) ?: return "none" val active = cm.activeNetwork ?: return "none" val caps = cm.getNetworkCapabilities(active) ?: return "none" return when { caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> "mobile" caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "wifi" caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> "ethernet" caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> findNonVpnTransport(cm) ?: "vpn" else -> "other" } } private fun isMobileNetwork(): Boolean { val ctx = context ?: return false val cm = ctx.getSystemService(ConnectivityManager::class.java) ?: return false val active = cm.activeNetwork ?: return false val caps = cm.getNetworkCapabilities(active) ?: return false if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { return true } if (caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { return findNonVpnTransport(cm) == "mobile" } return false } private fun findNonVpnTransport(cm: ConnectivityManager): String? { for (network in cm.allNetworks) { val caps = cm.getNetworkCapabilities(network) ?: continue if (!caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { continue } if (caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { continue } if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { return "mobile" } if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { return "wifi" } if (caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { return "ethernet" } } return null } } ================================================ FILE: android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: android/build.gradle ================================================ buildscript { ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:7.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() mavenCentral() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } tasks.register("clean", Delete) { delete rootProject.buildDir } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ #Fri Oct 29 09:53:43 CST 2021 distributionBase=GRADLE_USER_HOME distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true ================================================ FILE: android/settings.gradle ================================================ include ':app' def localPropertiesFile = new File(rootProject.projectDir, "local.properties") def properties = new Properties() assert localPropertiesFile.exists() localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" ================================================ FILE: ci/cmd/check_asset/main.go ================================================ package main import ( "ci/commons" "encoding/json" "fmt" "io/ioutil" "net/http" "os" ) func main() { // get ghToken ghToken := os.Getenv("GITHUB_TOKEN") if ghToken == "" { println("Env ${GITHUB_TOKEN} is not set") os.Exit(1) } // get version version := commons.LoadVersion() // get TARGET target := os.Getenv("TARGET") if target == "" { println("Env ${TARGET} is not set") os.Exit(1) } // get FLUTTER_VERSION flutterVersion := os.Getenv("FLUTTER_VERSION") if target == "" { println("Env ${FLUTTER_VERSION} is not set") os.Exit(1) } // get BRANCH branch := os.Getenv("BRANCH") if target == "" { println("Env ${BRANCH} is not set") os.Exit(1) } // var releaseFileName = commons.AssetName(version, flutterVersion, target, branch) // get version githubRepository := os.Getenv("GITHUB_REPOSITORY") if githubRepository == "" { println("Env ${GITHUB_REPOSITORY} is not set") os.Exit(1) } getReleaseRequest, err := http.NewRequest( "GET", fmt.Sprintf("https://api.github.com/repos/%v/releases/tags/%v", githubRepository, version.Code), nil, ) if err != nil { panic(err) } getReleaseRequest.Header.Set("User-Agent", commons.Ua) getReleaseRequest.Header.Set("Authorization", "token "+ghToken) getReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest) if err != nil { panic(err) } defer getReleaseResponse.Body.Close() if getReleaseResponse.StatusCode == 404 { panic("NOT FOUND RELEASE") } buff, err := ioutil.ReadAll(getReleaseResponse.Body) if err != nil { panic(err) } var release commons.Release err = json.Unmarshal(buff, &release) if err != nil { println(string(buff)) panic(err) } for _, asset := range release.Assets { if asset.Name == releaseFileName { println("::set-output name=skip_build::true") os.Exit(0) } } print("::set-output name=skip_build::false") } ================================================ FILE: ci/cmd/check_asset_core/main.go ================================================ package main import ( "ci/commons" "encoding/json" "fmt" "io/ioutil" "net/http" "os" ) func main() { // get ghToken ghToken := os.Getenv("GITHUB_TOKEN") if ghToken == "" { println("Env ${GITHUB_TOKEN} is not set") os.Exit(1) } // get version version := commons.LoadVersion() // get TARGET target := os.Getenv("TARGET") if target == "" { println("Env ${TARGET} is not set") os.Exit(1) } // var releaseFileName = fmt.Sprintf("core-%v-%v.zip", version.Code, target) // get version githubRepository := os.Getenv("GITHUB_REPOSITORY") if githubRepository == "" { println("Env ${GITHUB_REPOSITORY} is not set") os.Exit(1) } getReleaseRequest, err := http.NewRequest( "GET", fmt.Sprintf("https://api.github.com/repos/%v/releases/tags/%v", githubRepository, version.Code), nil, ) if err != nil { panic(err) } getReleaseRequest.Header.Set("User-Agent", commons.Ua) getReleaseRequest.Header.Set("Authorization", "token "+ghToken) getReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest) if err != nil { panic(err) } defer getReleaseResponse.Body.Close() if getReleaseResponse.StatusCode == 404 { panic("NOT FOUND RELEASE") } buff, err := ioutil.ReadAll(getReleaseResponse.Body) if err != nil { panic(err) } var release commons.Release err = json.Unmarshal(buff, &release) if err != nil { println(string(buff)) panic(err) } for _, asset := range release.Assets { if asset.Name == releaseFileName { println("::set-output name=skip_build::true") os.Exit(0) } } print("::set-output name=skip_build::false") } ================================================ FILE: ci/cmd/check_release/main.go ================================================ package main import ( "bytes" "ci/commons" "encoding/json" "fmt" "io/ioutil" "net/http" "os" ) func main() { // get ghToken ghToken := os.Getenv("GITHUB_TOKEN") if ghToken == "" { println("Env ${GITHUB_TOKEN} is not set") os.Exit(1) } // get version version := commons.LoadVersion() // get version githubRepository := os.Getenv("GITHUB_REPOSITORY") if githubRepository == "" { println("Env ${GITHUB_REPOSITORY} is not set") os.Exit(1) } getReleaseRequest, err := http.NewRequest( "GET", fmt.Sprintf("https://api.github.com/repos/%v/releases/tags/%v", githubRepository, version.Code), nil, ) if err != nil { panic(nil) } getReleaseRequest.Header.Set("User-Agent", commons.Ua) getReleaseRequest.Header.Set("Authorization", "token "+ghToken) getReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest) if err != nil { panic(nil) } defer getReleaseResponse.Body.Close() if getReleaseResponse.StatusCode == 404 { url := fmt.Sprintf("https://api.github.com/repos/%v/releases", githubRepository) body := map[string]interface{}{ "tag_name": version.Code, "target_commitish": commons.MainBranch, "name": version.Code, "body": version.Info, } var buff []byte buff, err = json.Marshal(&body) if err != nil { panic(err) } var createReleaseRequest *http.Request createReleaseRequest, err = http.NewRequest("POST", url, bytes.NewBuffer(buff)) if err != nil { panic(nil) } createReleaseRequest.Header.Set("User-Agent", commons.Ua) createReleaseRequest.Header.Set("Authorization", "token "+ghToken) var createReleaseResponse *http.Response createReleaseResponse, err = http.DefaultClient.Do(createReleaseRequest) if err != nil { panic(nil) } defer createReleaseResponse.Body.Close() if createReleaseResponse.StatusCode != 201 { buff, err = ioutil.ReadAll(createReleaseResponse.Body) if err != nil { panic(err) } println(string(buff)) panic("NOT 201") } } } ================================================ FILE: ci/cmd/send_to_community/main.go ================================================ package main import ( "ci/commons" "context" "encoding/json" "fmt" "io/ioutil" "log" "os" "strings" "github.com/andersfylling/disgord" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) func main() { // get version var version commons.Version codeFile, err := ioutil.ReadFile("version.code.txt") if err != nil { panic(err) } version.Code = strings.TrimSpace(string(codeFile)) infoFile, err := ioutil.ReadFile("version.info.txt") if err != nil { panic(err) } version.Info = strings.TrimSpace(string(infoFile)) // message githubRepository := os.Getenv("GITHUB_REPOSITORY") if githubRepository == "" { println("Env ${GITHUB_REPOSITORY} is not set") os.Exit(1) } var message = fmt.Sprintf( "%v 版本 %v 发布! \n\n"+ "更新内容:\n"+ "%v\n\n"+ "https://github.com/%v/%v/releases/tag/%v", githubRepository, version.Code, version.Info, githubRepository, githubRepository, version.Code, ) // get accounts tgToken := os.Getenv("TG_BOT_TOKEN") tgChatIdsStr := os.Getenv("TG_CHAT_IDS") discordToken := os.Getenv("DISCORD_BOT_TOKEN") discordChatIdsStr := os.Getenv("DISCORD_CHAT_IDS") if tgToken != "" && tgChatIdsStr != "" { var tgChatIds []int64 json.Unmarshal([]byte(tgChatIdsStr), &tgChatIds) if len(tgChatIds) > 0 { sendMessageToTg(tgToken, tgChatIds, message) } } if discordToken != "" && discordChatIdsStr != "" { var discordChatIds []uint64 json.Unmarshal([]byte(discordChatIdsStr), &discordChatIds) if len(discordChatIds) > 0 { sendMessageToDiscord(discordToken, discordChatIds, message) } } } func sendMessageToTg(token string, ids []int64, message string) { bot, err := tgbotapi.NewBotAPI(token) if err != nil { log.Panic(err) } for _, id := range ids { msg := tgbotapi.NewMessage(id, message) _, err = bot.Send(msg) if err != nil { fmt.Sprintf("Send message to tg chat : %v (error : %v)", id, err.Error()) } else { fmt.Sprintf("Send message to tg chat : %v (success)", id) } } } func sendMessageToDiscord(token string, ids []uint64, message string) { client, err := disgord.NewClient(context.Background(), disgord.Config{ BotToken: token, }) if err != nil { fmt.Sprintf("discord login failed : %v", err.Error()) return } for _, id := range ids { _, err = client.SendMsg(disgord.Snowflake(id), message) if err != nil { fmt.Sprintf("Send message to tg chat : %v (error : %v)", id, err.Error()) } else { fmt.Sprintf("Send message to tg chat : %v (success)", id) } } } ================================================ FILE: ci/cmd/upload_asset/main.go ================================================ package main import ( "ci/commons" "encoding/json" "fmt" "io/ioutil" "net/http" "os" "path" ) func main() { // get ghToken ghToken := os.Getenv("GITHUB_TOKEN") if ghToken == "" { println("Env ${GITHUB_TOKEN} is not set") os.Exit(1) } // get version version := commons.LoadVersion() // get TARGET target := os.Getenv("TARGET") if target == "" { println("Env ${TARGET} is not set") os.Exit(1) } // get FLUTTER_VERSION flutterVersion := os.Getenv("FLUTTER_VERSION") if target == "" { println("Env ${FLUTTER_VERSION} is not set") os.Exit(1) } // get BRANCH branch := os.Getenv("BRANCH") if target == "" { println("Env ${BRANCH} is not set") os.Exit(1) } // var releaseFileName = commons.AssetName(version, flutterVersion, target, branch) // var releaseFilePath string var contentType string var contentLength int64 switch target { case "macos": releaseFilePath = "build/build.dmg" contentType = "application/octet-stream" case "ios": releaseFilePath = "build/nosign.ipa" contentType = "application/octet-stream" case "windows": releaseFilePath = "build/build.zip" contentType = "application/octet-stream" case "linux": releaseFilePath = "build/build.AppImage" contentType = "application/octet-stream" case "android-arm32": releaseFilePath = "build/app/outputs/flutter-apk/app-release.apk" contentType = "application/octet-stream" case "android-arm64": releaseFilePath = "build/app/outputs/flutter-apk/app-release.apk" contentType = "application/octet-stream" case "android-x86_64": releaseFilePath = "build/app/outputs/flutter-apk/app-release.apk" contentType = "application/octet-stream" } releaseFilePath = path.Join("..", releaseFilePath) info, err := os.Stat(releaseFilePath) if err != nil { panic(err) } contentLength = info.Size() if contentLength == 166 { panic("NOT FOUND RELEASE FILE") } // get githubRepository githubRepository := os.Getenv("GITHUB_REPOSITORY") if githubRepository == "" { println("Env ${GITHUB_REPOSITORY} is not set") os.Exit(1) } // get version getReleaseRequest, err := http.NewRequest( "GET", fmt.Sprintf("https://api.github.com/repos/%v/releases/tags/%v", githubRepository, version.Code), nil, ) if err != nil { panic(err) } getReleaseRequest.Header.Set("User-Agent", commons.Ua) getReleaseRequest.Header.Set("Authorization", "token "+ghToken) getReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest) if err != nil { panic(err) } defer getReleaseResponse.Body.Close() if getReleaseResponse.StatusCode == 404 { panic("NOT FOUND RELEASE") } buff, err := ioutil.ReadAll(getReleaseResponse.Body) if err != nil { panic(err) } var release commons.Release err = json.Unmarshal(buff, &release) if err != nil { println(string(buff)) panic(err) } file, err := os.Open(releaseFilePath) if err != nil { panic(err) } defer file.Close() uploadUrl := fmt.Sprintf("https://uploads.github.com/repos/%v/releases/%v/assets?name=%v", githubRepository, release.Id, releaseFileName) uploadRequest, err := http.NewRequest("POST", uploadUrl, file) if err != nil { panic(err) } uploadRequest.Header.Set("User-Agent", commons.Ua) uploadRequest.Header.Set("Authorization", "token "+ghToken) uploadRequest.Header.Set("Content-Type", contentType) uploadRequest.ContentLength = contentLength uploadResponse, err := http.DefaultClient.Do(uploadRequest) if err != nil { panic(err) } if uploadResponse.StatusCode != 201 { buff, err = ioutil.ReadAll(uploadResponse.Body) if err != nil { panic(err) } println(string(buff)) panic("NOT 201") } } ================================================ FILE: ci/cmd/upload_asset_core/main.go ================================================ package main import ( "ci/commons" "encoding/json" "fmt" "io/ioutil" "net/http" "os" "path" ) func main() { // get ghToken ghToken := os.Getenv("GITHUB_TOKEN") if ghToken == "" { println("Env ${GITHUB_TOKEN} is not set") os.Exit(1) } // get version version := commons.LoadVersion() // get TARGET target := os.Getenv("TARGET") if target == "" { println("Env ${TARGET} is not set") os.Exit(1) } // var releaseFileName = fmt.Sprintf("core-%v-%v.zip", version.Code, target) var releaseFilePath = "core.zip" var contentLength int64 releaseFilePath = path.Join("..", releaseFilePath) info, err := os.Stat(releaseFilePath) if err != nil { panic(err) } contentLength = info.Size() // get version githubRepository := os.Getenv("GITHUB_REPOSITORY") if githubRepository == "" { println("Env ${GITHUB_REPOSITORY} is not set") os.Exit(1) } getReleaseRequest, err := http.NewRequest( "GET", fmt.Sprintf("https://api.github.com/repos/%v/releases/tags/%v", githubRepository, version.Code), nil, ) if err != nil { panic(err) } getReleaseRequest.Header.Set("User-Agent", commons.Ua) getReleaseRequest.Header.Set("Authorization", "token "+ghToken) getReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest) if err != nil { panic(err) } defer getReleaseResponse.Body.Close() if getReleaseResponse.StatusCode == 404 { panic("NOT FOUND RELEASE") } buff, err := ioutil.ReadAll(getReleaseResponse.Body) if err != nil { panic(err) } var release commons.Release err = json.Unmarshal(buff, &release) if err != nil { println(string(buff)) panic(err) } file, err := os.Open(releaseFilePath) if err != nil { panic(err) } defer file.Close() uploadUrl := fmt.Sprintf("https://uploads.github.com/repos/%v/releases/%v/assets?name=%v", githubRepository, release.Id, releaseFileName) uploadRequest, err := http.NewRequest("POST", uploadUrl, file) if err != nil { panic(err) } uploadRequest.Header.Set("User-Agent", commons.Ua) uploadRequest.Header.Set("Authorization", "token "+ghToken) uploadRequest.Header.Set("Content-Type", "application/octet-stream") uploadRequest.ContentLength = contentLength uploadResponse, err := http.DefaultClient.Do(uploadRequest) if err != nil { panic(err) } if uploadResponse.StatusCode != 201 { buff, err = ioutil.ReadAll(uploadResponse.Body) if err != nil { panic(err) } println(string(buff)) panic("NOT 201") } } ================================================ FILE: ci/commons/funcs.go ================================================ package commons import ( "fmt" "io/ioutil" "strings" ) const Ua = "pikapika ci" const MainBranch = "master" func LoadVersion() Version { var version Version codeFile, err := ioutil.ReadFile("version.code.txt") if err != nil { panic(err) } version.Code = strings.TrimSpace(string(codeFile)) infoFile, err := ioutil.ReadFile("version.info.txt") if err != nil { panic(err) } version.Info = strings.TrimSpace(string(infoFile)) return version } func AssetName(version Version, flutterVersion, target, branch string) string { releaseFileName := fmt.Sprintf("pikapika-%v", version.Code) switch target { case "macos": releaseFileName += "-macos-intel" releaseFileName += "-flutter_" + flutterVersion releaseFileName += ".dmg" case "ios": releaseFileName += "-ios_nosign" releaseFileName += "-flutter_" + flutterVersion releaseFileName += ".ipa" case "windows": releaseFileName += "-windows-x86_64" releaseFileName += "-flutter_" + flutterVersion releaseFileName += ".zip" case "linux": releaseFileName += "-linux-x86_64" releaseFileName += "-flutter_" + flutterVersion releaseFileName += ".AppImage" case "android-arm32": releaseFileName += "-android-arm32" releaseFileName += "-flutter_" + flutterVersion releaseFileName += ".apk" case "android-arm64": releaseFileName += "-android-arm64" releaseFileName += "-flutter_" + flutterVersion releaseFileName += ".apk" case "android-x86_64": releaseFileName += "-android-x86_64" releaseFileName += "-flutter_" + flutterVersion releaseFileName += ".apk" } if branch != "master" && branch != "main" { releaseFileName = branch + "-" + releaseFileName } return releaseFileName } ================================================ FILE: ci/commons/types.go ================================================ package commons import "time" type Version struct { Code string `json:"code"` Info string `json:"info"` } type Release struct { Url string `json:"url"` HtmlUrl string `json:"html_url"` AssetsUrl string `json:"assets_url"` UploadUrl string `json:"upload_url"` TarballUrl string `json:"tarball_url"` ZipballUrl string `json:"zipball_url"` DiscussionUrl string `json:"discussion_url"` Id int `json:"id"` NodeId string `json:"node_id"` TagName string `json:"tag_name"` TargetCommitish string `json:"target_commitish"` Name string `json:"name"` Body string `json:"body"` Draft bool `json:"draft"` Prerelease bool `json:"prerelease"` CreatedAt time.Time `json:"created_at"` PublishedAt time.Time `json:"published_at"` Author struct { Login string `json:"login"` Id int `json:"id"` NodeId string `json:"node_id"` AvatarUrl string `json:"avatar_url"` GravatarId string `json:"gravatar_id"` Url string `json:"url"` HtmlUrl string `json:"html_url"` FollowersUrl string `json:"followers_url"` FollowingUrl string `json:"following_url"` GistsUrl string `json:"gists_url"` StarredUrl string `json:"starred_url"` SubscriptionsUrl string `json:"subscriptions_url"` OrganizationsUrl string `json:"organizations_url"` ReposUrl string `json:"repos_url"` EventsUrl string `json:"events_url"` ReceivedEventsUrl string `json:"received_events_url"` Type string `json:"type"` SiteAdmin bool `json:"site_admin"` } `json:"author"` Assets []struct { Url string `json:"url"` BrowserDownloadUrl string `json:"browser_download_url"` Id int `json:"id"` NodeId string `json:"node_id"` Name string `json:"name"` Label string `json:"label"` State string `json:"state"` ContentType string `json:"content_type"` Size int `json:"size"` DownloadCount int `json:"download_count"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Uploader struct { Login string `json:"login"` Id int `json:"id"` NodeId string `json:"node_id"` AvatarUrl string `json:"avatar_url"` GravatarId string `json:"gravatar_id"` Url string `json:"url"` HtmlUrl string `json:"html_url"` FollowersUrl string `json:"followers_url"` FollowingUrl string `json:"following_url"` GistsUrl string `json:"gists_url"` StarredUrl string `json:"starred_url"` SubscriptionsUrl string `json:"subscriptions_url"` OrganizationsUrl string `json:"organizations_url"` ReposUrl string `json:"repos_url"` EventsUrl string `json:"events_url"` ReceivedEventsUrl string `json:"received_events_url"` Type string `json:"type"` SiteAdmin bool `json:"site_admin"` } `json:"uploader"` } `json:"assets"` } ================================================ FILE: ci/go.mod ================================================ module ci go 1.17 require ( github.com/andersfylling/disgord v0.35.1 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 ) require ( github.com/andersfylling/snowflake/v5 v5.0.1 // indirect github.com/klauspost/compress v1.15.1 // indirect go.uber.org/atomic v1.9.0 // indirect golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect nhooyr.io/websocket v1.8.7 // indirect ) ================================================ FILE: ci/go.sum ================================================ github.com/andersfylling/disgord v0.35.1 h1:auhxW9z96/uSF7MYwfuv8AP71AVIc0+jZQWjZdwIqNE= github.com/andersfylling/disgord v0.35.1/go.mod h1:gTzujw2mWxJWxAPo3LwxG5+a4/n4ikdD+JMb1mONmUM= github.com/andersfylling/snowflake/v5 v5.0.1 h1:unXbYSij6tRCGJzoLz9zl3nJsqd9hu7bbYSgB8K8/i0= github.com/andersfylling/snowflake/v5 v5.0.1/go.mod h1:AdhrB+kewjnQInv8cR7ABe2SGoVXh79njnipUnz1HFc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A= github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= k8s.io/gengo v0.0.0-20220307231824-4627b89bbf1b/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= ================================================ FILE: ci/linux_font.yaml ================================================ fonts: - family: Roboto fonts: - asset: fonts/Roboto.ttf ================================================ FILE: ci/version.code.txt ================================================ v1.8.19 ================================================ FILE: ci/version.info.txt ================================================ v1.8.19 ================================================ FILE: ios/.gitignore ================================================ *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 12.0 ================================================ FILE: ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '12.0' # 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', '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 Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings["IPHONEOS_DEPLOYMENT_TARGET"] = "11.0" end end end ================================================ FILE: ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter import Mobile import LocalAuthentication import Network @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { private let networkMonitor = NWPathMonitor() private var latestPath: NWPath? override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] let applicationSupportsPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0] MobileMigration(documentsPath, applicationSupportsPath) MobileInitApplication(applicationSupportsPath) let monitorQueue = DispatchQueue(label: "network.monitor") networkMonitor.pathUpdateHandler = { [weak self] path in self?.latestPath = path } networkMonitor.start(queue: monitorQueue) let controller = self.window.rootViewController as! FlutterViewController let channel = FlutterMethodChannel.init(name: "method", binaryMessenger: controller as! FlutterBinaryMessenger) channel.setMethodCallHandler { (call, result) in Thread { if call.method == "flatInvoke" { if let args = call.arguments as? Dictionary, let method = args["method"] as? String, let params = args["params"] as? String{ var error: NSError? let data = MobileFlatInvoke(method, params, &error) if error != nil { result(FlutterError(code: "", message: error?.localizedDescription, details: "")) }else{ result(data) } }else{ result(FlutterError(code: "", message: "params error", details: "")) } } else if call.method == "verifyAuthentication"{ let context = LAContext() let can = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) guard can == true else { result(false) return } context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "身份验证") { (success, error) in result(success) } } else if call.method == "iosSaveFileToImage"{ if let args = call.arguments as? Dictionary, let path = args["path"] as? String{ do { let fileURL: URL = URL(fileURLWithPath: path) let imageData = try Data(contentsOf: fileURL) if let uiImage = UIImage(data: imageData) { UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil) result("OK") }else{ result(FlutterError(code: "", message: "Error loading image ", details: "")) } } catch { result(FlutterError(code: "", message: "Error loading image : \(error)", details: "")) } }else{ result(FlutterError(code: "", message: "params error", details: "")) } } else if call.method == "iosGetDocumentDir" { result(documentsPath) } else if call.method == "dataLocal" { result(applicationSupportsPath) } else if call.method == "fontList" { result(UIFont.familyNames) } else { result(FlutterMethodNotImplemented) } }.start() } let networkChannel = FlutterMethodChannel(name: "network", binaryMessenger: controller as! FlutterBinaryMessenger) networkChannel.setMethodCallHandler { [weak self] call, result in let path = self?.latestPath ?? self?.networkMonitor.currentPath guard let path = path, path.status == .satisfied else { result(call.method == "getIsMobile" ? false : "none") return } if call.method == "getIsMobile" { result(path.usesInterfaceType(.cellular)) return } guard call.method == "getNetworkType" else { result(FlutterMethodNotImplemented) return } if path.usesInterfaceType(.wifi) { result("wifi") return } if path.usesInterfaceType(.cellular) { result("mobile") return } if path.usesInterfaceType(.wiredEthernet) { result("ethernet") return } result("other") } // let eventChannel = FlutterEventChannel.init(name: "flatEvent", binaryMessenger: controller as! FlutterBinaryMessenger) class EventChannelHandler:NSObject, FlutterStreamHandler { func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { objc_sync_enter(mutex) sink = events objc_sync_exit(mutex) return nil } func onCancel(withArguments arguments: Any?) -> FlutterError? { objc_sync_enter(mutex) sink = nil objc_sync_exit(mutex) return nil } } class EventNotifyHandler:NSObject, MobileEventNotifyHandlerProtocol { func onNotify(_ message: String?) { objc_sync_enter(mutex) if sink != nil { sink?(message) } objc_sync_exit(mutex) } } eventChannel.setStreamHandler(EventChannelHandler.init()) MobileEventNotify(EventNotifyHandler.init()) // GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } var sink : FlutterEventSink? let mutex = NSObject.init() ================================================ FILE: ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "LaunchImage.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. ================================================ FILE: ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: ios/Runner/Info.plist ================================================ CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName pikapika CFBundleDocumentTypes CFBundleTypeName PKZ Archive LSHandlerRank Owner LSItemContentTypes opensource.pkz pkz CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleLocalizations zh_TW zh_CN en_US ja_JP ko_KR CFBundleName pikapika CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleURLTypes CFBundleURLSchemes pika CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSApplicationCategoryType public.app-category.entertainment LSRequiresIPhoneOS LSSupportsOpeningDocumentsInPlace NSFaceIDUsageDescription Authenticating using face id NSPhotoLibraryAddUsageDescription Save images NSPhotoLibraryUsageDescription Usage images UIApplicationSupportsIndirectInputEvents UIFileSharingEnabled UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance UTExportedTypeDeclarations UTTypeConformsTo public.data public.content com.apple.package UTTypeDescription PKZ Archive UTTypeIdentifier opensource.pkz UTTypeTagSpecification public.filename-extension pkz pki zip public.mime-type text/vnd.opensource.pkz text/vnd.opensource.pki text/vnd.opensource.zip ================================================ FILE: ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 0E44DEFD92B805627806403C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 605DB0C59210B25A843453FD /* Pods_Runner.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; DD1F548D273CB9A900B04493 /* Mobile.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD1F548C273CB9A900B04493 /* Mobile.xcframework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1001C50AAB0DFA884ACAD48C /* 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 = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3742BDBA4B7EA3162E2CDC75 /* 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 = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 605DB0C59210B25A843453FD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; 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 = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; CA7EB5DA1FDE22BAC5B01D77 /* 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 = ""; }; DD1F548C273CB9A900B04493 /* Mobile.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Mobile.xcframework; path = ../go/mobile/lib/Mobile.xcframework; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 0E44DEFD92B805627806403C /* Pods_Runner.framework in Frameworks */, DD1F548D273CB9A900B04493 /* Mobile.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 6CBB90743F7578DFC9C6BF75 /* Pods */ = { isa = PBXGroup; children = ( 3742BDBA4B7EA3162E2CDC75 /* Pods-Runner.debug.xcconfig */, 1001C50AAB0DFA884ACAD48C /* Pods-Runner.release.xcconfig */, CA7EB5DA1FDE22BAC5B01D77 /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( DD1F548C273CB9A900B04493 /* Mobile.xcframework */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 6CBB90743F7578DFC9C6BF75 /* Pods */, F6DB48AA376F5D49016BEA7A /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; F6DB48AA376F5D49016BEA7A /* Frameworks */ = { isa = PBXGroup; children = ( 605DB0C59210B25A843453FD /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 6D683F8ECDB7CFFB7E7E554B /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 774454864019DA9867B5A218 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 6D683F8ECDB7CFFB7E7E554B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); 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"; showEnvVarsInLog = 0; }; 774454864019DA9867B5A218 /* [CP] Embed Pods Frameworks */ = { 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"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_SEARCH_PATHS = "../go/mobile/lib/**"; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 43; DEVELOPMENT_TEAM = SSSSSSSSSS; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.8.19; PRODUCT_BUNDLE_IDENTIFIER = opensource.pikapika; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; FRAMEWORK_SEARCH_PATHS = "../go/mobile/lib/**"; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_SEARCH_PATHS = "../go/mobile/lib/**"; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 43; DEVELOPMENT_TEAM = SSSSSSSSSS; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.8.19; PRODUCT_BUNDLE_IDENTIFIER = opensource.pikapika; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 43; DEVELOPMENT_TEAM = SSSSSSSSSS; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.8.19; PRODUCT_BUNDLE_IDENTIFIER = opensource.pikapika; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: lib/assets/translations/en-US.json ================================================ { "language": { "title": "Language", "name": "English - United States" }, "app": { "categories": "Categories", "my": "My", "copied_to_clipboard": "Copied to clipboard", "not_supported_platform": "Not supported platform", "cancel": "Cancel", "confirm": "Confirm", "save_cancel": "Save canceled", "save_success": "Save success", "save_failed": "Save failed", "pro": "Pro", "pro_required": "Please upgrade to Pro to use this feature", "choose_folder": "Choose a folder to save the file", "permission_denied": "Permission denied", "loading": "Loading", "error": "Error", "pat": { "success": "Your sponsor login success, please return", "title": "Replace PAT account" }, "previous_page": "Previous page", "next_page": "Next page", "page": "Page", "please_enter_page_number": "Please enter page number:", "select_all": "Select all", "load_failed": "Load failed", "all": "All", "delete": "Delete", "save_image": "Save image", "preview_image": "Preview image", "please_select": "Please select", "refresh": "Refresh", "initializing": "Initializing", "like_failed": "Like failed", "network_error": "Network error, please check your network", "no_permission": "No permission or path is not available", "check_device_time": "Please check your device time", "resource_not_available": "Resource not available", "something_went_wrong": "Something went wrong", "click_refresh": "Click refresh", "pull_down_refresh": "Pull down refresh", "continue_reading": "Continue reading", "start_reading": "Start reading", "image_crop": "Image crop", "download": "Download", "download_failed": "Download failed", "download_finished": "Download finished", "downloading": "Downloading", "queue": "Queue", "deleting": "Deleting", "please_select_comic": "Please select comic", "please_choose": "Please choose", "last_viewed": "Last viewed", "auto_punch": "Auto punch", "yes": "Yes", "no": "No", "confirm_download": "Confirm download", "copy": "Copy" }, "net": { "no_address": "No address", "address": "Address", "address_sync": "Address sync", "address_sync_from_server": "Get the latest address from the server", "address_sync_reset": "Reset the address to the default value", "address_sync_success": "Address sync success", "address_sync_failed": "Address sync failed", "address_sync_reset_success": "Address sync reset success", "address_sync_reset_failed": "Address sync reset failed", "choose_address": "Choose address", "image_address": "Image address", "use_api_load_image": "Use API to load image", "ping_testing": "Testing", "ping_failed": "Failed" }, "categories": { "all": "All", "recommend": "Recommend", "rankings": "Rankings", "random": "Random", "game": "Game" }, "settings": { "settings": "Settings", "interface": "Interface", "network": "Network", "seal": "Seal", "interaction": "Interaction", "reading": "Reading", "download": "Downloads", "auto_download_on_favorite": "Auto download on favorite", "disable_auto_download_on_mobile": "Disable auto download on mobile data", "auto_delete_download_on_unfavorite": "Auto delete download on unfavorite", "web_server": "Web Server", "web_server_subtitle": "Let devices in the local network view the downloaded comics through the browser", "sync": "Sync", "history_sync": "History Sync", "local_favorite_sync_title": "Local Favorites Sync", "use_local_favorite": "Use Local Favorites", "use_local_favorite_desc": "Manage favorites locally with folder organization", "account": "Account", "modify_password": "Modify password", "ebook": "E-book", "system": "System", "clear_cache": "Clear cache", "migrate": "Migrate", "migrate_subtitle": "Change your data folder to the memory card", "migrate_confirm": "This feature will be saved after restarting the program, are you sure", "app_orientation": { "title": "App orientation", "choose": "Choose app orientation", "normal": "Normal", "landscape": "Landscape", "portrait": "Portrait" }, "will_pop_notice": "Press the back key twice in a row to exit the app", "android_secure_flag": "Disable screenshot/disable display in task view", "android_display_mode": { "title": "Screen refresh rate (Android)", "dialog_title": "Android screen refresh rate \n(No high refresh in power saving mode)" }, "authentication": "Authentication when entering the app (if the system has already entered the password or fingerprint)", "set_password": "Set application password", "auto_clean": { "title": "Auto clean cache", "one_month_ago": "One month ago", "one_week_ago": "One week ago", "one_day_ago": "One day ago", "no_auto_clean": "No auto clean" }, "categories_column_count": { "title": "Categories column count", "choose": "Choose categories column count", "auto": "Auto" }, "categories_sort": { "title": "Categories sort" }, "chooser_root": { "title": "Chooser root", "hint": "Enter the folder selector root path", "desc": "The default path for selecting the directory when exporting, also the root path, you can try to set this option if the export is not normal" }, "content_failed_reload_action": { "title": "Content failed reload action", "choose": "Choose content failed reload action", "pull_down": "Pull down", "touch_loader": "Touch loader" }, "copy_full_name": { "title": "Copy full name" }, "copy_full_name_template": { "title": "Copy full name template", "hint": "Enter the copy full name template" }, "copy_skip_confirm": { "title": "Copy skip confirm" }, "download_and_export_path": { "title": "Download and export path", "confirm": "Download and export", "desc": "You will select a directory, if the file system is writable, the download will be automatically exported" }, "download_cache_path": { "title": "Download cache path", "confirm": "Download cache", "desc": "You will select a directory, this directory is copied from the following directory to use. The download will be read as a cache folder first.", "cancel_desc": "Are you sure you want to cancel the function of downloading content acceleration with other software? You can set it again after canceling", "import_view_log_from_off": { "title": "Import other program's history record", "desc": "You will select a file, this file is copied from the following path to use.", "choose_file_dialog_title": "Choose the file to import" } }, "download_thread_count": { "title": "Download thread count", "choose": "Choose download thread count" }, "ebook_scrolling": { "title": "E-book scrolling UI" }, "ebook_scrolling_range": { "title": "E-book scrolling UI", "desc": "Scrolling range", "screen_height": "Screen height" }, "ebook_scrolling_trigger": { "title": "E-book scrolling UI", "desc": "Trigger distance", "cm": "cm" }, "export_path": { "ios_desc": "You can find the exported content in the file manager", "ios_desc2": "You are using an iOS device:\nPlease open the system's built-in file manager to browse the exported content", "export_path_desc": "Export path (click to modify)", "android_desc": "You are using an Android device:\nIf the export fails and prompts insufficient permissions, you can try to create a subdirectory under Download or Document for export" }, "export_rename": { "title": "Export with renaming" }, "yes": "Yes", "no": "No", "full_screen_action": { "title": "Control method", "choose": "Choose control method", "touch_once": "Touch once to fullscreen", "controller": "Use controller to fullscreen", "touch_double": "Double click to fullscreen", "touch_double_once_next": "Double click to fullscreen + click once to next page", "three_area": "Divide the screen into three areas (previous page, next page, fullscreen)" }, "full_screen_ui": { "title": "Full screen UI", "choose": "Choose full screen UI", "no": "Not use", "hidden_bottom": "Remove virtual controller", "all": "Full screen" }, "auto_full_screen": { "title": "Enter reader automatically full screen" }, "auto_full_screen_on_forward": { "title": "Auto Fullscreen on Forward" }, "ignore_info_history": { "title": "Ignore info history" }, "icon_loading": { "title": "Minimize UI animation" }, "ignore_upgrade_confirm": { "title": "Close upgrade popup" }, "hidden_fd_icon": { "title": "Hide personal space's power icon" }, "hidden_search_persion": { "title": "Hide search by author" }, "hidden_viewed": { "title": "Hide viewed comics" }, "hidden_sub_icon": { "title": "Hide subscription" }, "hide_online_favorite": { "title": "Hide online favorites", "desc": "Hide online favorites entry and favorite button" }, "hidden_words": { "title": "Hide by keyword", "clear_all": "Confirm clear", "clear_all_desc": "Are you sure you want to clear all keywords?", "input_hint": "Enter the keyword to hide", "no_words": "No keywords" }, "image_address": { "title": "Image address", "pinging": "Pinging", "failed": "Failed" }, "image_filter": { "title": "Reader image filter", "normal": "Normal", "gray": "Gray", "brown": "Brown", "choose": "Choose reader image filter" }, "import_notice": { "android_desc": "You are using an Android device:\nIf you cannot import and export and prompt insufficient permissions, you can try to create a subdirectory under Download or Document for import" }, "keyboard_controller": { "title": "Reader keyboard page (only PC)" }, "list_layout": { "choose": "Choose layout", "info_card": "Info", "only_image": "Cover", "cover_and_title": "Cover + Title" }, "local_history_sync": { "sync_to_local": "Sync history to local", "not_set": "Not set", "sync_success": "Sync success", "sync_failed": "Sync failed", "auto_sync": "Auto sync history to local", "auto_sync_desc": "After opening the application, the history will be automatically backed up", "choose_dir": "Choose directory", "clear_path": "Clear path", "clear_path_desc": "Are you sure you want to clear the path?" }, "local_favorite_sync": { "auto_sync": "Auto sync local favorites", "auto_sync_desc": "Auto sync local favorites with WebDAV", "manual_sync": "Manual sync local favorites", "sync_success": "Sync success", "sync_failed": "Sync failed" }, "no_animation": { "title": "Cancel page animation (tap screen, volume key, keyboard)" }, "pager_action": { "title": "List page loading method", "choose": "Choose list page loading method", "controller": "Use button", "stream": "Stream" }, "proxy": { "title": "Proxy server", "hint": "Enter proxy server", "desc": " ( e.g. socks5://127.0.0.1:1080/ ) ", "no_proxy": "Not set" }, "quality": { "title": "Image quality when browsing", "choose": "Choose image quality", "original": "Original", "low": "Low", "medium": "Medium", "high": "High" }, "reader_background_color": { "title": "Reader background color", "choose": "Choose reader background color", "black": "Black", "gray": "Gray", "white": "White" }, "reader_direction": { "title": "Reader direction", "choose": "Choose reader direction", "top_to_bottom": "Top to bottom", "left_to_right": "Left to right", "right_to_left": "Right to left" }, "reader_scroll_by_screen_percentage": { "title": "Flip distance by distance", "screen_size": "Screen size" }, "web_toon_scroll_mode": { "title": "WebToon Flip Mode", "choose": "Choose WebToon Flip Mode", "image": "Image", "screen": "Distance" }, "reader_zoom": { "out_title": "Zoom out multiplier (min scale)", "in_title": "Zoom in multiplier (max scale)", "double_tap_title": "Double tap zoom scale" }, "drag_region_lock": { "title": "Lock Drag Boundary" }, "gesture_speed": { "title": "Gesture Speed Multiplier" }, "reader_slider_position": { "title": "Slider position", "choose": "Choose slider position", "bottom": "Bottom", "right": "Right", "left": "Left" }, "reader_two_page_direction": { "title": "Two page reader content arrangement", "choose": "Choose two page reader content arrangement", "close_to": "Close to", "pull_away": "Pull away", "each_centered": "Each centered" }, "reader_type": { "title": "Reader mode", "choose": "Choose reader mode", "web_toon": "WebToon (Default)", "web_toon_zoom": "WebToon (Double click to zoom)", "gallery": "Gallery", "web_toon_free_zoom": "WebToon (ListView double click to zoom)\n(This mode progress bar is invalid)", "two_page_gallery": "Two page mode\n(Experimental)", "left_to_right": "Left to right", "right_to_left": "Right to left", "two_page_direction": "Two page direction", "two_page_direction_choose": "Choose two page direction" }, "shadow_categories": { "title": "Seal", "search_hint": "Search" }, "shadow_categories_mode": { "title": "Seal mode", "black_list": "Black list", "white_list": "White list" }, "startup_pic": { "title": "Set startup picture", "subtitle": "Set the picture displayed when the application starts", "clear_title": "Clear startup picture", "clear_subtitle": "Clear the picture displayed when the application starts", "clear_success": "Startup picture cleared", "update_success": "Startup picture updated" }, "show_comment_at_download": { "title": "Show comment at download" }, "font": { "title": "Font", "hint": "Please enter the font", "input_hint": "Please enter the font name and use English commas to separate, for example \"Songti, Heiti\", if you save it and it doesn't change, it means the font cannot be used or the name is wrong, you can refer to C:\\Windows\\Fonts to find your font. If you are using the flutter2 engine version, only the first font will take effect.", "choose_hint": "You need to select multiple fonts until you click the background area" }, "theme": { "origin": "Origin", "pink": "Pink", "black": "Black", "dark": "Dark", "dusty_blue": "Dusty blue", "dark_black": "Dark black", "choose_theme": "Choose theme", "book": "Book", "enable_status_bar_color": "Enable status bar color", "enable_status_restart_hint": "When disabled, you need to restart the application to refresh the status bar color" }, "three_keep_right": { "title": "Three area mode page always to the right" }, "time_zone": { "title": "Time zone" }, "timeout_lock": { "title": "Auto lock", "notice": "Note: Auto lock only supports timeout after minimizing on desktop, and supports timeout after background and screen lock on mobile. If no password is set, auto lock is invalid. Android and desktop only lock the desktop, not the download, iOS is not tested, you need to manually enable background activity.", "1_hour": "One hour", "10_minutes": "Ten minutes", "3_minutes": "Three minutes", "1_minute": "One minute", "10_seconds": "Ten seconds", "1_second": "One second", "no_lock": "No lock" }, "using_right_click_pop": { "title": "Mouse right click to return to the previous page" }, "volume_controller": { "title": "Reader volume button page turn" }, "volume_next_chapter": { "title": "Double click volume/keyboard/controller to next chapter" }, "webdav": { "title": "WebDav", "not_set": "Not set", "path": "WebDav path", "path_hint": "Please enter the WebDav path", "username": "WebDav username", "username_hint": "Please enter the WebDav username", "password": "WebDav password", "password_hint": "Please enter the WebDav password", "auto_sync_history_to_webdav": "Auto sync history to WebDav", "sync_history_to_webdav": "Sync history to WebDAV", "upload_history_to_webdav": "Upload history to WebDAV", "upload_history_to_webdav_desc": "If there are multiple devices, please note the automatic synchronization function", "sync_success": "Sync success", "sync_failed": "Sync failed" } }, "local_favorite": { "title": "Local Favorites", "all_folders": "All", "new_folder": "New Folder", "select_mode": "Select", "cancel_select_mode": "Cancel select", "select_all": "Select all", "delete_folder": "Delete Folder", "move_to_folder": "Move to Folder", "remove_selected": "Remove selected", "remove_selected_confirm": "Remove selected comics from local favorites?", "remove_selected_success": "Removed", "remove_selected_failed": "Remove failed", "select_comics": "Please select comics first", "folder_limit_reached": "Free version allows up to 3 folders. Upgrade to Pro for unlimited folders", "batch_download": "Batch Download", "select_folder": "Select Folder", "folder_name": "Folder Name", "delete_confirm": "Confirm delete folder?", "empty_folder": "No favorites yet", "no_folders": "No folders yet, please create one first", "remove_confirm_title": "Remove from Favorites", "remove_confirm_content": "Are you sure you want to remove this comic from local favorites?", "remove_failed": "Remove failed", "load_failed": "Load failed", "add_success": "Added to local favorites", "add_failed": "Add failed", "create_folder_failed": "Create folder failed", "create_success": "Created successfully", "delete_success": "Deleted successfully", "delete_failed": "Delete failed", "move_success": "Moved successfully", "move_failed": "Move failed", "select_comics_to_download": "Please select comics to download", "download_started": "Download started", "download_failed": "Download failed" }, "screen": { "about": { "title": "About", "version": "Software version", "check_update": "Check update", "tips": "Tips : \n1. The author/uploader/category/tag on the detail page can be clicked\n2. The author/uploader/title on the detail page can be copied by long press\n3. Using pagination instead of waterfall flow can quickly flip pages\n4. Download means caching to the local, you need to export to share\n5. Download long press can delete", "download_new_version": "Please download the new version from the channel", "no_new_version": "No new version detected", "download_release_version": "Download RELEASE version", "update_content": "Update content", "go_to_release_repository": "Go to RELEASE repository" }, "account": { "title": "Account", "username": "Username", "username_hint": "Please enter the username", "password": "Password", "password_hint": "Please enter the password", "no_account_register": "No account, I want to register", "password_reset": "Password reset", "check_username_password_or_network": "Please check the username and password or network environment", "check_device_time": "Please check the device time", "username_or_password_error": "Username or password error", "login_failed": "Login failed", "not_set": "Not set" }, "app": { "will_pop_notice": "Press the back key twice in a row to exit the app" }, "categories": { "search_hint": "Search" }, "clean": { "title": "Clean", "cleaning": "Cleaning", "clean_network_cache": "Clean network cache", "clean_image_cache": "Clean image cache", "clean_all_cache": "Clean all cache", "clean_success": "Clean success", "clean_failed": "Clean failed" }, "close_app": { "title": "Tips", "close_app": "Please close the app and reopen" }, "comic_collections": { "no_resource": "There is no resource here" }, "comic_info": { "chapter": "Chapter", "comment": "Comment", "recommend": "Recommend" }, "comics": { "search_hint": "Search category", "choose_category": "Please choose category" }, "comic_subscribes": { "update_reminder": "Update reminder", "check_update": "Check update", "cancel_all_update_reminder": "Cancel all update reminder" }, "comment": { "title": "Comment", "hint_text": "Please enter the comment content", "success": "Comment success", "i_have_something_to_say": "I have something to say", "please_enter_comment": "Please enter the comment content" }, "desktop_authentication": { "current_password": "Current password", "password_error": "Password error", "password_initialization": "Password initialization", "password": "Password", "re_enter_password": "Re-enter password", "password_mismatch": "The two passwords entered are different", "set_password": "Set password" }, "download_confirm": { "please_select_ep": "Please select the EP to download", "already_added_to_download_list": "Already added to download list" }, "download_export_group": { "title": "Batch export", "please_select_content": "Please select the content to export", "exporting": "Exporting", "export_failed": "Export failed", "export_success": "Export success", "export_to_pkz": "Export to PKZ\n(Encrypted mode, prevent web detection, can be opened with pikapika)", "export_to_pki": "Export to PKI\n(Encrypted mode, prevent web detection, can be imported with pikapika)", "export_to_zip": "Export to ZIP\n(Unencrypted mode, can be imported or viewed with pikapika)", "export_to_jpeg_zip": "Export to ZIP+JPEG\n(Can be used directly with other readers, cannot be imported again)", "export_to_jpeg_folder": "Export to folder+JPEG", "export_to_pdf": "Export to PDF", "export_to_epub": "Export to EPUB", "export_to_pdf_folder": "Export to folder, each chapter one PDF", "export_to_cbz": "Export to cbz", "after_power_use": "After power use", "input_save_name": "Please enter the saved name", "export_confirm": "Export confirm", "export_to_pkz_title": "Export the selected comics to a PKZ", "export_to_pki_title": "Export the selected comics to separate PKI", "please_power_up": "Please power up first", "export_to_zip_title": "Export the selected comics to ZIP", "export_to_jpeg_zip_title": "Export the selected comics to ZIP+JPEG", "export_to_jpeg_zip_title_not_down_over": "Export the selected comics to ZIP+JPEG\n(Even if the download is not successful, it can be used)", "export_to_jpeg_folder_title": "Export the selected comics to folder+JPEG", "export_to_pdf_title": "Export the selected comics to PDF", "export_to_epub_title": "Export the selected comics to EPUB", "export_to_pdf_folder_title": "Export the selected comics to folder, each chapter one PDF", "export_to_cbz_title": "Export the selected comics to cbz", "exporting_please_wait": "Exporting, please wait" }, "download_export_to_file": { "title": "Export", "transfer_to_other_device": "Transfer to other device", "input_save_name": "Please enter the saved name", "export_confirm": "Export confirm", "export_to_pkz_title": "Export the selected comics to a PKZ", "export_to_pkz_desc": "Export to xxx.pkz\n(Encrypted mode, prevent web detection, can be opened with pikapika)", "export_to_pki_title": "Export the selected comics to separate PKI", "export_to_pki_desc": "Export to xxx.pki\n(Encrypted mode, prevent web detection, can be imported with pikapika)", "export_to_zip_title": "Export the selected comics to ZIP", "export_to_zip_desc": "Export to xxx.zip\n(Unencrypted mode, can be imported or viewed with pikapika)", "export_to_jpeg_zip_title": "Export the selected comics to ZIP+JPEG", "export_to_jpeg_zip_desc": "Export to xxx.jpeg\n(Can be used directly with other readers, cannot be imported again)", "export_to_pdf_title": "Export the selected comics to PDF", "export_to_pdf_desc": "Export to xxx.pdf\n(Even if the download is not successful, it can be used, and the failed image will be skipped)\n(Can be opened directly in the photo album)", "export_to_pdf_folder_title": "Export the selected comics to folder, each chapter one PDF", "export_to_pdf_folder_desc": "Export to xxx.pdf\n(Even if the download is not successful, it can be used, and the failed image will be skipped)\n(Can be opened directly in the photo album)", "export_to_epub_title": "Export the selected comics to EPUB", "export_to_epub_desc": "Export to xxx.epub\n(Can be opened directly in the reader)", "export_to_jpeg_folder_title": "Export the selected comics to JPEGS.zip", "export_to_jpeg_folder_desc": "Export to JPGS.zip\n(Cannot be imported again)", "export_to_cbz_title": "Export the selected comics to cbk.zip", "export_to_cbz_desc": "Export to xxx.cbz, reader can use it directly (cannot be imported again)" }, "download_export_to_socket": { "title": "Network export", "loading": "Loading", "tips": "Do not exit the page before the transfer is successful, only one device can be exported at a time, the two devices need to be in the same network segment or infinite LAN, please enter IP:port on the other device, if there is only one IP, please select the IP of the infinite LAN, usually 192.168 starts", "get_ip_failed": "Get IP failed", "getting_ip": "Getting IP", "port": "Port" }, "download_import": { "title": "Import", "open_file": "Open file", "select_file": "Select the file to import", "import_success": "Import success", "import_failed": "Import failed", "select_file_desc": "Select zip file to import\nSelect pki file to import\nSelect pkz file to read", "input_address": "Please enter the address provided by the export device\nFor example \"192.168.1.2:50000\"", "import_from_other_device": "Import from other device", "select_folder_desc": "Select folder\n(Import all zip/pki in the folder)\n(After power use)" }, "download_info": { "loading": "Loading", "chapter": "Chapter", "comment": "Comment", "recommend": "Recommend" }, "download_list": { "search_download": "Search download", "multi_select_operation": "Multi select operation", "download_list": "Download list", "search": "Search", "select_folder": "Select folder", "download_already_in_delete_queue": "The download is already in the delete queue", "import": "Import", "export": "Export", "file": "File", "download_task": "Download task", "pause_download": "Pause download?", "start_download": "Start download?", "resume_failed": "Resume failed task", "resume_failed_desc": "All failed downloads have been resumed", "downloading": "Downloading", "paused": "Paused", "move_download": "Move download", "select_download_to_move": "Please select the download to move", "select_download_to_delete": "Please select the download to delete", "input_name": "==> Input name <==", "empty_folder_will_be_deleted": "(Empty folder will be automatically deleted, next time you need to manually input)", "folder_name": "Folder name", "please_input_folder_name": "Please input folder name", "delete_download": "Delete download", "delete_selected_download": "Delete selected download?", "multi_select": "Multi select" }, "download_only_import": { "importing": "Importing", "import_success": "Import success", "import_failed": "Import failed", "click_import_file": "Click import file", "importing_please_wait": "Importing, please wait" }, "favourite_paper": { "favourite": "Favourite" }, "forgot_password": { "title": "Password Recovery", "username": "Username", "not_set": "Not set", "confirm": "Confirm", "please_enter_username": "Please enter username", "question_1": "Question 1", "question_2": "Question 2", "question_3": "Question 3", "answer_1": "Answer 1", "answer_2": "Answer 2", "answer_3": "Answer 3", "please_enter_answer_1": "Please enter answer 1", "please_enter_answer_2": "Please enter answer 2", "please_enter_answer_3": "Please enter answer 3", "use_answer_1_recover": "Use answer 1 to recover password", "use_answer_2_recover": "Use answer 2 to recover password", "use_answer_3_recover": "Use answer 3 to recover password", "please_enter_answer": "Please enter answer", "new_password_copied": "New password is being copied to clipboard", "answer_incorrect": "Answer is incorrect", "password": "Password" }, "game_download": { "title": "Download", "download_links_obtained": "Download links obtained, you just need to choose one of them" }, "game_info": { "download": "Download", "details": "Details", "comments": "Comments" }, "games": { "title": "Games" }, "import_from_off": { "title": "Import", "import_success": "Import success", "import_failed": "Import failed" }, "modify_password": { "title": "Modify password", "please_wait": "Please wait", "old_password": "Old password", "new_password": "New password", "repeat_new_password": "Repeat new password", "not_filled": "Not filled", "please_enter_old_password": "Please enter old password", "please_enter_new_password": "Please enter new password", "please_repeat_new_password": "Please repeat new password", "new_password_mismatch": "New passwords do not match", "modify_success": "Modify success", "failed": "Failed", "confirm": "Confirm" }, "network_settings": { "title": "Network Settings" }, "pkz_reader": { "reading_downloaded_comic": "You are reading a downloaded comic" }, "pro": { "title": "Power Center", "power_center": "Power Center", "power_status": "Power Status", "powered": "Powered", "not_powered": "Not Powered", "pat_membership": "PAT Membership", "pat_status": "PAT Status", "pat_normal": "PAT Normal", "pat_bind_hint": "Please click here to bind to current account for power", "pat_rebind_hint": "Please click to rebind to current account for power", "pat_not_detected": "No membership detected, please go to download page to join", "i_have_powered": "I have powered before", "i_just_powered": "I just powered", "enter_code": "Enter code", "power_method": "Power Method", "wind_power": "Wind Power", "hydro_power": "Hydro Power", "solar_power": "Solar Power", "nuclear_power": "Nuclear Power", "choose_power_method": "Choose power method", "sign_in_exchange": "Sign in/Exchange", "click_pat_to_change": "Click PAT membership below to change", "update_pat_status": "Update PAT power status", "bind_to_account": "Bind to this account", "change_pat_key": "Change PAT key", "clear_pat_info": "Clear PAT info", "click_to_bind": "Click to bind", "enter_auth_code": "Please enter authorization code", "please_wait": "Please wait", "key_recorded": "Key: Recorded", "pat_account": "PAT Account", "bind_pika_account": "Bind PIKA Account", "bind_account_time": "Bind account time", "rebind_time": "Rebind available time", "power_features": "Power features: Multi-thread download / Batch import/export download", "power_guide": "Go to \"About\" page to find maintenance address for power guide\n\n \"I have powered before\" can sync corresponding power status\n \"I just powered\" exchange mysterious code\n \"Power method\" can be changed when network is not working\n \"PAT membership\" is independent power method" }, "rankings": { "title": "Rankings", "day": "Day", "week": "Week", "month": "Month", "knight": "Knight", "refresh": "Refresh", "comics_count": "comics" }, "random_comics": { "title": "Random Comics" }, "register": { "title": "Register", "registering": "Registering", "register_success": "Register Success", "register_failed": "Register Failed", "account_exists": "Account already exists", "name_exists": "Name already exists", "check_form": "Please check form, no empty fields allowed", "account": "Account", "password": "Password", "nickname": "Nickname", "gender": "Gender", "birthday": "Birthday", "question_1": "Question 1", "answer_1": "Answer 1", "question_2": "Question 2", "answer_2": "Answer 2", "question_3": "Question 3", "answer_3": "Answer 3", "not_set": "Not set", "please_enter_account": "Please enter account", "please_enter_password": "Please enter password", "please_enter_nickname": "Please enter nickname", "please_enter_question_1": "Please enter question 1", "please_enter_answer_1": "Please enter answer 1", "please_enter_question_2": "Please enter question 2", "please_enter_answer_2": "Please enter answer 2", "please_enter_question_3": "Please enter question 3", "please_enter_answer_3": "Please enter answer 3", "account_desc": "(lowercase letters + numbers / for login)", "password_desc": "(uppercase and lowercase letters + numbers / 8 or more characters)", "nickname_desc": "(Chinese allowed / 2-50 characters)", "choose_gender": "Choose your gender", "futa": "Futa", "male": "Male", "female": "Female", "register_success_desc": "You have registered successfully, please return to login", "account_label": "Account", "nickname_label": "Nickname" }, "search": { "title": "Search", "search_hint": "Search", "choose_category": "Please choose category" }, "search_author": { "title": "Search by Author", "search_hint": "Search by author + ", "by_author": "By author: " }, "space": { "title": "My", "logout": "Logout", "logout_confirm": "Are you sure you want to logout from current account?", "my_favourites": "My Favourites", "view_history": "View History", "my_downloads": "My Downloads" }, "theme": { "title": "Theme Settings", "theme": "Theme", "dark_mode_different_theme": "Use different theme in dark mode", "dark_mode_theme": "Theme (Dark Mode)" }, "view_logs": { "title": "View History", "clear_all": "Do you want to clear all view history?", "clear_all_desc": "Reading progress will also be deleted!", "clear_one": "Do you want to clear this view history?", "clear_one_desc": "Reading progress will also be deleted!", "clear_selected": "Do you want to clear selected view history?", "clear_selected_desc": "Reading progress will also be deleted!", "categories": "Categories" }, "web_server": { "title": "Download - Web Server", "loading": "Loading", "get_ip_failed": "Get IP failed", "getting_ip": "Getting IP", "port": "Port: 8080", "usage_instruction": "Enter \"http://device_ip:8080/\" in browser to access downloaded comics", "leave_notice": "Server will close after leaving this page" } }, "components": { "comic_info_card": { "categories": "Categories", "finished": "Finished", "viewed": "Viewed" }, "comic_list": { "shadow": "Shadowed Comics" }, "common": { "display_mode": "Display Mode", "shadow_mode": "Shadow Mode", "shadow_list": "Shadow List", "batch_download": "Batch Download" }, "image_reader": { "already_at_the_end": "Already at the end", "click_to_next_chapter": "Click to next chapter", "reload_page": "Reload page", "next_chapter": "Next chapter", "end_reading": "End reading", "reload_image": "Reload image", "save_image_in_this_page": "Save image in this page", "image_load_failed": "Image load failed" } } } ================================================ FILE: lib/assets/translations/ja-JP.json ================================================ { "language": { "title": "言語", "name": "日本語 - 日本" }, "app": { "categories": "カテゴリ", "my": "マイ", "copied_to_clipboard": "クリップボードにコピーしました", "not_supported_platform": "サポートされていないプラットフォーム", "cancel": "キャンセル", "confirm": "確認", "save_cancel": "保存がキャンセルされました", "save_success": "保存に成功しました", "save_failed": "保存に失敗しました", "pro": "プロ", "pro_required": "この機能を使用するにはプロにアップグレードしてください", "choose_folder": "ファイルを保存するフォルダを選択してください", "permission_denied": "権限が拒否されました", "loading": "読み込み中", "error": "エラー", "pat": { "success": "スポンサーログインが成功しました。戻ってください", "title": "PATアカウントを置き換え" }, "previous_page": "前のページ", "next_page": "次のページ", "page": "ページ", "please_enter_page_number": "ページ番号を入力してください:", "select_all": "すべて選択", "load_failed": "読み込みに失敗しました", "all": "すべて", "delete": "削除", "save_image": "画像を保存", "preview_image": "画像をプレビュー", "please_select": "選択してください", "refresh": "更新", "initializing": "初期化中", "like_failed": "いいねに失敗しました", "network_error": "ネットワークエラーです。ネットワークを確認してください", "no_permission": "権限がないか、パスが利用できません", "check_device_time": "デバイスの時間を確認してください", "resource_not_available": "リソースが利用できません", "something_went_wrong": "何かが間違っています", "click_refresh": "クリックして更新", "pull_down_refresh": "下にプルして更新", "continue_reading": "読み続ける", "start_reading": "読み始める", "image_crop": "画像トリミング", "download": "ダウンロード", "download_failed": "ダウンロードに失敗しました", "download_finished": "ダウンロード完了", "downloading": "ダウンロード中", "queue": "キュー中", "deleting": "削除中", "please_select_comic": "漫画を選択してください", "please_choose": "選択してください", "last_viewed": "最後に見た", "auto_punch": "自動チェックイン", "yes": "はい", "no": "いいえ", "confirm_download": "ダウンロード確認", "copy": "コピー" }, "net": { "no_address": "アドレスなし", "address": "アドレス", "address_sync": "アドレス同期", "address_sync_from_server": "サーバーから最新のアドレスを取得", "address_sync_reset": "アドレスをデフォルト値にリセット", "address_sync_success": "アドレス同期に成功しました", "address_sync_failed": "アドレス同期に失敗しました", "address_sync_reset_success": "アドレス同期リセットに成功しました", "address_sync_reset_failed": "アドレス同期リセットに失敗しました", "choose_address": "アドレスを選択", "image_address": "画像アドレス", "use_api_load_image": "APIを使用して画像を読み込む", "ping_testing": "テスト中", "ping_failed": "失敗しました" }, "categories": { "all": "すべて", "recommend": "おすすめ", "rankings": "ランキング", "random": "ランダム", "game": "ゲーム" }, "settings": { "settings": "設定", "interface": "インターフェース", "network": "ネットワーク", "seal": "封印", "interaction": "インタラクション", "reading": "読書", "download": "ダウンロード", "auto_download_on_favorite": "お気に入り時に自動ダウンロード", "disable_auto_download_on_mobile": "モバイル通信時は自動ダウンロードしない", "auto_delete_download_on_unfavorite": "お気に入り解除で自動削除", "web_server": "ウェブサーバー", "web_server_subtitle": "ローカルネットワーク内のデバイスがブラウザを通じてダウンロードした漫画を閲覧できるようにします", "sync": "同期", "account": "アカウント", "modify_password": "Modify password", "ebook": "電子書籍", "system": "システム", "clear_cache": "キャッシュをクリア", "migrate": "移行", "migrate_subtitle": "データフォルダをメモリカードに変更", "migrate_confirm": "この機能はプログラムを再起動した後に保存されます。よろしいですか", "app_orientation": { "title": "アプリの向き", "choose": "アプリの向きを選択", "normal": "通常", "landscape": "横向き", "portrait": "縦向き" }, "will_pop_notice": "戻るキーを2回連続で押すとアプリを終了します", "android_secure_flag": "スクリーンショットを無効化/タスクビューでの表示を無効化", "android_display_mode": { "title": "画面リフレッシュレート(Android)", "dialog_title": "Android画面リフレッシュレート \n(省電力モードでは高リフレッシュレートになりません)" }, "authentication": "アプリに入る際の認証(システムが既にパスワードまたは指紋を入力している場合)", "set_password": "アプリケーションパスワードを設定", "auto_clean": { "title": "自動キャッシュクリア", "one_month_ago": "1ヶ月前", "one_week_ago": "1週間前", "one_day_ago": "1日前", "no_auto_clean": "自動クリアしない" }, "categories_column_count": { "title": "カテゴリの列数", "choose": "カテゴリの列数を選択", "auto": "自動" }, "categories_sort": { "title": "カテゴリソート" }, "chooser_root": { "title": "フォルダセレクタルートパス", "hint": "フォルダセレクタルートパスを入力してください", "desc": "エクスポート時にディレクトリを選択するデフォルトパス、またルートパスでもあります。エクスポートが正常に動作しない場合は、このオプションを設定してみてください。" }, "content_failed_reload_action": { "title": "コンテンツ読み込み失敗時の再読み込み方法", "choose": "コンテンツ読み込み失敗時の再読み込み方法を選択", "pull_down": "プルダウン", "touch_loader": "画面タッチ" }, "copy_full_name": { "title": "漫画名コピー時にテンプレートを使用" }, "copy_full_name_template": { "title": "漫画名コピーテンプレート", "hint": "漫画名コピーテンプレートを入力してください" }, "copy_skip_confirm": { "title": "長押しコピー時に確認しない" }, "download_and_export_path": { "title": "ダウンロードと同時にファイルシステムへエクスポート", "confirm": "ダウンロードと同時にファイルシステムへエクスポート", "desc": "ディレクトリを選択すると、ファイルシステムが書き込み可能な場合、ダウンロードと同時に自動的にエクスポートされます" }, "download_cache_path": { "title": "他のプログラムのキャッシュを使用してダウンロード加速", "confirm": "他のプログラムのキャッシュを使用してダウンロード加速", "desc": "ディレクトリを選択すると、このディレクトリは次のディレクトリからコピーされたものでなければ使用できません。ダウンロード時にキャッシュフォルダとして優先的に読み込まれます。", "cancel_desc": "他のソフトウェアのダウンロードコンテンツ加速機能をキャンセルしますか?キャンセル後に再度設定することができます", "import_view_log_from_off": { "title": "他のプログラムの履歴記録をインポート", "desc": "ファイルを選択すると、このファイルは次のパスからコピーされたものでなければ使用できません。", "choose_file_dialog_title": "インポートするファイルを選択" } }, "download_thread_count": { "title": "ダウンロードスレッド数", "choose": "ダウンロードスレッド数を選択" }, "ebook_scrolling": { "title": "電子書籍モードスクロールUI" }, "ebook_scrolling_range": { "title": "電子書籍モードスクロールUI", "desc": "スクロール範囲", "screen_height": "画面の高さ" }, "ebook_scrolling_trigger": { "title": "電子書籍モードスクロールUI", "desc": "トリガー距離", "cm": "センチメートル" }, "export_path": { "ios_desc": "ファイルマネージャーでエクスポートしたコンテンツを見つけることができます", "ios_desc2": "iOS デバイスを使用しています:\nファイルにエクスポートしたコンテンツは、システム内蔵のファイルマネージャーを開いてご覧ください", "export_path_desc": "エクスポートパス(クリックして変更)", "android_desc": "Android デバイスを使用しています:\nエクスポートが失敗して権限不足のエラーが発生した場合、Download または Document の下にサブディレクトリを作成してエクスポートを試してみてください" }, "export_rename": { "title": "エクスポート時に名前を変更" }, "yes": "はい", "no": "いいえ", "full_screen_action": { "title": "操作方法", "choose": "操作方法を選択", "touch_once": "画面を一度タッチしてフルスクリーン", "controller": "コントローラーを使用してフルスクリーン", "touch_double": "画面をダブルクリックしてフルスクリーン", "touch_double_once_next": "画面をダブルクリックしてフルスクリーン + 一度クリックで次のページ", "three_area": "画面を3つの領域に分割(前のページ、次のページ、フルスクリーン)" }, "full_screen_ui": { "title": "全屏UI", "choose": "全屏UIを選択", "no": "使用しない", "hidden_bottom": "仮想コントローラーを削除", "all": "フルスクリーン" }, "auto_full_screen": { "title": "リーダーに入ったら自動フルスクリーン" }, "auto_full_screen_on_forward": { "title": "前へ進むときに自動全画面表示" }, "ignore_info_history": { "title": "詳細ページの履歴記録を除外" }, "icon_loading": { "title": "UI アニメーションを最小化" }, "ignore_upgrade_confirm": { "title": "アップグレードポップアップを閉じる" }, "hidden_fd_icon": { "title": "個人スペースの電源アイコンを非表示" }, "hidden_search_persion": { "title": "作者別検索機能を非表示" }, "hidden_viewed": { "title": "読んだ漫画を非表示" }, "hidden_sub_icon": { "title": "購読機能を非表示" }, "hide_online_favorite": { "title": "オンライン收藏を隠す", "desc": "オンライン收藏の入口と收藏ボタンを隠す" }, "hidden_words": { "title": "キーワードで非表示", "clear_all": "クリア確認", "clear_all_desc": "すべてのキーワードをクリアしますか?", "input_hint": "非表示にするキーワードを入力", "no_words": "キーワードなし" }, "image_address": { "title": "画像アドレス", "pinging": "Ping中", "failed": "失敗" }, "image_filter": { "title": "リーダー画像フィルタ", "normal": "通常", "gray": "グレー", "brown": "ブラウン", "choose": "リーダー画像フィルタを選択" }, "import_notice": { "android_desc": "Android デバイスを使用しています:\nインポート/エクスポートができず、権限不足のエラーが発生した場合、Download または Document の下にサブディレクトリを作成してインポートを試してみてください" }, "keyboard_controller": { "title": "リーダーキーボードページ(PC のみ)" }, "list_layout": { "choose": "レイアウトを選択", "info_card": "情報", "only_image": "表紙", "cover_and_title": "表紙 + タイトル" }, "local_history_sync": { "sync_to_local": "履歴をローカルに同期", "not_set": "設定されていません", "sync_success": "同期成功", "sync_failed": "同期失敗", "auto_sync": "履歴をローカルに自動同期", "auto_sync_desc": "アプリケーションを開いた後、履歴が自動的にバックアップされます", "choose_dir": "ディレクトリを選択", "clear_path": "パスをクリア", "clear_path_desc": "パスをクリアしますか?" }, "history_sync": "履歴同期", "local_favorite_sync_title": "ローカル收藏同期", "use_local_favorite": "ローカル收藏を使用", "use_local_favorite_desc": "ローカルで收藏を管理し、フォルダー分類に対応", "local_favorite_sync": { "auto_sync": "ローカル收藏を自動同期", "auto_sync_desc": "WebDAVでローカル收藏を自動同期", "manual_sync": "ローカル收藏を手動同期", "sync_success": "同期成功", "sync_failed": "同期失敗" }, "no_animation": { "title": "ページアニメーションをキャンセル(画面タップ、音量キー、キーボード)" }, "pager_action": { "title": "リストページの読み込み方法", "choose": "リストページの読み込み方法を選択", "controller": "ボタンを使用", "stream": "ストリーム" }, "proxy": { "title": "プロキシサーバー", "hint": "プロキシサーバーを入力", "desc": " ( 例:socks5://127.0.0.1:1080/ ) ", "no_proxy": "設定されていません" }, "quality": { "title": "閲覧時の画像品質", "choose": "画像品質を選択", "original": "オリジナル", "low": "低", "medium": "中", "high": "高" }, "reader_background_color": { "title": "リーダー背景色", "choose": "リーダー背景色を選択", "black": "黒", "gray": "グレー", "white": "白" }, "reader_direction": { "title": "リーダー方向", "choose": "リーダー方向を選択", "top_to_bottom": "上から下へ", "left_to_right": "左から右へ", "right_to_left": "右から左へ" }, "reader_scroll_by_screen_percentage": { "title": "距離でページをめくる長さ", "screen_size": "画面サイズ" }, "web_toon_scroll_mode": { "title": "WebToon ページめくりモード", "choose": "WebToon ページめくりモードを選択", "image": "画像", "screen": "距離" }, "reader_zoom": { "out_title": "縮小倍率(最小ズーム)", "in_title": "拡大倍率(最大ズーム)", "double_tap_title": "ダブルタップズーム倍率" }, "drag_region_lock": { "title": "ドラッグ境界をロック" }, "gesture_speed": { "title": "ジェスチャー速度倍率" }, "reader_slider_position": { "title": "スライダー位置", "choose": "スライダー位置を選択", "bottom": "下", "right": "右", "left": "左" }, "reader_two_page_direction": { "title": "2ページリーダーコンテンツ配置", "choose": "2ページリーダーコンテンツ配置を選択", "close_to": "近づく", "pull_away": "離れる", "each_centered": "それぞれ中央" }, "reader_type": { "title": "リーダーモード", "choose": "リーダーモードを選択", "web_toon": "WebToon(デフォルト)", "web_toon_zoom": "WebToon(ダブルクリックでズーム)", "gallery": "ギャラリー", "web_toon_free_zoom": "WebToon(ListViewダブルクリックでズーム)\n(このモードはプログレスバーが無効)", "two_page_gallery": "2ページモード\n(実験的)", "left_to_right": "左から右へ", "right_to_left": "右から左へ", "two_page_direction": "2ページ方向", "two_page_direction_choose": "2ページ方向を選択" }, "shadow_categories": { "title": "封印", "search_hint": "検索" }, "shadow_categories_mode": { "title": "封印モード", "black_list": "ブラックリスト", "white_list": "ホワイトリスト" }, "startup_pic": { "title": "起動画面設定", "subtitle": "アプリケーション起動時に表示される画像を設定", "clear_title": "起動画面をクリア", "clear_subtitle": "アプリケーション起動時に表示される画像をクリア", "clear_success": "起動画面がクリアされました", "update_success": "起動画面が更新されました" }, "show_comment_at_download": { "title": "ダウンロードでコメントを表示" }, "font": { "title": "フォント", "hint": "フォントを入力してください", "input_hint": "フォント名を入力し、英語のカンマで区切ってください。例:\"MS ゴシック, メイリオ\"、保存後に変更されない場合は、フォントが使用できないか名前が間違っていることを意味します。C:\\Windows\\Fonts を参照してフォントを見つけることができます。flutter2 エンジンバージョンを使用している場合、最初のフォントのみが有効になります。", "choose_hint": "背景エリアをクリックするまで複数のフォントを選択する必要があります" }, "theme": { "origin": "オリジナル", "pink": "ピンク", "black": "ブラック", "dark": "ダーク", "dusty_blue": "ダスティブルー", "dark_black": "ダークブラック", "choose_theme": "テーマを選択", "book": "本", "enable_status_bar_color": "ステータスバーの色を有効にする", "enable_status_restart_hint": "無効になった場合、ステータスバーの色を更新するにはアプリケーションを再起動する必要があります" }, "three_keep_right": { "title": "3領域モードページを常に右に" }, "time_zone": { "title": "タイムゾーン" }, "timeout_lock": { "title": "自動ロック", "notice": "注意:自動ロックは、デスクトップでは最小化後のタイムアウトのみをサポートし、モバイルではバックグラウンドおよび画面ロック後のタイムアウトをサポートします。パスワードが設定されていない場合、自動ロックは無効です。Android およびデスクトップはデスクトップのみをロックし、ダウンロードはロックしません。iOS はテストされていないため、バックグラウンドアクティビティを手動で有効にする必要があります。", "1_hour": "1時間", "10_minutes": "10分", "3_minutes": "3分", "1_minute": "1分", "10_seconds": "10秒", "1_second": "1秒", "no_lock": "ロックしない" }, "using_right_click_pop": { "title": "マウス右クリックで前のページに戻る" }, "volume_controller": { "title": "リーダー音量ボタンページめくり" }, "volume_next_chapter": { "title": "音量/キーボード/コントローラーのダブルクリックで次のチャプター" }, "webdav": { "title": "WebDav", "not_set": "未設定", "path": "WebDav パス", "path_hint": "WebDav パスを入力してください", "username": "WebDav ユーザー名", "username_hint": "WebDav ユーザー名を入力してください", "password": "WebDav パスワード", "password_hint": "WebDav パスワードを入力してください", "auto_sync_history_to_webdav": "履歴を WebDav に自動同期", "sync_history_to_webdav": "履歴を WebDAV に同期", "upload_history_to_webdav": "WebDAV の履歴を上書き", "upload_history_to_webdav_desc": "複数のデバイスがある場合、自動同期機能に注意してください", "sync_success": "同期成功", "sync_failed": "同期失敗" } }, "local_favorite": { "title": "ローカル收藏", "all_folders": "すべて", "new_folder": "新しいフォルダー", "select_mode": "選択", "cancel_select_mode": "選択を終了", "select_all": "すべて選択", "delete_folder": "フォルダーを削除", "move_to_folder": "フォルダーへ移動", "remove_selected": "選択した收藏を削除", "remove_selected_confirm": "選択した漫画をローカル收藏から削除しますか?", "remove_selected_success": "削除しました", "remove_selected_failed": "削除に失敗しました", "select_comics": "先に漫画を選択してください", "folder_limit_reached": "無料版は最大3つのフォルダーまでです。Proにアップグレードすると無制限になります", "batch_download": "一括ダウンロード", "select_folder": "フォルダーを選択", "folder_name": "フォルダー名", "delete_confirm": "フォルダーを削除しますか?", "empty_folder": "まだ收藏がありません", "no_folders": "フォルダーがありません。先に作成してください", "remove_confirm_title": "收藏から削除", "remove_confirm_content": "この漫画をローカル收藏から削除しますか?", "remove_failed": "削除に失敗しました", "load_failed": "読み込みに失敗しました", "add_success": "ローカル收藏に追加しました", "add_failed": "追加に失敗しました", "create_folder_failed": "フォルダー作成に失敗しました", "create_success": "作成しました", "delete_success": "削除しました", "delete_failed": "削除に失敗しました", "move_success": "移動しました", "move_failed": "移動に失敗しました", "select_comics_to_download": "ダウンロードする漫画を選択してください", "download_started": "ダウンロードを開始しました", "download_failed": "ダウンロードに失敗しました" }, "screen": { "about": { "title": "バージョン情報", "version": "ソフトウェアバージョン", "check_update": "アップデートを確認", "tips": "ヒント : \n1. 詳細ページの作者/アップローダー/カテゴリ/タグはクリックできます\n2. 詳細ページの作者/アップローダー/タイトルは長押しでコピーできます\n3. ウォーターフォールフローの代わりにページネーションを使用すると、すばやくページをめくることができます\n4. ダウンロードはローカルにキャッシュすることを意味し、共有するにはエクスポートする必要があります\n5. ダウンロードは長押しで削除できます", "download_new_version": "チャネルから新しいバージョンをダウンロードしてください", "no_new_version": "新しいバージョンは検出されませんでした", "download_release_version": "RELEASE版をダウンロード", "update_content": "更新内容", "go_to_release_repository": "RELEASEリポジトリへ移動" }, "account": { "title": "アカウント", "username": "ユーザー名", "username_hint": "ユーザー名を入力してください", "password": "パスワード", "password_hint": "パスワードを入力してください", "no_account_register": "アカウントがありません、登録したいです", "password_reset": "パスワードをリセット", "check_username_password_or_network": "ユーザー名とパスワードまたはネットワーク環境を確認してください", "check_device_time": "デバイスの時刻を確認してください", "username_or_password_error": "ユーザー名またはパスワードが間違っています", "login_failed": "ログインに失敗しました", "not_set": "未設定" }, "app": { "will_pop_notice": "戻るキーを2回連続で押すとアプリを終了します" }, "categories": { "search_hint": "検索" }, "clean": { "title": "クリーン", "cleaning": "クリーニング中", "clean_network_cache": "ネットワークキャッシュをクリーン", "clean_image_cache": "画像キャッシュをクリーン", "clean_all_cache": "すべてのキャッシュをクリーン", "clean_success": "クリーン成功", "clean_failed": "クリーン失敗" }, "close_app": { "title": "ヒント", "close_app": "アプリを閉じて再度開いてください" }, "comic_collections": { "no_resource": "ここにはリソースがありません" }, "comic_info": { "chapter": "チャプター", "comment": "コメント", "recommend": "おすすめ" }, "comics": { "search_hint": "カテゴリを検索", "choose_category": "カテゴリを選択してください" }, "comic_subscribes": { "update_reminder": "更新リマインダー", "check_update": "更新を確認", "cancel_all_update_reminder": "すべての更新リマインダーをキャンセル" }, "comment": { "title": "コメント", "hint_text": "コメント内容を入力してください", "success": "コメント成功", "i_have_something_to_say": "言いたいことがあります", "please_enter_comment": "コメント内容を入力してください" }, "desktop_authentication": { "current_password": "現在のパスワード", "password_error": "パスワードが間違っています", "password_initialization": "パスワードの初期化", "password": "パスワード", "re_enter_password": "パスワードを再入力", "password_mismatch": "入力された2つのパスワードが異なります", "set_password": "パスワードを設定" }, "download_confirm": { "please_select_ep": "ダウンロードするEPを選択してください", "already_added_to_download_list": "すでにダウンロードリストに追加されています" }, "download_export_group": { "title": "一括エクスポート", "please_select_content": "エクスポートするコンテンツを選択してください", "exporting": "エクスポート中", "export_failed": "エクスポート失敗", "export_success": "エクスポート成功", "export_to_pkz": "PKZにエクスポート\n(暗号化モード、ウェブ検出を防止、pikapikaで開くことができます)", "export_to_pki": "PKIにエクスポート\n(暗号化モード、ウェブ検出を防止、pikapikaでインポートできます)", "export_to_zip": "ZIPにエクスポート\n(非暗号化モード、pikapikaでインポートまたは表示できます)", "export_to_jpeg_zip": "ZIP+JPEGにエクスポート\n(他のリーダーで直接使用できますが、再インポートはできません)", "export_to_jpeg_folder": "フォルダ+JPEGにエクスポート", "export_to_pdf": "PDFにエクスポート", "export_to_epub": "EPUBにエクスポート", "export_to_pdf_folder": "フォルダにエクスポート、各章ごとに1つのPDF", "export_to_cbz": "cbzにエクスポート", "after_power_use": "パワー使用後", "input_save_name": "保存名を入力してください", "export_confirm": "エクスポートの確認", "export_to_pkz_title": "選択した漫画をPKZにエクスポート", "export_to_pki_title": "選択した漫画を個別のPKIにエクスポート", "please_power_up": "最初にパワーアップしてください", "export_to_zip_title": "選択した漫画をZIPにエクスポート", "export_to_jpeg_zip_title": "選択した漫画をZIP+JPEGにエクスポート", "export_to_jpeg_zip_title_not_down_over": "選択した漫画をZIP+JPEGにエクスポート\n(ダウンロードが成功しなくても使用できます)", "export_to_jpeg_folder_title": "選択した漫画をフォルダ+JPEGにエクスポート", "export_to_pdf_title": "選択した漫画をPDFにエクスポート", "export_to_epub_title": "選択した漫画をEPUBにエクスポート", "export_to_pdf_folder_title": "選択した漫画をフォルダにエクスポート、各章ごとに1つのPDF", "export_to_cbz_title": "選択した漫画をcbzにエクスポート", "exporting_please_wait": "エクスポート中です、しばらくお待ちください" }, "download_export_to_file": { "title": "エクスポート", "transfer_to_other_device": "他のデバイスに転送", "input_save_name": "保存名を入力してください", "export_confirm": "エクスポートの確認", "export_to_pkz_title": "選択した漫画をPKZにエクスポート", "export_to_pkz_desc": "xxx.pkzにエクスポート\n(暗号化モード、ウェブ検出を防止、pikapikaで開くことができます)", "export_to_pki_title": "選択した漫画を個別のPKIにエクスポート", "export_to_pki_desc": "xxx.pkiにエクスポート\n(暗号化モード、ウェブ検出を防止、pikapikaでインポートできます)", "export_to_zip_title": "選択した漫画をZIPにエクスポート", "export_to_zip_desc": "xxx.zipにエクスポート\n(非暗号化モード、pikapikaでインポートまたは表示できます)", "export_to_jpeg_zip_title": "選択した漫画をZIP+JPEGにエクスポート", "export_to_jpeg_zip_desc": "xxx.jpegにエクスポート\n(他のリーダーで直接使用できますが、再インポートはできません)", "export_to_pdf_title": "選択した漫画をPDFにエクスポート", "export_to_pdf_desc": "xxx.pdfにエクスポート\n(ダウンロードが成功しなくても使用でき、失敗した画像はスキップされます)\n(フォトアルバムで直接開くことができます)", "export_to_pdf_folder_title": "選択した漫画をフォルダにエクスポート、各章ごとに1つのPDF", "export_to_pdf_folder_desc": "xxx.pdfにエクスポート\n(ダウンロードが成功しなくても使用でき、失敗した画像はスキップされます)\n(フォトアルバムで直接開くことができます)", "export_to_epub_title": "選択した漫画をEPUBにエクスポート", "export_to_epub_desc": "xxx.epubにエクスポート\n(リーダーで直接開くことができます)", "export_to_jpeg_folder_title": "選択した漫画をJPEGS.zipにエクスポート", "export_to_jpeg_folder_desc": "JPGS.zipにエクスポート\n(再インポートはできません)", "export_to_cbz_title": "選択した漫画をcbk.zipにエクスポート", "export_to_cbz_desc": "xxx.cbzにエクスポート、リーダーで直接使用できます(再インポートはできません)" }, "download_export_to_socket": { "title": "ネットワークエクスポート", "loading": "読み込み中", "tips": "転送が成功するまでページを終了しないでください。一度に1つのデバイスしかエクスポートできず、2つのデバイスは同じネットワークセグメントまたは無限LANにある必要があります。他のデバイスでIP:portを入力してください。IPが1つしかない場合は、無限LANのIPを選択してください。通常は192.168で始まります", "get_ip_failed": "IP取得失敗", "getting_ip": "IP取得中", "port": "ポート" }, "download_import": { "title": "インポート", "open_file": "ファイルを開く", "select_file": "インポートするファイルを選択", "import_success": "インポート成功", "import_failed": "インポート失敗", "select_file_desc": "インポートするzipファイルを選択\nインポートするpkiファイルを選択\n読むpkzファイルを選択", "input_address": "エクスポートデバイスから提供されたアドレスを入力してください\n例:「192.168.1.2:50000」", "import_from_other_device": "他のデバイスからインポート", "select_folder_desc": "フォルダを選択\n(フォルダ内のすべてのzip/pkiをインポート)\n(パワー使用後)" }, "download_info": { "loading": "読み込み中", "chapter": "チャプター", "comment": "コメント", "recommend": "おすすめ" }, "download_list": { "search_download": "ダウンロードを検索", "multi_select_operation": "複数選択操作", "download_list": "ダウンロードリスト", "search": "検索", "select_folder": "フォルダーを選択", "download_already_in_delete_queue": "ダウンロードはすでに削除キューにあります", "import": "インポート", "export": "エクスポート", "file": "ファイル", "download_task": "ダウンロードタスク", "pause_download": "ダウンロードを一時停止しますか?", "start_download": "ダウンロードを開始しますか?", "resume_failed": "失敗したタスクを再開", "resume_failed_desc": "すべての失敗したダウンロードが再開されました", "downloading": "ダウンロード中", "paused": "一時停止", "move_download": "ダウンロードを移動", "select_download_to_move": "移動するダウンロードを選択してください", "select_download_to_delete": "削除するダウンロードを選択してください", "input_name": "==> 名前を入力 <==", "empty_folder_will_be_deleted": "(空のフォルダは自動的に削除されます、次回は手動で入力する必要があります)", "folder_name": "フォルダ名", "please_input_folder_name": "フォルダ名を入力してください", "delete_download": "ダウンロードを削除", "delete_selected_download": "選択したダウンロードを削除しますか?", "multi_select": "複数選択" }, "download_only_import": { "importing": "インポート中", "import_success": "インポート成功", "import_failed": "インポート失敗", "click_import_file": "インポートファイルをクリック", "importing_please_wait": "インポート中です、しばらくお待ちください" }, "favourite_paper": { "favourite": "お気に入り" }, "forgot_password": { "title": "パスワードの回復", "username": "ユーザー名", "not_set": "未設定", "confirm": "確認", "please_enter_username": "ユーザー名を入力してください", "question_1": "質問1", "question_2": "質問2", "question_3": "質問3", "answer_1": "回答1", "answer_2": "回答2", "answer_3": "回答3", "please_enter_answer_1": "回答1を入力してください", "please_enter_answer_2": "回答2を入力してください", "please_enter_answer_3": "回答3を入力してください", "use_answer_1_recover": "回答1を使用してパスワードを回復", "use_answer_2_recover": "回答2を使用してパスワードを回復", "use_answer_3_recover": "回答3を使用してパスワードを回復", "please_enter_answer": "回答を入力してください", "new_password_copied": "新しいパスワードがクリップボードにコピーされました", "answer_incorrect": "回答が間違っています", "password": "パスワード" }, "game_download": { "title": "ダウンロード", "download_links_obtained": "ダウンロードリンクが取得されました、そのうちの1つを選択するだけです" }, "game_info": { "download": "ダウンロード", "details": "詳細", "comments": "コメント" }, "games": { "title": "ゲーム" }, "import_from_off": { "title": "インポート", "import_success": "インポート成功", "import_failed": "インポート失敗" }, "modify_password": { "title": "パスワードを変更", "please_wait": "お待ちください", "old_password": "古いパスワード", "new_password": "新しいパスワード", "repeat_new_password": "新しいパスワードを繰り返す", "not_filled": "未入力", "please_enter_old_password": "古いパスワードを入力してください", "please_enter_new_password": "新しいパスワードを入力してください", "please_repeat_new_password": "新しいパスワードを再度入力してください", "new_password_mismatch": "新しいパスワードが一致しません", "modify_success": "変更成功", "failed": "失敗", "confirm": "確認" }, "network_settings": { "title": "ネットワーク設定" }, "pkz_reader": { "reading_downloaded_comic": "ダウンロードした漫画を読んでいます" }, "pro": { "title": "パワーセンター", "power_center": "パワーセンター", "power_status": "パワーステータス", "powered": "パワーオン", "not_powered": "パワーオフ", "pat_membership": "PATメンバーシップ", "pat_status": "PATステータス", "pat_normal": "PAT正常", "pat_bind_hint": "現在のアカウントにバインドしてパワーを得るにはここをクリックしてください", "pat_rebind_hint": "現在のアカウントに再バインドしてパワーを得るにはクリックしてください", "pat_not_detected": "メンバーシップが検出されません、ダウンロードページに移動して参加してください", "i_have_powered": "以前にパワーオンしました", "i_just_powered": "ちょうどパワーオンしました", "enter_code": "コードを入力", "power_method": "パワー方法", "wind_power": "風力発電", "hydro_power": "水力発電", "solar_power": "太陽光発電", "nuclear_power": "原子力発電", "choose_power_method": "パワー方法を選択", "sign_in_exchange": "サインイン/交換", "click_pat_to_change": "変更するには下のPATメンバーシップをクリックしてください", "update_pat_status": "PATパワーステータスを更新", "bind_to_account": "このアカウントにバインド", "change_pat_key": "PATキーを変更", "clear_pat_info": "PAT情報をクリア", "click_to_bind": "クリックしてバインド", "enter_auth_code": "認証コードを入力してください", "please_wait": "お待ちください", "key_recorded": "キー:記録済み", "pat_account": "PATアカウント", "bind_pika_account": "PIKAアカウントをバインド", "bind_account_time": "アカウントのバインド時間", "rebind_time": "再バインド可能時間", "power_features": "パワー機能:マルチスレッドダウンロード/一括インポート/エクスポートダウンロード", "power_guide": "「バージョン情報」ページに移動して、パワーガイドのメンテナンスアドレスを見つけてください\n\n 「以前にパワーオンしました」は対応するパワーステータスを同期できます\n 「ちょうどパワーオンしました」は神秘的なコードを交換します\n 「パワー方法」はネットワークが機能していないときに変更できます\n 「PATメンバーシップ」は独立したパワー方法です" }, "rankings": { "title": "ランキング", "day": "日", "week": "週", "month": "月", "knight": "ナイト", "refresh": "更新", "comics_count": "漫画" }, "random_comics": { "title": "ランダム漫画" }, "register": { "title": "登録", "registering": "登録中", "register_success": "登録成功", "register_failed": "登録失敗", "account_exists": "アカウントはすでに存在します", "name_exists": "名前はすでに存在します", "check_form": "フォームを確認してください、空のフィールドは許可されていません", "account": "アカウント", "password": "パスワード", "nickname": "ニックネーム", "gender": "性別", "birthday": "誕生日", "question_1": "質問1", "answer_1": "回答1", "question_2": "質問2", "answer_2": "回答2", "question_3": "質問3", "answer_3": "回答3", "not_set": "未設定", "please_enter_account": "アカウントを入力してください", "please_enter_password": "パスワードを入力してください", "please_enter_nickname": "ニックネームを入力してください", "please_enter_question_1": "質問1を入力してください", "please_enter_answer_1": "回答1を入力してください", "please_enter_question_2": "質問2を入力してください", "please_enter_answer_2": "回答2を入力してください", "please_enter_question_3": "質問3を入力してください", "please_enter_answer_3": "回答3を入力してください", "account_desc": "(小文字+数字/ログイン用)", "password_desc": "(大文字と小文字+数字/ 8文字以上)", "nickname_desc": "(日本語可/ 2-50文字)", "choose_gender": "性別を選択してください", "futa": "ふたなり", "male": "男性", "female": "女性", "register_success_desc": "登録に成功しました、ログインに戻ってください", "account_label": "アカウント", "nickname_label": "ニックネーム" }, "search": { "title": "検索", "search_hint": "検索", "choose_category": "カテゴリを選択してください" }, "search_author": { "title": "作者で検索", "search_hint": "作者で検索+", "by_author": "作者:" }, "space": { "title": "マイページ", "logout": "ログアウト", "logout_confirm": "現在のアカウントからログアウトしますか?", "my_favourites": "お気に入り", "view_history": "閲覧履歴", "my_downloads": "マイダウンロード" }, "theme": { "title": "テーマ設定", "theme": "テーマ", "dark_mode_different_theme": "ダークモードで異なるテーマを使用", "dark_mode_theme": "テーマ(ダークモード)" }, "view_logs": { "title": "閲覧履歴", "clear_all": "すべての閲覧履歴をクリアしますか?", "clear_all_desc": "読書の進捗も削除されます!", "clear_one": "この閲覧履歴をクリアしますか?", "clear_one_desc": "読書の進捗も削除されます!", "clear_selected": "選択した閲覧履歴をクリアしますか?", "clear_selected_desc": "読書の進捗も削除されます!", "categories": "カテゴリ" }, "web_server": { "title": "ダウンロード - Webサーバー", "loading": "読み込み中", "get_ip_failed": "IP取得失敗", "getting_ip": "IP取得中", "port": "ポート:8080", "usage_instruction": "ブラウザで「http://device_ip:8080/」と入力して、ダウンロードした漫画にアクセスします", "leave_notice": "このページを離れるとサーバーが閉じます" } }, "components": { "comic_info_card": { "categories": "カテゴリ", "finished": "完結", "viewed": "閲覧済み" }, "comic_list": { "shadow": "シャドウコミック" }, "common": { "display_mode": "表示モード", "shadow_mode": "シャドウモード", "shadow_list": "シャドウリスト", "batch_download": "一括ダウンロード" }, "image_reader": { "already_at_the_end": "すでに最後に達しています", "click_to_next_chapter": "クリックして次の章へ", "reload_page": "ページを再読み込み", "next_chapter": "次の章", "end_reading": "読書を終了", "reload_image": "画像を再読み込み", "save_image_in_this_page": "このページの画像を保存", "image_load_failed": "画像の読み込みに失敗しました" } } } ================================================ FILE: lib/assets/translations/ko-KR.json ================================================ { "language": { "title": "언어", "name": "한국어 - 대한민국" }, "app": { "categories": "카테고리", "my": "내 정보", "copied_to_clipboard": "클립보드에 복사되었습니다", "not_supported_platform": "지원되지 않는 플랫폼", "cancel": "취소", "confirm": "확인", "save_cancel": "저장이 취소되었습니다", "save_success": "저장에 성공했습니다", "save_failed": "저장에 실패했습니다", "pro": "프로", "pro_required": "이 기능을 사용하려면 프로로 업그레이드하세요", "choose_folder": "파일을 저장할 폴더를 선택하세요", "permission_denied": "권한이 거부되었습니다", "loading": "로딩 중", "error": "오류", "pat": { "success": "스폰서 로그인이 성공했습니다. 돌아가세요", "title": "PAT 계정 교체" }, "previous_page": "이전 페이지", "next_page": "다음 페이지", "page": "페이지", "please_enter_page_number": "페이지 번호를 입력하세요:", "select_all": "모두 선택", "load_failed": "로딩에 실패했습니다", "all": "모두", "delete": "삭제", "save_image": "이미지 저장", "preview_image": "이미지 미리보기", "please_select": "선택하세요", "refresh": "새로고침", "initializing": "초기화 중", "like_failed": "좋아요에 실패했습니다", "network_error": "네트워크 오류입니다. 네트워크를 확인하세요", "no_permission": "권한이 없거나 경로를 사용할 수 없습니다", "check_device_time": "장치 시간을 확인하세요", "resource_not_available": "리소스를 사용할 수 없습니다", "something_went_wrong": "문제가 발생했습니다", "click_refresh": "클릭하여 새로고침", "pull_down_refresh": "아래로 당겨서 새로고침", "continue_reading": "계속 읽기", "start_reading": "읽기 시작", "image_crop": "이미지 자르기", "download": "다운로드", "download_failed": "다운로드에 실패했습니다", "download_finished": "다운로드 완료", "downloading": "다운로드 중", "queue": "대기열", "deleting": "삭제 중", "please_select_comic": "만화를 선택하세요", "please_choose": "선택하세요", "last_viewed": "마지막으로 본", "auto_punch": "자동 체크인", "yes": "예", "no": "아니오", "confirm_download": "다운로드 확인", "copy": "복사" }, "net": { "no_address": "주소 없음", "address": "주소", "address_sync": "주소 동기화", "address_sync_from_server": "서버에서 최신 주소 가져오기", "address_sync_reset": "주소를 기본값으로 재설정", "address_sync_success": "주소 동기화에 성공했습니다", "address_sync_failed": "주소 동기화에 실패했습니다", "address_sync_reset_success": "주소 동기화 재설정에 성공했습니다", "address_sync_reset_failed": "주소 동기화 재설정에 실패했습니다", "choose_address": "주소 선택", "image_address": "이미지 주소", "use_api_load_image": "API를 사용하여 이미지 로드", "ping_testing": "테스트 중", "ping_failed": "실패했습니다" }, "categories": { "all": "모두", "recommend": "추천", "rankings": "랭킹", "random": "랜덤", "game": "게임" }, "settings": { "settings": "설정", "interface": "인터페이스", "network": "네트워크", "seal": "봉인", "interaction": "상호작용", "reading": "읽기", "download": "다운로드", "auto_download_on_favorite": "즐겨찾기 시 자동 다운로드", "disable_auto_download_on_mobile": "모바일 데이터에서는 자동 다운로드 안 함", "auto_delete_download_on_unfavorite": "즐겨찾기 해제 시 자동 삭제", "web_server": "웹 서버", "web_server_subtitle": "로컬 네트워크의 장치가 브라우저를 통해 다운로드한 만화를 볼 수 있게 합니다", "sync": "동기화", "account": "계정", "modify_password": "Modify password", "ebook": "전자책", "system": "시스템", "clear_cache": "캐시 지우기", "migrate": "이전", "migrate_subtitle": "데이터 폴더를 메모리 카드로 변경", "migrate_confirm": "이 기능은 프로그램을 다시 시작한 후에 저장됩니다. 확실합니까", "app_orientation": { "title": "앱 방향", "choose": "앱 방향 선택", "normal": "일반", "landscape": "가로", "portrait": "세로" }, "will_pop_notice": "뒤로 키를 연속으로 두 번 눌러 앱을 종료합니다", "android_secure_flag": "스크린샷 비활성화/작업 보기에서 표시 비활성화", "android_display_mode": { "title": "화면 새로고침 속도(Android)", "dialog_title": "Android 화면 새로고침 속도 \n(절전 모드에서는 고주사율이 적용되지 않음)" }, "authentication": "앱에 들어갈 때 인증 (시스템이 이미 비밀번호나 지문을 입력한 경우)", "set_password": "애플리케이션 비밀번호 설정", "auto_clean": { "title": "자동 캐시 정리", "one_month_ago": "한 달 전", "one_week_ago": "일주일 전", "one_day_ago": "하루 전", "no_auto_clean": "자동 정리 안 함" }, "categories_column_count": { "title": "카테고리 열 수", "choose": "카테고리 열 수 선택", "auto": "자동" }, "categories_sort": { "title": "카테고리 정렬" }, "chooser_root": { "title": "폴더 선택기 루트 경로", "hint": "폴더 선택기 루트 경로를 입력하세요", "desc": "내보낼 때 디렉토리 선택의 기본 경로이며, 루트 경로이기도 합니다. 내보내기가 정상적으로 작동하지 않을 때 이 옵션을 설정해 볼 수 있습니다." }, "content_failed_reload_action": { "title": "콘텐츠 로드 실패 시 새로고침 방식", "choose": "콘텐츠 로드 실패 시 새로고침 방식 선택", "pull_down": "아래로 당기기", "touch_loader": "화면 터치" }, "copy_full_name": { "title": "만화 이름 복사 시 템플릿 사용" }, "copy_full_name_template": { "title": "만화 이름 복사 템플릿", "hint": "만화 이름 복사 템플릿을 입력하세요" }, "copy_skip_confirm": { "title": "긴 터치 복사 시 확인 안 함" }, "download_and_export_path": { "title": "다운로드와 동시에 파일 시스템으로 내보내기", "confirm": "다운로드와 동시에 파일 시스템으로 내보내기", "desc": "디렉토리를 선택하면, 파일 시스템이 쓰기 가능한 경우 다운로드와 동시에 자동으로 내보내집니다" }, "download_cache_path": { "title": "다른 프로그램의 캐시를 사용하여 다운로드 가속화", "confirm": "다른 프로그램의 캐시를 사용하여 다운로드 가속화", "desc": "디렉토리를 선택하면, 이 디렉토리는 다음 디렉토리에서 복사된 것이어야 사용할 수 있습니다. 다운로드 시 캐시 폴더로 우선 읽어집니다.", "cancel_desc": "다른 소프트웨어의 다운로드 콘텐츠 가속화 기능을 취소하시겠습니까? 취소 후 다시 설정할 수 있습니다", "import_view_log_from_off": { "title": "다른 프로그램의 기록 가져오기", "desc": "파일을 선택하면, 이 파일은 다음 경로에서 복사된 것이어야 사용할 수 있습니다.", "choose_file_dialog_title": "가져올 파일 선택" } }, "download_thread_count": { "title": "다운로드 스레드 수", "choose": "다운로드 스레드 수 선택" }, "ebook_scrolling": { "title": "전자책 모드 스크롤 UI" }, "ebook_scrolling_range": { "title": "전자책 모드 스크롤 UI", "desc": "스크롤 범위", "screen_height": "화면 높이" }, "ebook_scrolling_trigger": { "title": "전자책 모드 스크롤 UI", "desc": "트리거 거리", "cm": "센티미터" }, "export_path": { "ios_desc": "파일 관리자에서 내보낸 콘텐츠를 찾을 수 있습니다", "ios_desc2": "iOS 장치를 사용하고 있습니다:\n파일로 내보낸 콘텐츠는 시스템 내장 파일 관리자를 열어서 찾아보세요", "export_path_desc": "내보내기 경로 (클릭하여 수정)", "android_desc": "Android 장치를 사용하고 있습니다:\n내보내기가 실패하고 권한 부족 오류가 발생하면, Download 또는 Document 하위에 서브디렉토리를 만들어 내보내기를 시도해 보세요" }, "export_rename": { "title": "내보낼 때 이름 바꾸기" }, "yes": "예", "no": "아니오", "full_screen_action": { "title": "조작 방식", "choose": "조작 방식 선택", "touch_once": "화면 한 번 터치로 전체 화면", "controller": "컨트롤러 사용으로 전체 화면", "touch_double": "화면 두 번 터치로 전체 화면", "touch_double_once_next": "화면 두 번 터치로 전체 화면 + 한 번 터치로 다음 페이지", "three_area": "화면을 세 영역으로 나누기 (이전 페이지, 다음 페이지, 전체 화면)" }, "full_screen_ui": { "title": "전체 화면 UI", "choose": "전체 화면 UI 선택", "no": "사용 안 함", "hidden_bottom": "가상 컨트롤러 제거", "all": "전체 화면" }, "auto_full_screen": { "title": "리더 진입 시 자동 전체 화면" }, "auto_full_screen_on_forward": { "title": "앞으로 갈 때 자동 전체 화면" }, "ignore_info_history": { "title": "상세 페이지 기록 제외" }, "icon_loading": { "title": "UI 애니메이션 최소화" }, "ignore_upgrade_confirm": { "title": "업그레이드 팝업 닫기" }, "hidden_fd_icon": { "title": "개인 공간의 전원 아이콘 숨기기" }, "hidden_search_persion": { "title": "작가별 검색 기능 숨기기" }, "hidden_viewed": { "title": "읽은 만화 숨기기" }, "hidden_sub_icon": { "title": "구독 기능 숨기기" }, "hide_online_favorite": { "title": "온라인 즐겨찾기 숨기기", "desc": "온라인 즐겨찾기 진입 및 즐겨찾기 버튼 숨기기" }, "hidden_words": { "title": "키워드로 숨기기", "clear_all": "지우기 확인", "clear_all_desc": "모든 키워드를 지우시겠습니까?", "input_hint": "숨길 키워드를 입력하세요", "no_words": "키워드 없음" }, "image_address": { "title": "이미지 주소", "pinging": "핑 테스트 중", "failed": "실패" }, "image_filter": { "title": "리더 이미지 필터", "normal": "일반", "gray": "회색", "brown": "갈색", "choose": "리더 이미지 필터 선택" }, "import_notice": { "android_desc": "Android 장치를 사용하고 있습니다:\n가져오기/내보내기가 안 되고 권한 부족 오류가 발생하면, Download 또는 Document 하위에 서브디렉토리를 만들어 가져오기를 시도해 보세요" }, "keyboard_controller": { "title": "리더 키보드 페이지 (PC만 해당)" }, "list_layout": { "choose": "레이아웃 선택", "info_card": "정보", "only_image": "표지", "cover_and_title": "표지 + 제목" }, "local_history_sync": { "sync_to_local": "기록을 로컬에 동기화", "not_set": "설정되지 않음", "sync_success": "동기화 성공", "sync_failed": "동기화 실패", "auto_sync": "기록을 로컬에 자동 동기화", "auto_sync_desc": "애플리케이션을 열 때마다 기록이 자동으로 백업됩니다", "choose_dir": "디렉터리 선택", "clear_path": "경로 지우기", "clear_path_desc": "경로를 지우시겠습니까?" }, "history_sync": "기록 동기화", "local_favorite_sync_title": "로컬 즐겨찾기 동기화", "use_local_favorite": "로컬 즐겨찾기 사용", "use_local_favorite_desc": "즐겨찾기를 로컬에서 관리하고 폴더 분류를 지원합니다", "local_favorite_sync": { "auto_sync": "로컬 즐겨찾기 자동 동기화", "auto_sync_desc": "WebDAV로 로컬 즐겨찾기를 자동 동기화", "manual_sync": "로컬 즐겨찾기 수동 동기화", "sync_success": "동기화 성공", "sync_failed": "동기화 실패" }, "no_animation": { "title": "페이지 애니메이션 취소 (화면 터치, 볼륨 키, 키보드)" }, "pager_action": { "title": "목록 페이지 로딩 방법", "choose": "목록 페이지 로딩 방법 선택", "controller": "버튼 사용", "stream": "스트림" }, "proxy": { "title": "프록시 서버", "hint": "프록시 서버를 입력하세요", "desc": " ( 예: socks5://127.0.0.1:1080/ ) ", "no_proxy": "설정되지 않음" }, "quality": { "title": "탐색 시 이미지 품질", "choose": "이미지 품질 선택", "original": "원본", "low": "낮음", "medium": "보통", "high": "높음" }, "reader_background_color": { "title": "리더 배경색", "choose": "리더 배경색 선택", "black": "검은색", "gray": "회색", "white": "흰색" }, "reader_direction": { "title": "리더 방향", "choose": "리더 방향 선택", "top_to_bottom": "위에서 아래로", "left_to_right": "왼쪽에서 오른쪽으로", "right_to_left": "오른쪽에서 왼쪽으로" }, "reader_scroll_by_screen_percentage": { "title": "거리별 페이지 넘김 길이", "screen_size": "화면 크기" }, "web_toon_scroll_mode": { "title": "WebToon 페이지 넘김 모드", "choose": "WebToon 페이지 넘김 모드 선택", "image": "이미지", "screen": "거리" }, "reader_zoom": { "out_title": "축소 배율 (최소 확대)", "in_title": "확대 배율 (최대 확대)", "double_tap_title": "더블탭 확대 배율" }, "drag_region_lock": { "title": "드래그 경계 잠금" }, "gesture_speed": { "title": "제스처 속도 배율" }, "reader_slider_position": { "title": "슬라이더 위치", "choose": "슬라이더 위치 선택", "bottom": "아래", "right": "오른쪽", "left": "왼쪽" }, "reader_two_page_direction": { "title": "두 페이지 리더 콘텐츠 배열", "choose": "두 페이지 리더 콘텐츠 배열 선택", "close_to": "가깝게", "pull_away": "멀리", "each_centered": "각각 중앙" }, "reader_type": { "title": "리더 모드", "choose": "리더 모드 선택", "web_toon": "웹툰 (기본)", "web_toon_zoom": "웹툰 (더블 클릭으로 확대)", "gallery": "갤러리", "web_toon_free_zoom": "웹툰 (ListView 더블 클릭으로 확대)\n(이 모드는 진행률 표시줄이 무효)", "two_page_gallery": "두 페이지 모드\n(실험적)", "left_to_right": "왼쪽에서 오른쪽으로", "right_to_left": "오른쪽에서 왼쪽으로", "two_page_direction": "두 페이지 방향", "two_page_direction_choose": "두 페이지 방향 선택" }, "shadow_categories": { "title": "봉인", "search_hint": "검색" }, "shadow_categories_mode": { "title": "봉인 모드", "black_list": "블랙 리스트", "white_list": "화이트 리스트" }, "startup_pic": { "title": "시작 화면 설정", "subtitle": "애플리케이션 시작 시 표시되는 이미지 설정", "clear_title": "시작 화면 지우기", "clear_subtitle": "애플리케이션 시작 시 표시되는 이미지 지우기", "clear_success": "시작 화면이 지워졌습니다", "update_success": "시작 화면이 업데이트되었습니다" }, "show_comment_at_download": { "title": "다운로드에서 댓글 표시" }, "font": { "title": "글꼴", "hint": "글꼴을 입력하세요", "input_hint": "글꼴 이름을 입력하고 영어 쉼표로 구분하세요. 예: \"굴림, 돋움\", 저장 후 변경되지 않으면 글꼴을 사용할 수 없거나 이름이 잘못되었음을 의미합니다. C:\\Windows\\Fonts를 참조하여 글꼴을 찾을 수 있습니다. flutter2 엔진 버전을 사용하는 경우 첫 번째 글꼴만 적용됩니다.", "choose_hint": "배경 영역을 클릭할 때까지 여러 글꼴을 선택해야 합니다" }, "theme": { "origin": "원본", "pink": "핑크", "black": "검은색", "dark": "다크", "dusty_blue": "먼지 파란색", "dark_black": "진한 검은색", "choose_theme": "테마 선택", "book": "책", "enable_status_bar_color": "상태 표시줄 색상 활성화", "enable_status_restart_hint": "비활성화된 경우 상태 표시줄 색상을 새로 고치려면 애플리케이션을 다시 시작해야 합니다" }, "three_keep_right": { "title": "세 영역 모드 페이지를 항상 오른쪽으로" }, "time_zone": { "title": "시간대" }, "timeout_lock": { "title": "자동 잠금", "notice": "참고: 자동 잠금은 데스크톱에서 최소화 후 타임아웃만 지원하며, 모바일에서는 백그라운드 및 화면 잠금 후 타임아웃을 지원합니다. 비밀번호가 설정되지 않은 경우 자동 잠금이 무효입니다. Android와 데스크톱은 데스크톱만 잠그고 다운로드는 잠그지 않으며, iOS는 테스트되지 않았으므로 백그라운드 활동을 수동으로 활성화해야 합니다.", "1_hour": "1시간", "10_minutes": "10분", "3_minutes": "3분", "1_minute": "1분", "10_seconds": "10초", "1_second": "1초", "no_lock": "잠금 안 함" }, "using_right_click_pop": { "title": "마우스 오른쪽 클릭으로 이전 페이지로 돌아가기" }, "volume_controller": { "title": "리더 볼륨 버튼 페이지 넘기기" }, "volume_next_chapter": { "title": "볼륨/키보드/컨트롤러 더블 클릭으로 다음 챕터" }, "webdav": { "title": "WebDav", "not_set": "설정되지 않음", "path": "WebDav 경로", "path_hint": "WebDav 경로를 입력하세요", "username": "WebDav 사용자명", "username_hint": "WebDav 사용자명을 입력하세요", "password": "WebDav 비밀번호", "password_hint": "WebDav 비밀번호를 입력하세요", "auto_sync_history_to_webdav": "WebDav에 기록 자동 동기화", "sync_history_to_webdav": "기록을 WebDAV에 동기화", "upload_history_to_webdav": "WebDAV의 기록 덮어쓰기", "upload_history_to_webdav_desc": "여러 장치가 있는 경우 자동 동기화 기능에 유의하세요", "sync_success": "동기화 성공", "sync_failed": "동기화 실패" } }, "local_favorite": { "title": "로컬 즐겨찾기", "all_folders": "전체", "new_folder": "새 폴더", "select_mode": "선택", "cancel_select_mode": "선택 취소", "select_all": "전체 선택", "delete_folder": "폴더 삭제", "move_to_folder": "폴더로 이동", "remove_selected": "선택 항목 제거", "remove_selected_confirm": "선택한 만화를 로컬 즐겨찾기에서 제거할까요?", "remove_selected_success": "제거됨", "remove_selected_failed": "제거 실패", "select_comics": "먼저 만화를 선택하세요", "folder_limit_reached": "무료 버전은 폴더를 최대 3개까지 만들 수 있습니다. Pro로 업그레이드하면 무제한입니다", "batch_download": "일괄 다운로드", "select_folder": "폴더 선택", "folder_name": "폴더 이름", "delete_confirm": "폴더를 삭제하시겠습니까?", "empty_folder": "아직 즐겨찾기가 없습니다", "no_folders": "폴더가 없습니다. 먼저 폴더를 만들어주세요", "remove_confirm_title": "즐겨찾기에서 제거", "remove_confirm_content": "이 만화를 로컬 즐겨찾기에서 제거하시겠습니까?", "remove_failed": "제거 실패", "load_failed": "불러오기 실패", "add_success": "로컬 즐겨찾기에 추가됨", "add_failed": "추가 실패", "create_folder_failed": "폴더 생성 실패", "create_success": "생성됨", "delete_success": "삭제됨", "delete_failed": "삭제 실패", "move_success": "이동됨", "move_failed": "이동 실패", "select_comics_to_download": "다운로드할 만화를 선택하세요", "download_started": "다운로드 시작됨", "download_failed": "다운로드 실패" }, "screen": { "about": { "title": "정보", "version": "소프트웨어 버전", "check_update": "업데이트 확인", "tips": "팁 : \n1. 상세 페이지의 작가/업로더/카테고리/태그는 클릭 가능합니다\n2. 상세 페이지의 작가/업로더/제목은 길게 눌러 복사할 수 있습니다\n3. 폭포수 흐름 대신 페이지네이션을 사용하면 빠르게 페이지를 넘길 수 있습니다\n4. 다운로드는 로컬에 캐시하는 것을 의미하며, 공유하려면 내보내야 합니다\n5. 다운로드는 길게 눌러 삭제할 수 있습니다", "download_new_version": "채널에서 새 버전을 다운로드하십시오", "no_new_version": "새 버전이 없습니다", "download_release_version": "RELEASE 버전 다운로드", "update_content": "업데이트 내용", "go_to_release_repository": "RELEASE 저장소로 이동" }, "account": { "title": "계정", "username": "사용자 이름", "username_hint": "사용자 이름을 입력하세요", "password": "비밀번호", "password_hint": "비밀번호를 입력하세요", "no_account_register": "계정이 없습니다, 등록하고 싶습니다", "password_reset": "비밀번호 재설정", "check_username_password_or_network": "사용자 이름과 비밀번호 또는 네트워크 환경을 확인하세요", "check_device_time": "장치 시간을 확인하세요", "username_or_password_error": "사용자 이름 또는 비밀번호 오류", "login_failed": "로그인 실패", "not_set": "설정되지 않음" }, "app": { "will_pop_notice": "뒤로 키를 연속으로 두 번 눌러 앱을 종료합니다" }, "categories": { "search_hint": "검색" }, "clean": { "title": "정리", "cleaning": "정리 중", "clean_network_cache": "네트워크 캐시 정리", "clean_image_cache": "이미지 캐시 정리", "clean_all_cache": "모든 캐시 정리", "clean_success": "정리 성공", "clean_failed": "정리 실패" }, "close_app": { "title": "팁", "close_app": "앱을 닫고 다시 열어주세요" }, "comic_collections": { "no_resource": "여기에 리소스가 없습니다" }, "comic_info": { "chapter": "챕터", "comment": "댓글", "recommend": "추천" }, "comics": { "search_hint": "카테고리 검색", "choose_category": "카테고리를 선택하세요" }, "comic_subscribes": { "update_reminder": "업데이트 알림", "check_update": "업데이트 확인", "cancel_all_update_reminder": "모든 업데이트 알림 취소" }, "comment": { "title": "댓글", "hint_text": "댓글 내용을 입력하세요", "success": "댓글 성공", "i_have_something_to_say": "할 말이 있습니다", "please_enter_comment": "댓글 내용을 입력하세요" }, "desktop_authentication": { "current_password": "현재 비밀번호", "password_error": "비밀번호 오류", "password_initialization": "비밀번호 초기화", "password": "비밀번호", "re_enter_password": "비밀번호 다시 입력", "password_mismatch": "입력한 두 비밀번호가 다릅니다", "set_password": "비밀번호 설정" }, "download_confirm": { "please_select_ep": "다운로드할 EP를 선택하세요", "already_added_to_download_list": "이미 다운로드 목록에 추가되었습니다" }, "download_export_group": { "title": "일괄 내보내기", "please_select_content": "내보낼 콘텐츠를 선택하세요", "exporting": "내보내는 중", "export_failed": "내보내기 실패", "export_success": "내보내기 성공", "export_to_pkz": "PKZ로 내보내기\n(암호화 모드, 웹 감지 방지, pikapika로 열 수 있음)", "export_to_pki": "PKI로 내보내기\n(암호화 모드, 웹 감지 방지, pikapika로 가져올 수 있음)", "export_to_zip": "ZIP으로 내보내기\n(암호화되지 않은 모드, pikapika로 가져오거나 볼 수 있음)", "export_to_jpeg_zip": "ZIP+JPEG로 내보내기\n(다른 리더에서 직접 사용할 수 있으며, 다시 가져올 수 없음)", "export_to_jpeg_folder": "폴더+JPEG로 내보내기", "export_to_pdf": "PDF로 내보내기", "export_to_epub": "EPUB로 내보내기", "export_to_pdf_folder": "폴더로 내보내기, 각 챕터마다 PDF 하나씩", "export_to_cbz": "cbz로 내보내기", "after_power_use": "전원 사용 후", "input_save_name": "저장할 이름을 입력하세요", "export_confirm": "내보내기 확인", "export_to_pkz_title": "선택한 만화를 PKZ로 내보내기", "export_to_pki_title": "선택한 만화를 별도의 PKI로 내보내기", "please_power_up": "먼저 전원을 켜주세요", "export_to_zip_title": "선택한 만화를 ZIP으로 내보내기", "export_to_jpeg_zip_title": "선택한 만화를 ZIP+JPEG로 내보내기", "export_to_jpeg_zip_title_not_down_over": "선택한 만화를 ZIP+JPEG로 내보내기\n(다운로드가 성공하지 않아도 사용할 수 있음)", "export_to_jpeg_folder_title": "선택한 만화를 폴더+JPEG로 내보내기", "export_to_pdf_title": "선택한 만화를 PDF로 내보내기", "export_to_epub_title": "선택한 만화를 EPUB로 내보내기", "export_to_pdf_folder_title": "선택한 만화를 폴더로 내보내기, 각 챕터마다 PDF 하나씩", "export_to_cbz_title": "선택한 만화를 cbz로 내보내기", "exporting_please_wait": "내보내는 중, 잠시만 기다려주세요" }, "download_export_to_file": { "title": "내보내기", "transfer_to_other_device": "다른 장치로 전송", "input_save_name": "저장할 이름을 입력하세요", "export_confirm": "내보내기 확인", "export_to_pkz_title": "선택한 만화를 PKZ로 내보내기", "export_to_pkz_desc": "xxx.pkz로 내보내기\n(암호화 모드, 웹 감지 방지, pikapika로 열 수 있음)", "export_to_pki_title": "선택한 만화를 별도의 PKI로 내보내기", "export_to_pki_desc": "xxx.pki로 내보내기\n(암호화 모드, 웹 감지 방지, pikapika로 가져올 수 있음)", "export_to_zip_title": "선택한 만화를 ZIP으로 내보내기", "export_to_zip_desc": "xxx.zip으로 내보내기\n(암호화되지 않은 모드, pikapika로 가져오거나 볼 수 있음)", "export_to_jpeg_zip_title": "선택한 만화를 ZIP+JPEG로 내보내기", "export_to_jpeg_zip_desc": "xxx.jpeg로 내보내기\n(다른 리더에서 직접 사용할 수 있으며, 다시 가져올 수 없음)", "export_to_pdf_title": "선택한 만화를 PDF로 내보내기", "export_to_pdf_desc": "xxx.pdf로 내보내기\n(다운로드가 성공하지 않아도 사용할 수 있으며, 실패한 이미지는 건너뜁니다)\n(사진 앨범에서 직접 열 수 있음)", "export_to_pdf_folder_title": "선택한 만화를 폴더로 내보내기, 각 챕터마다 PDF 하나씩", "export_to_pdf_folder_desc": "xxx.pdf로 내보내기\n(다운로드가 성공하지 않아도 사용할 수 있으며, 실패한 이미지는 건너뜁니다)\n(사진 앨범에서 직접 열 수 있음)", "export_to_epub_title": "선택한 만화를 EPUB로 내보내기", "export_to_epub_desc": "xxx.epub로 내보내기\n(리더에서 직접 열 수 있음)", "export_to_jpeg_folder_title": "선택한 만화를 JPEGS.zip으로 내보내기", "export_to_jpeg_folder_desc": "JPGS.zip으로 내보내기\n(다시 가져올 수 없음)", "export_to_cbz_title": "선택한 만화를 cbk.zip으로 내보내기", "export_to_cbz_desc": "xxx.cbz로 내보내기, 리더에서 직접 사용할 수 있음 (다시 가져올 수 없음)" }, "download_export_to_socket": { "title": "네트워크 내보내기", "loading": "로딩 중", "tips": "전송이 성공하기 전에 페이지를 나가지 마십시오. 한 번에 하나의 장치만 내보낼 수 있으며, 두 장치는 동일한 네트워크 세그먼트 또는 무한 LAN에 있어야 합니다. 다른 장치에서 IP:port를 입력하십시오. IP가 하나만 있는 경우 무한 LAN의 IP를 선택하십시오. 일반적으로 192.168로 시작합니다", "get_ip_failed": "IP 가져오기 실패", "getting_ip": "IP 가져오는 중", "port": "포트" }, "download_import": { "title": "가져오기", "open_file": "파일 열기", "select_file": "가져올 파일 선택", "import_success": "가져오기 성공", "import_failed": "가져오기 실패", "select_file_desc": "가져올 zip 파일 선택\n가져올 pki 파일 선택\n읽을 pkz 파일 선택", "input_address": "내보내기 장치에서 제공한 주소를 입력하세요\n예: \"192.168.1.2:50000\"", "import_from_other_device": "다른 장치에서 가져오기", "select_folder_desc": "폴더 선택\n(폴더의 모든 zip/pki 가져오기)\n(전원 사용 후)" }, "download_info": { "loading": "로딩 중", "chapter": "챕터", "comment": "댓글", "recommend": "추천" }, "download_list": { "search_download": "다운로드 검색", "multi_select_operation": "다중 선택 작업", "download_list": "다운로드 목록", "search": "검색", "select_folder": "폴더 선택", "download_already_in_delete_queue": "다운로드가 이미 삭제 대기열에 있습니다", "import": "가져오기", "export": "내보내기", "file": "파일", "download_task": "다운로드 작업", "pause_download": "다운로드를 일시 중지하시겠습니까?", "start_download": "다운로드를 시작하시겠습니까?", "resume_failed": "실패한 작업 다시 시작", "resume_failed_desc": "모든 실패한 다운로드가 다시 시작되었습니다", "downloading": "다운로드 중", "paused": "일시 중지됨", "move_download": "다운로드 이동", "select_download_to_move": "이동할 다운로드를 선택하세요", "select_download_to_delete": "삭제할 다운로드를 선택하세요", "input_name": "==> 이름 입력 <==", "empty_folder_will_be_deleted": "(빈 폴더는 자동으로 삭제되며, 다음에 수동으로 입력해야 합니다)", "folder_name": "폴더 이름", "please_input_folder_name": "폴더 이름을 입력하세요", "delete_download": "다운로드 삭제", "delete_selected_download": "선택한 다운로드를 삭제하시겠습니까?", "multi_select": "다중 선택" }, "download_only_import": { "importing": "가져오는 중", "import_success": "가져오기 성공", "import_failed": "가져오기 실패", "click_import_file": "가져올 파일 클릭", "importing_please_wait": "가져오는 중, 잠시만 기다려주세요" }, "favourite_paper": { "favourite": "즐겨찾기" }, "forgot_password": { "title": "비밀번호 복구", "username": "사용자 이름", "not_set": "설정되지 않음", "confirm": "확인", "please_enter_username": "사용자 이름을 입력하세요", "question_1": "질문 1", "question_2": "질문 2", "question_3": "질문 3", "answer_1": "답변 1", "answer_2": "답변 2", "answer_3": "답변 3", "please_enter_answer_1": "답변 1을 입력하세요", "please_enter_answer_2": "답변 2를 입력하세요", "please_enter_answer_3": "답변 3을 입력하세요", "use_answer_1_recover": "답변 1을 사용하여 비밀번호 복구", "use_answer_2_recover": "답변 2를 사용하여 비밀번호 복구", "use_answer_3_recover": "답변 3을 사용하여 비밀번호 복구", "please_enter_answer": "답변을 입력하세요", "new_password_copied": "새 비밀번호가 클립보드에 복사되었습니다", "answer_incorrect": "답변이 올바르지 않습니다", "password": "비밀번호" }, "game_download": { "title": "다운로드", "download_links_obtained": "다운로드 링크를 얻었습니다. 그 중 하나를 선택하면 됩니다" }, "game_info": { "download": "다운로드", "details": "상세 정보", "comments": "댓글" }, "games": { "title": "게임" }, "import_from_off": { "title": "가져오기", "import_success": "가져오기 성공", "import_failed": "가져오기 실패" }, "modify_password": { "title": "비밀번호 수정", "please_wait": "잠시만 기다려주세요", "old_password": "이전 비밀번호", "new_password": "새 비밀번호", "repeat_new_password": "새 비밀번호 반복", "not_filled": "채워지지 않음", "please_enter_old_password": "이전 비밀번호를 입력하세요", "please_enter_new_password": "새 비밀번호를 입력하세요", "please_repeat_new_password": "새 비밀번호를 다시 입력하세요", "new_password_mismatch": "새 비밀번호가 일치하지 않습니다", "modify_success": "수정 성공", "failed": "실패", "confirm": "확인" }, "network_settings": { "title": "네트워크 설정" }, "pkz_reader": { "reading_downloaded_comic": "다운로드한 만화를 읽고 있습니다" }, "pro": { "title": "파워 센터", "power_center": "파워 센터", "power_status": "파워 상태", "powered": "전원 켜짐", "not_powered": "전원 꺼짐", "pat_membership": "PAT 멤버십", "pat_status": "PAT 상태", "pat_normal": "PAT 정상", "pat_bind_hint": "전원을 위해 현재 계정에 바인딩하려면 여기를 클릭하세요", "pat_rebind_hint": "전원을 위해 현재 계정에 다시 바인딩하려면 클릭하세요", "pat_not_detected": "멤버십이 감지되지 않았습니다. 다운로드 페이지로 이동하여 가입하세요", "i_have_powered": "이전에 전원을 켰습니다", "i_just_powered": "방금 전원을 켰습니다", "enter_code": "코드 입력", "power_method": "전원 방식", "wind_power": "풍력 발전", "hydro_power": "수력 발전", "solar_power": "태양광 발전", "nuclear_power": "원자력 발전", "choose_power_method": "전원 방식 선택", "sign_in_exchange": "로그인/교환", "click_pat_to_change": "변경하려면 아래의 PAT 멤버십을 클릭하세요", "update_pat_status": "PAT 전원 상태 업데이트", "bind_to_account": "이 계정에 바인딩", "change_pat_key": "PAT 키 변경", "clear_pat_info": "PAT 정보 지우기", "click_to_bind": "바인딩하려면 클릭", "enter_auth_code": "인증 코드를 입력하세요", "please_wait": "잠시만 기다려주세요", "key_recorded": "키: 기록됨", "pat_account": "PAT 계정", "bind_pika_account": "PIKA 계정 바인딩", "bind_account_time": "계정 바인딩 시간", "rebind_time": "재바인딩 가능 시간", "power_features": "파워 기능: 다중 스레드 다운로드 / 일괄 가져오기/내보내기 다운로드", "power_guide": "\"정보\" 페이지로 이동하여 파워 가이드를 위한 유지 관리 주소를 찾으십시오\n\n \"이전에 전원을 켰습니다\"는 해당 파워 상태를 동기화할 수 있습니다\n \"방금 전원을 켰습니다\"는 신비한 코드를 교환합니다\n \"전원 방식\"은 네트워크가 작동하지 않을 때 변경할 수 있습니다\n \"PAT 멤버십\"은 독립적인 전원 방식입니다" }, "rankings": { "title": "순위", "day": "일", "week": "주", "month": "월", "knight": "기사", "refresh": "새로고침", "comics_count": "만화" }, "random_comics": { "title": "랜덤 만화" }, "register": { "title": "등록", "registering": "등록 중", "register_success": "등록 성공", "register_failed": "등록 실패", "account_exists": "계정이 이미 존재합니다", "name_exists": "이름이 이미 존재합니다", "check_form": "양식을 확인하세요, 빈 필드는 허용되지 않습니다", "account": "계정", "password": "비밀번호", "nickname": "닉네임", "gender": "성별", "birthday": "생일", "question_1": "질문 1", "answer_1": "답변 1", "question_2": "질문 2", "answer_2": "답변 2", "question_3": "질문 3", "answer_3": "답변 3", "not_set": "설정되지 않음", "please_enter_account": "계정을 입력하세요", "please_enter_password": "비밀번호를 입력하세요", "please_enter_nickname": "닉네임을 입력하세요", "please_enter_question_1": "질문 1을 입력하세요", "please_enter_answer_1": "답변 1을 입력하세요", "please_enter_question_2": "질문 2를 입력하세요", "please_enter_answer_2": "답변 2를 입력하세요", "please_enter_question_3": "질문 3을 입력하세요", "please_enter_answer_3": "답변 3을 입력하세요", "account_desc": "(소문자 + 숫자 / 로그인용)", "password_desc": "(대소문자 + 숫자 / 8자 이상)", "nickname_desc": "(한글 허용 / 2-50자)", "choose_gender": "성별을 선택하세요", "futa": "후타", "male": "남성", "female": "여성", "register_success_desc": "성공적으로 등록되었습니다. 로그인으로 돌아가세요", "account_label": "계정", "nickname_label": "닉네임" }, "search": { "title": "검색", "search_hint": "검색", "choose_category": "카테고리를 선택하세요" }, "search_author": { "title": "작가별 검색", "search_hint": "작가별 검색 + ", "by_author": "작가별: " }, "space": { "title": "내 정보", "logout": "로그아웃", "logout_confirm": "현재 계정에서 로그아웃하시겠습니까?", "my_favourites": "내 즐겨찾기", "view_history": "조회 기록", "my_downloads": "내 다운로드" }, "theme": { "title": "테마 설정", "theme": "테마", "dark_mode_different_theme": "다크 모드에서 다른 테마 사용", "dark_mode_theme": "테마 (다크 모드)" }, "view_logs": { "title": "조회 기록", "clear_all": "모든 조회 기록을 지우시겠습니까?", "clear_all_desc": "읽기 진행 상황도 삭제됩니다!", "clear_one": "이 조회 기록을 지우시겠습니까?", "clear_one_desc": "읽기 진행 상황도 삭제됩니다!", "clear_selected": "선택한 조회 기록을 지우시겠습니까?", "clear_selected_desc": "읽기 진행 상황도 삭제됩니다!", "categories": "카테고리" }, "web_server": { "title": "다운로드 - 웹 서버", "loading": "로딩 중", "get_ip_failed": "IP 가져오기 실패", "getting_ip": "IP 가져오는 중", "port": "포트: 8080", "usage_instruction": "브라우저에 \"http://device_ip:8080/\"를 입력하여 다운로드한 만화에 액세스", "leave_notice": "이 페이지를 나가면 서버가 닫힙니다" } }, "components": { "comic_info_card": { "categories": "카테고리", "finished": "완결", "viewed": "조회함" }, "comic_list": { "shadow": "그림자 만화" }, "common": { "display_mode": "표시 모드", "shadow_mode": "그림자 모드", "shadow_list": "그림자 목록", "batch_download": "일괄 다운로드" }, "image_reader": { "already_at_the_end": "이미 끝에 도달했습니다", "click_to_next_chapter": "다음 챕터로 이동하려면 클릭", "reload_page": "페이지 새로고침", "next_chapter": "다음 챕터", "end_reading": "읽기 종료", "reload_image": "이미지 새로고침", "save_image_in_this_page": "이 페이지의 이미지 저장", "image_load_failed": "이미지 로드 실패" } } } ================================================ FILE: lib/assets/translations/zh-CN.json ================================================ { "language": { "title": "语言", "name": "简体中文 - 中国大陆" }, "app": { "categories": "分类", "my": "我的", "copied_to_clipboard": "已复制到剪切板", "not_supported_platform": "暂不支持该平台", "cancel": "取消", "confirm": "确定", "save_cancel": "保存取消", "save_success": "保存成功", "save_failed": "保存失败", "pro": "发电", "pro_required": "请先发电再使用", "choose_folder": "选择一个文件夹, 将文件保存到这里", "permission_denied": "申请权限被拒绝", "loading": "加载中", "error": "错误", "pat": { "success": "您的赞助登录成功, 请返回", "title": "更换PAT账户" }, "previous_page": "上一页", "next_page": "下一页", "page": "页", "please_enter_page_number": "请输入页数:", "select_all": "全选", "load_failed": "加载失败", "all": "全部", "delete": "删除", "save_image": "保存图片", "preview_image": "预览图片", "please_select": "请选择", "refresh": "刷新", "initializing": "初始化", "like_failed": "点赞失败", "network_error": "连接不上啦, 请检查网络", "no_permission": "没有权限或路径不可用", "check_device_time": "请检查设备时间", "resource_not_available": "资源未审核或不可用", "something_went_wrong": "啊哦, 被玩坏了", "click_refresh": "点击刷新", "pull_down_refresh": "下拉刷新", "continue_reading": "继续阅读", "start_reading": "开始阅读", "image_crop": "图片裁剪", "download": "下载", "download_failed": "下载失败", "download_finished": "下载完成", "downloading": "下载中", "queue": "队列中", "deleting": "删除中", "please_select_comic": "请选择漫画", "please_choose": "请选择", "last_viewed": "上次观看到", "auto_punch": "自动打卡", "yes": "是", "no": "否", "confirm_download": "确认下载", "copy": "复制" }, "net": { "no_address": "不分流", "address": "分流", "address_sync": "分流同步", "address_sync_from_server": "从服务器获取最新的分流地址", "address_sync_reset": "重制分流为默认值", "address_sync_success": "分流同步成功", "address_sync_failed": "分流同步失败", "address_sync_reset_success": "分流重制成功", "address_sync_reset_failed": "分流重制失败", "choose_address": "选择分流", "image_address": "图片分流", "use_api_load_image": "用API加载图片", "ping_testing": "测速中", "ping_failed": "失败" }, "categories": { "all": "全分类", "recommend": "推荐", "rankings": "排行榜", "random": "随机本子", "game": "游戏专区" }, "settings": { "settings": "设置", "interface": "界面", "network": "网络", "seal": "封印", "interaction": "交互", "reading": "阅读", "download": "下载", "auto_download_on_favorite": "收藏时自动下载", "disable_auto_download_on_mobile": "移动网络下收藏时不自动下载", "auto_delete_download_on_unfavorite": "取消收藏时自动删除下载", "web_server": "启动Web服务器", "web_server_subtitle": "让局域网内的设备通过浏览器看下载的漫画", "sync": "同步", "history_sync": "历史记录同步", "local_favorite_sync_title": "本地收藏夹同步", "use_local_favorite": "使用本地收藏夹", "use_local_favorite_desc": "在本地管理收藏,支持文件夹分类", "account": "账户", "modify_password": "修改密码", "ebook": "电纸书", "system": "系统", "clear_cache": "清除缓存", "migrate": "文件迁移", "migrate_subtitle": "更换您的数据文件夹到内存卡", "migrate_confirm": "此功能菜单保存后, 需要重启程序, 您确认吗", "app_orientation": { "title": "APP方向", "choose": "请选择APP方向", "normal": "正常", "landscape": "横屏", "portrait": "竖屏" }, "will_pop_notice": "在首页连续按两下返回键才能退出APP", "android_secure_flag": "禁止截图/禁止显示在任务视图", "android_display_mode": { "title": "屏幕刷新率(安卓)", "dialog_title": "安卓屏幕刷新率 \n(省电模式下不会高刷)" }, "authentication": "进入APP时验证身份(如果系统已经录入密码或指纹)", "set_password": "设置应用程序密码", "auto_clean": { "title": "自动清理缓存", "one_month_ago": "一个月前", "one_week_ago": "一周前", "one_day_ago": "一天前", "no_auto_clean": "不自动清理" }, "categories_column_count": { "title": "首页分类展示列数", "choose": "选择首页分类展示列数", "auto": "自动" }, "categories_sort": { "title": "首页分类排序" }, "chooser_root": { "title": "文件夹选择器根路径", "hint": "请输入文件夹选择器根路径", "desc": "导出时选择目录的默认路径, 同时也是根路径, 不能正常导出时也可以尝试设置此选项。" }, "content_failed_reload_action": { "title": "页面加载失败刷新方式", "choose": "选择页面加载失败刷新方式", "pull_down": "下拉刷新", "touch_loader": "点击屏幕刷新" }, "copy_full_name": { "title": "复制漫画名称时使用模版" }, "copy_full_name_template": { "title": "复制漫画名称模版", "hint": "模版内容" }, "copy_skip_confirm": { "title": "长按复制不需要确认" }, "download_and_export_path": { "title": "下载的同时导出到文件系统", "confirm": "下载的同时导出到文件系统", "desc": "您即将选择一个目录, 如果文件系统可写, 下载的同时会为您自动导出一份" }, "download_cache_path": { "title": "使用其他程序的缓存下载加速", "confirm": "使用其他程序的缓存下载加速", "desc": "您即将选择一个目录, 这个目录拷贝自以下目录才能使用。下载时将会作为缓存文件夹优先读取。 ", "cancel_desc": "您确定取消使用其他软件的下载内容加速的功能吗? 取消之后您可以再次点击设置", "import_view_log_from_off": { "title": "导入其他程序的历史记录", "desc": "您即将选择一个文件, 这个文件拷贝自以下路径才能使用。", "choose_file_dialog_title": "选择要导入的文件" } }, "download_thread_count": { "title": "下载线程数", "choose": "选择下载线程数" }, "ebook_scrolling": { "title": "电子书模式滚动UI" }, "ebook_scrolling_range": { "title": "电子书模式滚动UI", "desc": "滚动幅度", "screen_height": "屏幕高度" }, "ebook_scrolling_trigger": { "title": "电子书模式滚动UI", "desc": "触发距离", "cm": "厘米" }, "export_path": { "ios_desc": "随后可在文件管理中找到导出的内容", "ios_desc2": "您正在使用iOS设备:\n导出到文件的内容请打开系统自带文件管理进行浏览", "export_path_desc": "导出路径 (点击可修改)", "android_desc": "您正在使用安卓设备:\n如果不能成功导出并且提示权限不足, 可以尝试在Download或Document下建立子目录进行导出" }, "export_rename": { "title": "导出时进行重命名" }, "yes": "是", "no": "否", "full_screen_action": { "title": "操控方式", "choose": "选择操控方式", "touch_once": "点击屏幕一次全屏", "controller": "使用控制器全屏", "touch_double": "双击屏幕全屏", "touch_double_once_next": "双击屏幕全屏 + 单击屏幕下一页", "three_area": "将屏幕划分成三个区域 (上一页, 下一页, 全屏)" }, "full_screen_ui": { "title": "全屏UI", "choose": "选择全屏UI", "no": "不使用", "hidden_bottom": "去除虚拟控制器", "all": "全屏" }, "auto_full_screen": { "title": "进入阅读器自动全屏" }, "auto_full_screen_on_forward": { "title": "前进时自动全屏" }, "ignore_info_history": { "title": "详情页不计入历史记录" }, "icon_loading": { "title": "尽量减少UI动画" }, "ignore_upgrade_confirm": { "title": "关闭升级弹窗" }, "hidden_fd_icon": { "title": "隐藏个人空间的发电图标" }, "hidden_search_persion": { "title": "隐藏按作者搜索功能" }, "hidden_viewed": { "title": "隐藏阅读过的漫画" }, "hidden_sub_icon": { "title": "隐藏订阅功能" }, "hide_online_favorite": { "title": "隐藏在线收藏", "desc": "隐藏在线收藏入口与收藏按钮" }, "hidden_words": { "title": "根据关键词隐藏", "clear_all": "确认清空", "clear_all_desc": "确定要清空所有关键词吗?", "input_hint": "输入要隐藏的关键词", "no_words": "暂无关键词" }, "image_address": { "title": "图片分流", "pinging": "测速中", "failed": "失败" }, "image_filter": { "title": "阅读器图片滤镜", "normal": "正常", "gray": "灰度", "brown": "棕褐色", "choose": "选择阅读器图片滤镜" }, "import_notice": { "android_desc": "您正在使用安卓设备:\n如果不能导入导出并且提示权限不足, 可以尝试在Download或Document下建立子目录进行导入" }, "keyboard_controller": { "title": "阅读器键盘翻页(仅PC)" }, "list_layout": { "choose": "请选择布局", "info_card": "详情", "only_image": "封面", "cover_and_title": "封面+标题" }, "local_history_sync": { "sync_to_local": "同步历史记录到本地", "not_set": "未设置", "sync_success": "同步成功", "sync_failed": "同步失败", "auto_sync": "自动同步历史记录到本地", "auto_sync_desc": "开启后每次打开应用会自动备份历史记录", "choose_dir": "选择目录", "clear_path": "清除路径", "clear_path_desc": "确定要清除路径吗?" }, "local_favorite_sync": { "auto_sync": "自动同步本地收藏夹", "auto_sync_desc": "使用WebDAV自动同步本地收藏夹", "manual_sync": "手动同步本地收藏夹", "sync_success": "同步成功", "sync_failed": "同步失败" }, "no_animation": { "title": "取消翻页动画(点按屏幕、音量键、键盘)" }, "pager_action": { "title": "列表页加载方式", "choose": "选择列表页加载方式", "controller": "使用按钮", "stream": "瀑布流" }, "proxy": { "title": "代理服务器", "hint": "请输入代理服务器", "desc": " ( 例如 socks5://127.0.0.1:1080/ ) ", "no_proxy": "未设置" }, "quality": { "title": "浏览时的图片质量", "choose": "请选择图片质量", "original": "原图", "low": "低", "medium": "中", "high": "高" }, "reader_background_color": { "title": "阅读器背景色", "choose": "选择阅读器背景色", "black": "黑色", "gray": "灰度", "white": "白色" }, "reader_direction": { "title": "阅读器方向", "choose": "选择翻页方向", "top_to_bottom": "从上到下", "left_to_right": "从左到右", "right_to_left": "从右到左" }, "reader_scroll_by_screen_percentage": { "title": "按距离翻页长度", "screen_size": "屏幕尺寸" }, "web_toon_scroll_mode": { "title": "WebToon 翻页模式", "choose": "选择 WebToon 翻页模式", "image": "图片", "screen": "距离" }, "reader_zoom": { "out_title": "缩小倍数(最小缩放)", "in_title": "放大倍数(最大缩放)", "double_tap_title": "双击缩放倍数" }, "drag_region_lock": { "title": "锁定拖动边界" }, "gesture_speed": { "title": "手势速度倍率" }, "reader_slider_position": { "title": "滚动条的位置", "choose": "选择滑动条位置", "bottom": "下方", "right": "右侧", "left": "左侧" }, "reader_two_page_direction": { "title": "双页阅读器内容排列", "choose": "选择双页阅读器内容排列", "close_to": "靠近", "pull_away": "远离", "each_centered": "各自居中" }, "reader_type": { "title": "阅读器模式", "choose": "选择阅读器模式", "web_toon": "WebToon (默认)", "web_toon_zoom": "WebToon (双击放大)", "gallery": "相册", "web_toon_free_zoom": "WebToon (ListView双击放大)\n(此模式进度条无效)", "two_page_gallery": "双页模式\n(实验)", "left_to_right": "从左到右", "right_to_left": "从右到左", "two_page_direction": "双页方向", "two_page_direction_choose": "选择双页方向" }, "shadow_categories": { "title": "封印", "search_hint": "搜索" }, "shadow_categories_mode": { "title": "封印模式", "black_list": "黑名单", "white_list": "白名单" }, "startup_pic": { "title": "设置启动图片", "subtitle": "设置应用启动时显示的图片", "clear_title": "清除启动图片", "clear_subtitle": "清除应用启动时显示的图片", "clear_success": "启动图片已清除", "update_success": "启动图片已更新" }, "show_comment_at_download": { "title": "在下载显示评论区" }, "font": { "title": "字体", "hint": "请输入字体", "input_hint": "请输入字体的名称且用英文逗号分隔, 例如 \"宋体,黑体\", 如果您保存后没有发生变化, 说明字体无法使用或名称错误, 可以去参考C:\\Windows\\Fonts寻找您的字体。若您使用的是flutter2引擎的版本,只有第一个字体生效。", "choose_hint": "需要您选择多个字体,直至您点击背景区域" }, "theme": { "origin": "原生", "pink": "粉色", "black": "酷黑", "dark": "暗黑", "dusty_blue": "灰蓝", "dark_black": "纯黑", "choose_theme": "选择主题", "book": "书本", "enable_status_bar_color": "启用状态栏颜色", "enable_status_restart_hint": "关闭时需要重新启动应用程序刷新状态栏颜色" }, "three_keep_right": { "title": "三区域模式翻页始终为右侧下一页" }, "time_zone": { "title": "时区" }, "timeout_lock": { "title": "自动锁定", "notice": "注意:自动锁定在桌面端仅支持最小化后超时,手机端支持后台以及锁屏后超时。如果没有设置密码,自动锁定无效。安卓以及桌面端只会锁定桌面,不会锁定下载,iOS未测试,需要手动开启后台活动。", "1_hour": "一小时", "10_minutes": "十分钟", "3_minutes": "三分钟", "1_minute": "一分钟", "10_seconds": "十秒", "1_second": "一秒", "no_lock": "不锁定" }, "using_right_click_pop": { "title": "鼠标右键返回上一页" }, "volume_controller": { "title": "阅读器音量键翻页" }, "volume_next_chapter": { "title": "双击 音量/键盘/控制器 下一章节" }, "webdav": { "title": "WebDav", "not_set": "未设置", "path": "WebDav 路径", "path_hint": "请输入WebDav 路径", "username": "WebDav 用户名", "username_hint": "请输入WebDav 用户名", "password": "WebDav 密码", "password_hint": "请输入WebDav 密码", "auto_sync_history_to_webdav": "开启时自动同步浏览记录到WebDav", "sync_history_to_webdav": "立即同步浏览记录到WebDAV", "upload_history_to_webdav": "覆盖WebDAV中的浏览记录", "upload_history_to_webdav_desc": "如有多台设备,请注意自动同步功能", "sync_success": "同步成功", "sync_failed": "同步失败" } }, "local_favorite": { "title": "本地收藏夹", "all_folders": "全部", "new_folder": "新建文件夹", "select_mode": "多选", "cancel_select_mode": "退出多选", "select_all": "全选", "delete_folder": "删除文件夹", "move_to_folder": "移动到文件夹", "remove_selected": "移除选中收藏", "remove_selected_confirm": "确认从本地收藏夹中移除选中的漫画?", "remove_selected_success": "已移除", "remove_selected_failed": "移除失败", "select_comics": "请先选择漫画", "folder_limit_reached": "免费版最多创建3个文件夹,升级Pro解锁无限制", "batch_download": "批量下载", "select_folder": "选择文件夹", "folder_name": "文件夹名称", "delete_confirm": "确认删除文件夹?", "empty_folder": "暂无收藏", "no_folders": "还没有文件夹,请先创建", "remove_confirm_title": "取消收藏", "remove_confirm_content": "确定要从本地收藏夹中移除这部漫画吗?", "remove_failed": "取消收藏失败", "load_failed": "加载失败", "add_success": "已添加到本地收藏夹", "add_failed": "添加失败", "create_folder_failed": "创建文件夹失败", "create_success": "创建成功", "delete_success": "删除成功", "delete_failed": "删除失败", "move_success": "移动成功", "move_failed": "移动失败", "select_comics_to_download": "请选择要下载的漫画", "download_started": "开始下载", "download_failed": "下载失败" }, "screen": { "about": { "title": "关于", "version": "软件版本", "check_update": "检查更新", "tips": "提示 : \n1. 详情页的作者/上传者/分类/标签都可以点击\n2. 详情页的作者/上传者/标题长按可以复制\n3. 使用分页而不是瀑布流点击页码可以快速翻页\n4. 下载指的是缓存到本地, 需要导出才可以分享\n5. 下载长按可以删除", "download_new_version": "请从获取渠道下载新版", "no_new_version": "未检测到新版本", "download_release_version": "下载RELEASE版", "update_content": "更新内容", "go_to_release_repository": "去RELEASE仓库" }, "account": { "title": "账户", "username": "账号", "username_hint": "请输入账号", "password": "密码", "password_hint": "请输入密码", "no_account_register": "没有账号,我要注册", "password_reset": "密码找回", "check_username_password_or_network": "请检查账号密码或网络环境", "check_device_time": "请检查设备时间", "username_or_password_error": "账号或密码错误", "login_failed": "登录失败", "not_set": "未设置" }, "app": { "will_pop_notice": "再次返回将会退出应用程序" }, "categories": { "search_hint": "搜索" }, "clean": { "title": "清理", "cleaning": "清理中", "clean_network_cache": "清理网络缓存", "clean_image_cache": "清理图片缓存", "clean_all_cache": "清理全部缓存", "clean_success": "清理成功", "clean_failed": "清理失败" }, "close_app": { "title": "提示", "close_app": "请关闭应用重新打开" }, "comic_collections": { "no_resource": "这里没有资源呀" }, "comic_info": { "chapter": "章节", "comment": "评论", "recommend": "推荐" }, "comics": { "search_hint": "搜索分类", "choose_category": "请选择分类" }, "comic_subscribes": { "update_reminder": "更新提醒", "check_update": "检查更新", "cancel_all_update_reminder": "取消所有更新提醒" }, "comment": { "title": "评论", "hint_text": "请输入评论内容", "success": "评论成功", "i_have_something_to_say": "我有话要讲", "please_enter_comment": "请输入评论内容" }, "desktop_authentication": { "current_password": "当前密码", "password_error": "密码错误", "password_initialization": "密码初始化", "password": "密码", "re_enter_password": "再次输入密码", "password_mismatch": "两次输入的密码不一致", "set_password": "设置密码" }, "download_confirm": { "please_select_ep": "请选择下载的EP", "already_added_to_download_list": "已经加入下载列表" }, "download_export_group": { "title": "批量导出", "please_select_content": "请选择导出的内容", "exporting": "正在导出", "export_failed": "导出失败", "export_success": "导出成功", "export_to_pkz": "导出成一个PKZ\n(加密模式,防止网盘检测,能用pikapika打开观看)", "export_to_pki": "每部漫画都打包一个PKI\n(加密模式,防止网盘检测,能用pikapika导入)", "export_to_zip": "每部漫画都打包一个ZIP\n(不加密模式,能用pikapika导入或网页浏览器观看)", "export_to_jpeg_zip": "每部漫画都打包一个ZIP+JPEG\n(能直接使用其他阅读器看,不可再导入)", "export_to_jpeg_folder": "每部漫画都导出成文件夹+JPEG", "export_to_pdf": "每部漫画都导出成PDF", "export_to_epub": "每部漫画都导出成EPUB", "export_to_pdf_folder": "每部漫画都导出到文件夹, 每个章节一个PDF", "export_to_cbz": "每部漫画都导出成cbz", "after_power_use": "发电后使用", "input_save_name": "请输入保存后的名称", "export_confirm": "导出确认", "export_to_pkz_title": "将导出您所选的漫画为一个PKZ", "export_to_pki_title": "将您所选的漫画分别导出成单独的PKI", "please_power_up": "请先发电鸭", "export_to_zip_title": "将您所选的漫画分别导出成ZIP", "export_to_jpeg_zip_title": "将您所选的漫画分别导出成ZIP+JPEG", "export_to_jpeg_zip_title_not_down_over": "将您所选的漫画分别导出成ZIP+JPEG\n(即便没有下载完成也可以使用)", "export_to_jpeg_folder_title": "将您所选的漫画分别导出成文件夹+JPEG", "export_to_pdf_title": "将您所选的漫画分别导出成PDF", "export_to_epub_title": "将您所选的漫画分别导出成EPUB", "export_to_pdf_folder_title": "将您所选的漫画分别导出到文件夹, 每个章节一个PDF", "export_to_cbz_title": "将您所选的漫画分别导出成cbz", "exporting_please_wait": "导出中, 请稍后" }, "download_export_to_file": { "title": "导出", "transfer_to_other_device": "传输到其他设备", "input_save_name": "请输入保存后的名称", "export_confirm": "导出确认", "export_to_pkz_title": "将您所选的漫画导出PKZ", "export_to_pkz_desc": "导出到xxx.pkz\n(可直接打开观看的格式,不支持导入)\n(可以躲避网盘或者聊天软件的扫描)", "export_to_pki_title": "将您所选的漫画导出PKI", "export_to_pki_desc": "导出到xxx.pki\n(只支持导入, 不支持直接阅读)\n(可以躲避网盘或者聊天软件的扫描)\n(后期版本可能支持直接阅读)", "export_to_zip_title": "将您所选的漫画导出HTML+ZIP", "export_to_zip_desc": "导出到xxx.zip\n(可从其他设备导入 / 解压后可阅读)", "export_to_jpeg_zip_title": "将您所选的漫画导出HTML+JPEG", "export_to_jpeg_zip_desc": "导出到xxx.jpeg\n(可直接在相册中打开观看)", "export_to_pdf_title": "将您所选的漫画导出PDF", "export_to_pdf_desc": "导出到xxx.pdf\n(即使没有下载成功也可以使用、未成功下载的图片将会跳过)\n(可直接在相册中打开观看)", "export_to_pdf_folder_title": "将您所选的漫画导出到文件夹, 每个章节一个PDF", "export_to_pdf_folder_desc": "导出到xxx.pdf\n(即使没有下载成功也可以使用、未成功下载的图片将会跳过)\n(可直接在相册中打开观看)", "export_to_epub_title": "将您所选的漫画导出EPUB", "export_to_epub_desc": "导出到xxx.epub\n(可直接在阅读器中打开观看)", "export_to_jpeg_folder_title": "将您所选的漫画导出JPEGS.zip", "export_to_jpeg_folder_desc": "导出阅读器用JPGS.zip\n(不可再导入)", "export_to_cbz_title": "将您所选的漫画导出cbk.zip", "export_to_cbz_desc": "导出到xxx.cbz, 阅读器可以直接使用(不可再导入)" }, "download_export_to_socket": { "title": "网络导出", "loading": "加载中", "tips": "传输成功之前请不要退出页面, 一次只能导出到一个设备, 两台设备需要在同一网段或无限局域网中, 请另外一台设备输入 IP:端口 , 有一个IP时请选择无限局域网的IP, 通常是192.168开头", "get_ip_failed": "获取IP失败", "getting_ip": "正在获取IP", "port": "端口号" }, "download_import": { "title": "导入", "open_file": "打开文件", "select_file": "选择要导入的文件", "import_success": "导入成功", "import_failed": "导入失败", "select_file_desc": "选择zip文件进行导入\n选择pki文件进行导入\n选择pkz文件进行阅读", "input_address": "请输入导出设备提供的地址\n例如 \"192.168.1.2:50000\"", "import_from_other_device": "从其他设备导入", "select_folder_desc": "选择文件夹\n(导入里面所有的zip/pki)\n(发电后使用)" }, "download_info": { "loading": "加载中", "chapter": "章节", "comment": "评论", "recommend": "推荐" }, "download_list": { "search_download": "搜索下载", "multi_select_operation": "多选操作", "download_list": "下载列表", "search": "搜索", "select_folder": "选择文件夹", "download_already_in_delete_queue": "该下载已经在删除队列中", "import": "导入", "export": "导出", "file": "文件", "download_task": "下载任务", "pause_download": "暂停下载吗?", "start_download": "启动下载吗?", "resume_failed": "恢复失败任务", "resume_failed_desc": "所有失败的下载已经恢复", "downloading": "下载中", "paused": "暂停中", "move_download": "移动下载", "select_download_to_move": "请选择要移动下载", "select_download_to_delete": "请选择要删除下载", "input_name": "==> 输入名称 <==", "empty_folder_will_be_deleted": "(空文件夹将会自动删除,下次需要手动输入)", "folder_name": "文件夹名称", "please_input_folder_name": "请输入文件夹名称", "delete_download": "删除下载", "delete_selected_download": "删除选中的下载吗?", "multi_select": "多选" }, "download_only_import": { "importing": "正在导入", "import_success": "导入成功", "import_failed": "导入失败", "click_import_file": "点击导入文件", "importing_please_wait": "导入中, 请稍后" }, "favourite_paper": { "favourite": "收藏" }, "forgot_password": { "title": "找回密码", "username": "账号", "not_set": "未设置", "confirm": "确认", "please_enter_username": "请输入账号", "question_1": "问题1", "question_2": "问题2", "question_3": "问题3", "answer_1": "回答1", "answer_2": "回答2", "answer_3": "回答3", "please_enter_answer_1": "请输入回答1", "please_enter_answer_2": "请输入回答2", "please_enter_answer_3": "请输入回答3", "use_answer_1_recover": "使用回答1找回密码", "use_answer_2_recover": "使用回答2找回密码", "use_answer_3_recover": "使用回答3找回密码", "please_enter_answer": "请输入答案", "new_password_copied": "新密码正在复制到剪切板", "answer_incorrect": "答案不正确", "password": "密码" }, "game_download": { "title": "下载", "download_links_obtained": "获取到下载链接, 您只需要选择其中一个" }, "game_info": { "download": "下载", "details": "详情", "comments": "评论" }, "games": { "title": "游戏" }, "import_from_off": { "title": "导入", "import_success": "导入成功", "import_failed": "导入失败" }, "modify_password": { "title": "修改密码", "please_wait": "请稍后", "old_password": "旧密码", "new_password": "新密码", "repeat_new_password": "重复输入新密码", "not_filled": "未填写", "please_enter_old_password": "请输入旧密码", "please_enter_new_password": "请输入新密码", "please_repeat_new_password": "请重复输入新密码", "new_password_mismatch": "新密码不匹配", "modify_success": "修改成功", "failed": "失败", "confirm": "确认" }, "network_settings": { "title": "网络设置" }, "pkz_reader": { "reading_downloaded_comic": "您阅读的是下载漫画" }, "pro": { "title": "发电中心", "power_center": "发电中心", "power_status": "发电状态", "powered": "发电中", "not_powered": "未发电", "pat_membership": "PAT入会", "pat_status": "PAT状态", "pat_normal": "PAT正常", "pat_bind_hint": "请点击这里绑定到当前账号发电", "pat_rebind_hint": "请点换绑到当前账号发电", "pat_not_detected": "未检测到入会, 请到下载页入会", "i_have_powered": "我曾经发过电", "i_just_powered": "我刚才发了电", "enter_code": "输入代码", "power_method": "发电方式", "wind_power": "风力发电", "hydro_power": "水力发电", "solar_power": "光伏发电", "nuclear_power": "核能发电", "choose_power_method": "选择发电方式", "sign_in_exchange": "签到/兑换", "click_pat_to_change": "点击下面的PAT会籍进行变更", "update_pat_status": "更新PAT发电状态", "bind_to_account": "绑定到此账号", "change_pat_key": "更换PAT密钥", "clear_pat_info": "清除PAT信息", "click_to_bind": "点击绑定", "enter_auth_code": "请输入授权代码", "please_wait": "请稍后", "key_recorded": "密钥 : 已录入", "pat_account": "PAT账号", "bind_pika_account": "绑定PIKA账号", "bind_account_time": "绑定账号时间", "rebind_time": "可以换绑时间", "power_features": "发电小功能: 多线程下载 / 批量导入导出下载", "power_guide": "去\"关于\"界面找到维护地址可获得发电指引\n\n \"我曾经发过电\"可同步相应发电状态\n \"我刚才发了电\"兑换神秘代码\n \"发电方式\"可以在网络不通时尝试更换\n \"PAT入会\"是独立的发电方式" }, "rankings": { "title": "排行榜", "day": "天", "week": "周", "month": "月", "knight": "骑", "refresh": "刷新", "comics_count": "本" }, "random_comics": { "title": "随机本子" }, "register": { "title": "注册", "registering": "注册中", "register_success": "注册成功", "register_failed": "注册失败", "account_exists": "账号已存在", "name_exists": "昵称已存在", "check_form": "请检查表单, 不允许留空", "account": "账号", "password": "密码", "nickname": "昵称", "gender": "性别", "birthday": "生日", "question_1": "问题1", "answer_1": "回答1", "question_2": "问题2", "answer_2": "回答2", "question_3": "问题3", "answer_3": "回答3", "not_set": "未设置", "please_enter_account": "请输入账号", "please_enter_password": "请输入密码", "please_enter_nickname": "请输入昵称", "please_enter_question_1": "请输入问题1", "please_enter_answer_1": "请输入回答1", "please_enter_question_2": "请输入问题2", "please_enter_answer_2": "请输入回答2", "please_enter_question_3": "请输入问题3", "please_enter_answer_3": "请输入回答3", "account_desc": "(小写字母+数字/登录使用)", "password_desc": "(大小写字母+数字/8位或以上)", "nickname_desc": "(可使用中文/2-50字)", "choose_gender": "选择您的性别", "futa": "扶她", "male": "公", "female": "母", "register_success_desc": "您已经注册成功, 请返回登录", "account_label": "账号", "nickname_label": "昵称" }, "search": { "title": "搜索", "search_hint": "搜索", "choose_category": "请选择分类" }, "search_author": { "title": "按作者搜索", "search_hint": "搜索 按作者 + ", "by_author": "按作者: " }, "space": { "title": "我的", "logout": "退出登录", "logout_confirm": "您确认要退出当前账号吗?", "my_favourites": "我的收藏", "view_history": "浏览记录", "my_downloads": "我的下载" }, "theme": { "title": "主题设置", "theme": "主题", "dark_mode_different_theme": "深色模式下使用不同的主题", "dark_mode_theme": "主题 (深色模式)" }, "view_logs": { "title": "浏览记录", "clear_all": "您要清除所有浏览记录吗? ", "clear_all_desc": "将会同时删除浏览进度!", "clear_one": "您要清除这条浏览记录吗? ", "clear_one_desc": "将会同时删除浏览进度!", "clear_selected": "您要清除选中的浏览记录吗? ", "clear_selected_desc": "将会同时删除浏览进度!", "categories": "分类" }, "web_server": { "title": "下载 - Web服务器", "loading": "加载中", "get_ip_failed": "获取IP失败", "getting_ip": "正在获取IP", "port": "端口号:8080", "usage_instruction": "在浏览器中输入\"http://本设备ip:8080/\"访问下载的漫画", "leave_notice": "离开页面后服务器将关闭" } }, "components": { "comic_info_card": { "categories": "分类", "finished": "完结", "viewed": "看过" }, "comic_list": { "shadow": "被封印的本子" }, "common": { "display_mode": "显示模式", "shadow_mode": "封印模式", "shadow_list": "封印列表", "batch_download": "批量下载" }, "image_reader": { "already_at_the_end": "已经到头了", "click_to_next_chapter": "再次点击跳转到下一章", "reload_page": "重载页面", "next_chapter": "下一章", "end_reading": "结束阅读", "reload_image": "重新加载图片", "save_image_in_this_page": "保存本页的图片", "image_load_failed": "图片加载失败" } } } ================================================ FILE: lib/assets/translations/zh-TW.json ================================================ { "language": { "title": "語言", "name": "繁體中文 - 中國台灣" }, "app": { "categories": "分類", "my": "我的", "copied_to_clipboard": "已複製到剪貼簿", "not_supported_platform": "不支援此平台", "cancel": "取消", "confirm": "確定", "save_cancel": "保存取消", "save_success": "保存成功", "save_failed": "保存失败", "pro": "發電", "pro_required": "請先發電再使用", "choose_folder": "選擇一個文件夾, 將文件保存到這裡", "permission_denied": "申請權限被拒絕", "loading": "加载中", "error": "错误", "pat": { "success": "您的贊助登錄成功, 請返回", "title": "更換PAT賬戶" }, "previous_page": "上一頁", "next_page": "下一頁", "page": "頁", "please_enter_page_number": "請輸入頁數:", "select_all": "全選", "load_failed": "加載失敗", "all": "全部", "delete": "刪除", "save_image": "保存圖片", "preview_image": "預覽圖片", "please_select": "請選擇", "refresh": "刷新", "initializing": "初始化", "like_failed": "點讚失敗", "network_error": "連接不上啦, 請檢查網路", "no_permission": "沒有權限或路徑不可用", "check_device_time": "請檢查設備時間", "resource_not_available": "資源未審核或不可用", "something_went_wrong": "啊哦, 被玩壞了", "click_refresh": "點擊刷新", "pull_down_refresh": "下拉刷新", "continue_reading": "繼續閱讀", "start_reading": "開始閱讀", "image_crop": "圖片裁剪", "download": "下載", "download_failed": "下載失敗", "download_finished": "下載完成", "downloading": "下載中", "queue": "隊列中", "deleting": "刪除中", "please_select_comic": "請選擇漫畫", "please_choose": "請選擇", "last_viewed": "上次觀看到", "auto_punch": "自動打卡", "yes": "是", "no": "否", "confirm_download": "確認下載", "copy": "複製" }, "net": { "no_address": "不分流", "address": "分流", "address_sync": "分流同步", "address_sync_from_server": "從伺服器獲取最新的分流地址", "address_sync_reset": "重制分流為預設值", "address_sync_success": "分流同步成功", "address_sync_failed": "分流同步失敗", "address_sync_reset_success": "分流重制成功", "address_sync_reset_failed": "分流重制失敗", "choose_address": "選擇分流", "image_address": "圖片分流", "use_api_load_image": "用API加載圖片", "ping_testing": "測速中", "ping_failed": "失敗" }, "categories": { "all": "全分類", "recommend": "推薦", "rankings": "排行榜", "random": "隨機本子", "game": "遊戲專區" }, "settings": { "settings": "設定", "interface": "介面", "network": "網路", "seal": "封印", "interaction": "交互", "reading": "閱讀", "download": "下載", "auto_download_on_favorite": "收藏時自動下載", "disable_auto_download_on_mobile": "行動網路下收藏時不自動下載", "auto_delete_download_on_unfavorite": "取消收藏時自動刪除下載", "web_server": "啟動Web伺服器", "web_server_subtitle": "讓局域網內的設備通過瀏覽器看下載的漫畫", "sync": "同步", "account": "帳戶", "modify_password": "修改密碼", "ebook": "電子書", "system": "系統", "clear_cache": "清除快取", "migrate": "文件遷移", "migrate_subtitle": "更換您的資料文件夾到記憶卡", "migrate_confirm": "此功能菜單保存後, 需要重啟程序, 您確認嗎", "app_orientation": { "title": "APP方向", "choose": "請選擇APP方向", "normal": "正常", "landscape": "橫屏", "portrait": "豎屏" }, "will_pop_notice": "在首頁連續按兩下返回鍵才能退出APP", "android_secure_flag": "禁止截圖/禁止顯示在任務視圖", "android_display_mode": { "title": "屏幕刷新率(安卓)", "dialog_title": "安卓屏幕刷新率 \n(省電模式下不會高刷)" }, "authentication": "進入APP時驗證身份(如果系統已經錄入密碼或指紋)", "set_password": "設置應用程序密碼", "auto_clean": { "title": "自動清理快取", "one_month_ago": "一個月前", "one_week_ago": "一周前", "one_day_ago": "一天前", "no_auto_clean": "不自動清理" }, "categories_column_count": { "title": "首頁分類展示列數", "choose": "選擇首頁分類展示列數", "auto": "自動" }, "categories_sort": { "title": "首頁分類排序" }, "chooser_root": { "title": "文件夾選擇器根路徑", "hint": "請輸入文件夾選擇器根路徑", "desc": "導出時選擇目錄的默認路徑, 同時也是根路徑, 不能正常導出時也可以嘗試設置此選項。" }, "content_failed_reload_action": { "title": "頁面加載失敗刷新方式", "choose": "選擇頁面加載失敗刷新方式", "pull_down": "下拉刷新", "touch_loader": "點擊屏幕刷新" }, "copy_full_name": { "title": "複製漫畫名稱時使用模版" }, "copy_full_name_template": { "title": "複製漫畫名稱模版", "hint": "模版內容" }, "copy_skip_confirm": { "title": "長按複製不需要確認" }, "download_and_export_path": { "title": "下載的同時導出到文件系統", "confirm": "下載的同時導出到文件系統", "desc": "您即將選擇一個目錄, 如果文件系統可寫, 下載的同時會為您自動導出一份" }, "download_cache_path": { "title": "使用其他程序的緩存下載加速", "confirm": "使用其他程序的緩存下載加速", "desc": "您即將選擇一個目錄, 這個目錄拷貝自以下目錄才能使用。下載時將會作為緩存文件夾優先讀取。 ", "cancel_desc": "您確定取消使用其他軟件的內容加速的功能嗎? 取消之後您可以再次點擊設置", "import_view_log_from_off": { "title": "導入其他程序的歷史記錄", "desc": "您即將選擇一個文件, 這個文件拷貝自以下路徑才能使用。", "choose_file_dialog_title": "選擇要導入的文件" } }, "download_thread_count": { "title": "下載線程數", "choose": "選擇下載線程數" }, "ebook_scrolling": { "title": "電子書模式滾動UI" }, "ebook_scrolling_range": { "title": "電子書模式滾動UI", "desc": "滾動幅度", "screen_height": "屏幕高度" }, "ebook_scrolling_trigger": { "title": "電子書模式滾動UI", "desc": "觸發距離", "cm": "厘米" }, "export_path": { "ios_desc": "隨後可在文件管理中找到導出的內容", "ios_desc2": "您正在使用iOS設備:\n導出到文件的內容請打開系統自帶文件管理進行瀏覽", "export_path_desc": "導出路徑 (點擊可修改)", "android_desc": "您正在使用安卓設備:\n如果不能成功導出並且提示權限不足, 可以嘗試在Download或Document下建立子目錄進行導出" }, "export_rename": { "title": "導出時進行重命名" }, "yes": "是", "no": "否", "full_screen_action": { "title": "操控方式", "choose": "選擇操控方式", "touch_once": "點擊屏幕一次全屏", "controller": "使用控制器全屏", "touch_double": "雙擊屏幕全屏", "touch_double_once_next": "雙擊屏幕全屏 + 單擊屏幕下一頁", "three_area": "將屏幕劃分成三個區域 (上一頁, 下一頁, 全屏)" }, "full_screen_ui": { "title": "全屏UI", "choose": "選擇全屏UI", "no": "不使用", "hidden_bottom": "去除虛擬控制器", "all": "全屏" }, "auto_full_screen": { "title": "進入閱讀器自動全屏" }, "auto_full_screen_on_forward": { "title": "前進時自動全螢幕" }, "ignore_info_history": { "title": "詳情頁不計入歷史記錄" }, "icon_loading": { "title": "盡量減少UI動畫" }, "ignore_upgrade_confirm": { "title": "關閉升級彈窗" }, "hidden_fd_icon": { "title": "隱藏個人空間的發電圖標" }, "hidden_search_persion": { "title": "隱藏按作者搜索功能" }, "hidden_viewed": { "title": "隱藏閱讀過的漫畫" }, "hidden_sub_icon": { "title": "隱藏訂閱功能" }, "hide_online_favorite": { "title": "隱藏線上收藏", "desc": "隱藏線上收藏入口與收藏按鈕" }, "hidden_words": { "title": "根據關鍵詞隱藏", "clear_all": "確認清空", "clear_all_desc": "確定要清空所有關鍵詞嗎?", "input_hint": "輸入要隱藏的關鍵詞", "no_words": "暫無關鍵詞" }, "image_address": { "title": "圖片分流", "pinging": "測速中", "failed": "失敗" }, "image_filter": { "title": "閱讀器圖片濾鏡", "normal": "正常", "gray": "灰度", "brown": "棕褐色", "choose": "選擇閱讀器圖片濾鏡" }, "import_notice": { "android_desc": "您正在使用安卓設備:\n如果不能導入導出並且提示權限不足, 可以嘗試在Download或Document下建立子目錄進行導入" }, "keyboard_controller": { "title": "閱讀器鍵盤翻頁(僅PC)" }, "list_layout": { "choose": "請選擇布局", "info_card": "詳情", "only_image": "封面", "cover_and_title": "封面+標題" }, "local_history_sync": { "sync_to_local": "同步歷史記錄到本地", "not_set": "未設置", "sync_success": "同步成功", "sync_failed": "同步失敗", "auto_sync": "自動同步歷史記錄到本地", "auto_sync_desc": "開啟後每次打開應用會自動備份歷史記錄", "choose_dir": "選擇目錄", "clear_path": "清除路徑", "clear_path_desc": "確定要清除路徑嗎?" }, "history_sync": "歷史記錄同步", "local_favorite_sync_title": "本地收藏夾同步", "use_local_favorite": "使用本地收藏夾", "use_local_favorite_desc": "在本地管理收藏,支援資料夾分類", "local_favorite_sync": { "auto_sync": "自動同步本地收藏夾", "auto_sync_desc": "使用WebDAV自動同步本地收藏夾", "manual_sync": "手動同步本地收藏夾", "sync_success": "同步成功", "sync_failed": "同步失敗" }, "no_animation": { "title": "取消翻頁動畫(點按屏幕、音量鍵、鍵盤)" }, "pager_action": { "title": "列表頁加載方式", "choose": "選擇列表頁加載方式", "controller": "使用按鈕", "stream": "瀑布流" }, "proxy": { "title": "代理伺服器", "hint": "請輸入代理伺服器", "desc": " ( 例如 socks5://127.0.0.1:1080/ ) ", "no_proxy": "未設置" }, "quality": { "title": "瀏覽時的圖片質量", "choose": "請選擇圖片質量", "original": "原圖", "low": "低", "medium": "中", "high": "高" }, "reader_background_color": { "title": "閱讀器背景色", "choose": "選擇閱讀器背景色", "black": "黑色", "gray": "灰度", "white": "白色" }, "reader_direction": { "title": "閱讀器方向", "choose": "選擇翻頁方向", "top_to_bottom": "從上到下", "left_to_right": "從左到右", "right_to_left": "從右到左" }, "reader_scroll_by_screen_percentage": { "title": "按距離翻頁長度", "screen_size": "屏幕尺寸" }, "web_toon_scroll_mode": { "title": "WebToon 翻頁模式", "choose": "選擇 WebToon 翻頁模式", "image": "圖片", "screen": "距離" }, "reader_zoom": { "out_title": "縮小倍數(最小縮放)", "in_title": "放大倍數(最大縮放)", "double_tap_title": "雙擊縮放倍數" }, "drag_region_lock": { "title": "鎖定拖動邊界" }, "gesture_speed": { "title": "手勢速度倍率" }, "reader_slider_position": { "title": "滾動條的位置", "choose": "選擇滑動條位置", "bottom": "下方", "right": "右側", "left": "左側" }, "reader_two_page_direction": { "title": "雙頁閱讀器內容排列", "choose": "選擇雙頁閱讀器內容排列", "close_to": "靠近", "pull_away": "遠離", "each_centered": "各自居中" }, "reader_type": { "title": "閱讀器模式", "choose": "選擇閱讀器模式", "web_toon": "WebToon (默認)", "web_toon_zoom": "WebToon (雙擊放大)", "gallery": "相冊", "web_toon_free_zoom": "WebToon (ListView雙擊放大)\n(此模式進度條無效)", "two_page_gallery": "雙頁模式\n(實驗)", "left_to_right": "從左到右", "right_to_left": "從右到左", "two_page_direction": "雙頁方向", "two_page_direction_choose": "選擇雙頁方向" }, "shadow_categories": { "title": "封印", "search_hint": "搜索" }, "shadow_categories_mode": { "title": "封印模式", "black_list": "黑名單", "white_list": "白名單" }, "startup_pic": { "title": "設置啟動圖片", "subtitle": "設置應用啟動時顯示的圖片", "clear_title": "清除啟動圖片", "clear_subtitle": "清除應用啟動時顯示的圖片", "clear_success": "啟動圖片已清除", "update_success": "啟動圖片已更新" }, "show_comment_at_download": { "title": "在下載顯示評論區" }, "font": { "title": "字體", "hint": "請輸入字體", "input_hint": "請輸入字體的名稱且用英文逗號分隔, 例如 \"宋體,黑體\", 如果您保存後沒有發生變化, 說明字體無法使用或名稱錯誤, 可以去參考C:\\Windows\\Fonts尋找您的字體。若您使用的是flutter2引擎的版本,只有第一個字體生效。", "choose_hint": "需要您選擇多個字體,直至您點擊背景區域" }, "theme": { "origin": "原生", "pink": "粉色", "black": "酷黑", "dark": "暗黑", "dusty_blue": "灰蓝", "dark_black": "纯黑", "choose_theme": "選擇主題", "book": "書本", "enable_status_bar_color": "啟用狀態欄顏色", "enable_status_restart_hint": "關閉時需要重新啟動應用程序刷新狀態欄顏色" }, "three_keep_right": { "title": "三區域模式翻頁始終為右側下一頁" }, "time_zone": { "title": "時區" }, "timeout_lock": { "title": "自動鎖定", "notice": "注意:自動鎖定在桌面端僅支持最小化後超時,手機端支持後台以及鎖屏後超時。如果沒有設置密碼,自動鎖定無效。安卓以及桌面端只會鎖定桌面,不會鎖定下載,iOS未測試,需要手動開啟後台活動。", "1_hour": "一小時", "10_minutes": "十分鐘", "3_minutes": "三分鐘", "1_minute": "一分鐘", "10_seconds": "十秒", "1_second": "一秒", "no_lock": "不鎖定" }, "using_right_click_pop": { "title": "鼠標右鍵返回上一頁" }, "volume_controller": { "title": "閱讀器音量鍵翻頁" }, "volume_next_chapter": { "title": "雙擊 音量/鍵盤/控制器 下一章節" }, "webdav": { "title": "WebDav", "not_set": "未設置", "path": "WebDav 路徑", "path_hint": "請輸入WebDav 路徑", "username": "WebDav 用戶名", "username_hint": "請輸入WebDav 用戶名", "password": "WebDav 密碼", "password_hint": "請輸入WebDav 密碼", "auto_sync_history_to_webdav": "開啟時自動同步瀏覽記錄到WebDav", "sync_history_to_webdav": "立即同步瀏覽記錄到WebDAV", "upload_history_to_webdav": "覆蓋WebDAV中的瀏覽記錄", "upload_history_to_webdav_desc": "如有多台設備,請注意自動同步功能", "sync_success": "同步成功", "sync_failed": "同步失敗" } }, "local_favorite": { "title": "本地收藏夾", "all_folders": "全部", "new_folder": "新建資料夾", "select_mode": "多選", "cancel_select_mode": "退出多選", "select_all": "全選", "delete_folder": "刪除資料夾", "move_to_folder": "移動到資料夾", "remove_selected": "移除選中收藏", "remove_selected_confirm": "確認從本地收藏夾中移除選中的漫畫?", "remove_selected_success": "已移除", "remove_selected_failed": "移除失敗", "select_comics": "請先選擇漫畫", "folder_limit_reached": "免費版最多建立3個資料夾,升級Pro解鎖無限制", "batch_download": "批次下載", "select_folder": "選擇資料夾", "folder_name": "資料夾名稱", "delete_confirm": "確認刪除資料夾?", "empty_folder": "暫無收藏", "no_folders": "還沒有資料夾,請先建立", "remove_confirm_title": "取消收藏", "remove_confirm_content": "確定要從本地收藏夾中移除這部漫畫嗎?", "remove_failed": "取消收藏失敗", "load_failed": "載入失敗", "add_success": "已新增到本地收藏夾", "add_failed": "新增失敗", "create_folder_failed": "建立資料夾失敗", "create_success": "建立成功", "delete_success": "刪除成功", "delete_failed": "刪除失敗", "move_success": "移動成功", "move_failed": "移動失敗", "select_comics_to_download": "請選擇要下載的漫畫", "download_started": "開始下載", "download_failed": "下載失敗" }, "screen": { "about": { "title": "關於", "version": "軟件版本", "check_update": "檢查更新", "tips": "提示 : \n1. 詳情頁的作者/上傳者/分類/標籤都可以點擊\n2. 詳情頁的作者/上傳者/標題長按可以複製\n3. 使用分頁而不是瀑布流點擊頁碼可以快速翻頁\n4. 下載指的是緩存到本地, 需要導出才可以分享\n5. 下載長按可以刪除", "download_new_version": "請從獲取渠道下載新版", "no_new_version": "未檢測到新版本", "download_release_version": "下載RELEASE版", "update_content": "更新內容", "go_to_release_repository": "去RELEASE倉庫" }, "account": { "title": "賬戶", "username": "賬號", "username_hint": "請輸入賬號", "password": "密碼", "password_hint": "請輸入密碼", "no_account_register": "沒有賬號,我要註冊", "password_reset": "密碼找回", "check_username_password_or_network": "請檢查賬號密碼或網絡環境", "check_device_time": "請檢查設備時間", "username_or_password_error": "賬號或密碼錯誤", "login_failed": "登錄失敗", "not_set": "未設置" }, "app": { "will_pop_notice": "再次返回將會退出應用程序" }, "categories": { "search_hint": "搜索" }, "clean": { "title": "清理", "cleaning": "清理中", "clean_network_cache": "清理网络缓存", "clean_image_cache": "清理图片缓存", "clean_all_cache": "清理全部缓存", "clean_success": "清理成功", "clean_failed": "清理失败" }, "close_app": { "title": "提示", "close_app": "請關閉應用重新打開" }, "comic_collections": { "no_resource": "這裡沒有資源呀" }, "comic_info": { "chapter": "章节", "comment": "评论", "recommend": "推荐" }, "comics": { "search_hint": "搜索分类", "choose_category": "请选择分类" }, "comic_subscribes": { "update_reminder": "更新提醒", "check_update": "检查更新", "cancel_all_update_reminder": "取消所有更新提醒" }, "comment": { "title": "評論", "hint_text": "請輸入評論內容", "success": "評論成功", "i_have_something_to_say": "我有話要講", "please_enter_comment": "請輸入評論內容" }, "desktop_authentication": { "current_password": "當前密碼", "password_error": "密碼錯誤", "password_initialization": "密碼初始化", "password": "密碼", "re_enter_password": "再次輸入密碼", "password_mismatch": "兩次輸入的密碼不一致", "set_password": "設置密碼" }, "download_confirm": { "please_select_ep": "請選擇下載的EP", "already_added_to_download_list": "已經加入下載列表" }, "download_export_group": { "title": "批量導出", "please_select_content": "請選擇導出的內容", "exporting": "正在導出", "export_failed": "導出失敗", "export_success": "導出成功", "export_to_pkz": "導出成一個PKZ\n(加密模式,防止網盤檢測,能用pikapika打開觀看)", "export_to_pki": "每部漫畫都打包一個PKI\n(加密模式,防止網盤檢測,能用pikapika導入)", "export_to_zip": "每部漫畫都打包一個ZIP\n(不加密模式,能用pikapika導入或網頁瀏覽器觀看)", "export_to_jpeg_zip": "每部漫畫都打包一個ZIP+JPEG\n(能直接使用其他閱讀器看,不可再導入)", "export_to_jpeg_folder": "每部漫畫都導出成文件夾+JPEG", "export_to_pdf": "每部漫畫都導出成PDF", "export_to_epub": "每部漫畫都導出成EPUB", "export_to_pdf_folder": "每部漫畫都導出到文件夾, 每個章節一個PDF", "export_to_cbz": "每部漫畫都導出成cbz", "after_power_use": "發電後使用", "input_save_name": "請輸入保存後的名称", "export_confirm": "導出確認", "export_to_pkz_title": "將導出您所選的漫畫為一個PKZ", "export_to_pki_title": "將您所選的漫畫分別導出成單獨的PKI", "please_power_up": "請先發電鴨", "export_to_zip_title": "將您所選的漫畫分別導出成ZIP", "export_to_jpeg_zip_title": "將您所選的漫畫分別導出成ZIP+JPEG", "export_to_jpeg_zip_title_not_down_over": "將您所選的漫畫分別導出成ZIP+JPEG\n(即便沒有下載完成也可以使用)", "export_to_jpeg_folder_title": "將您所選的漫畫分別導出成文件夾+JPEG", "export_to_pdf_title": "將您所選的漫畫分別導出成PDF", "export_to_epub_title": "將您所選的漫畫分別導出成EPUB", "export_to_pdf_folder_title": "將您所選的漫畫分別導出到文件夾, 每個章節一個PDF", "export_to_cbz_title": "將您所選的漫畫分別導出成cbz", "exporting_please_wait": "導出中, 請稍後" }, "download_export_to_file": { "title": "導出", "transfer_to_other_device": "傳輸到其他設備", "input_save_name": "請輸入保存後的名称", "export_confirm": "導出確認", "export_to_pkz_title": "將您所選的漫畫導出PKZ", "export_to_pkz_desc": "導出到xxx.pkz\n(可直接打開觀看,不支持導入)\n(可以躲避網盤或者聊天軟件的掃描)", "export_to_pki_title": "將您所選的漫畫導出PKI", "export_to_pki_desc": "導出到xxx.pki\n(只支持導入, 不支持直接閱讀)\n(可以躲避網盤或者聊天軟件的掃描)\n(後期版本可能支持直接閱讀)", "export_to_zip_title": "將您所選的漫畫導出HTML+ZIP", "export_to_zip_desc": "導出到xxx.zip\n(可從其他設備導入 / 解壓後可閱讀)", "export_to_jpeg_zip_title": "將您所選的漫畫導出HTML+JPEG", "export_to_jpeg_zip_desc": "導出到xxx.jpeg\n(可直接在相冊中打開觀看)", "export_to_pdf_title": "將您所選的漫畫導出PDF", "export_to_pdf_desc": "導出到xxx.pdf\n(即使沒有下載成功也可以使用、未成功下載的圖片將會跳過)\n(可直接在相冊中打開觀看)", "export_to_pdf_folder_title": "將您所選的漫畫導出到文件夾, 每個章節一個PDF", "export_to_pdf_folder_desc": "導出到xxx.pdf\n(即使沒有下載成功也可以使用、未成功下載的圖片將會跳過)\n(可直接在相冊中打開觀看)", "export_to_epub_title": "將您所選的漫畫導出EPUB", "export_to_epub_desc": "導出到xxx.epub, 閱讀器可以直接使用(不可再導入)", "export_to_jpeg_folder_title": "將您所選的漫畫導出JPEGS.zip", "export_to_jpeg_folder_desc": "導出閱讀器用JPGS.zip\n(不可再導入)", "export_to_cbz_title": "將您所選的漫畫導出cbk.zip", "export_to_cbz_desc": "導出到xxx.cbz, 閱讀器可以直接使用(不可再導入)" }, "download_export_to_socket": { "title": "網絡導出", "loading": "加載中", "tips": "傳輸成功之前請不要退出頁面, 一次只能導出到一個設備, 兩台設備需要在同一網段或無限局域網中, 請另外一台設備輸入 IP:端口 , 有一個IP時請選擇無限局域網的IP, 通常是192.168開頭", "get_ip_failed": "獲取IP失敗", "getting_ip": "正在獲取IP", "port": "端口號" }, "download_import": { "title": "導入", "open_file": "打開文件", "select_file": "選擇要導入的文件", "import_success": "導入成功", "import_failed": "導入失敗", "select_file_desc": "選擇zip文件進行導入\n選擇pki文件進行導入\n選擇pkz文件進行閱讀", "input_address": "請輸入導出設備提供的地址\n例如 \"192.168.1.2:50000\"", "import_from_other_device": "從其他設備導入", "select_folder_desc": "選擇文件夾\n(導入裡面所有的zip/pki)\n(發電後使用)" }, "download_info": { "loading": "加載中", "chapter": "章節", "comment": "評論", "recommend": "推薦" }, "download_list": { "search_download": "搜索下載", "multi_select_operation": "多選操作", "download_list": "下載列表", "search": "搜索", "select_folder": "選擇文件夾", "download_already_in_delete_queue": "該下載已經在刪除隊列中", "import": "導入", "export": "導出", "file": "文件", "download_task": "下載任務", "pause_download": "暫停下載嗎?", "start_download": "啟動下載嗎?", "resume_failed": "恢復失敗任務", "resume_failed_desc": "所有失敗的下載已經恢復", "downloading": "下載中", "paused": "暫停中", "move_download": "移動下載", "select_download_to_move": "請選擇要移動的下載", "select_download_to_delete": "請選擇要刪除的下載", "input_name": "==> 輸入名稱 <==", "empty_folder_will_be_deleted": "(空文件夾將會自動刪除,下次需要手動輸入)", "folder_name": "文件夾名稱", "please_input_folder_name": "請輸入文件夾名稱", "delete_download": "刪除下載", "delete_selected_download": "刪除選中的下載嗎?", "multi_select": "多選" }, "download_only_import": { "importing": "正在導入", "import_success": "導入成功", "import_failed": "導入失敗", "click_import_file": "點擊導入文件", "importing_please_wait": "導入中, 請稍後" }, "favourite_paper": { "favourite": "收藏" }, "forgot_password": { "title": "找回密碼", "username": "帳號", "not_set": "未設置", "confirm": "確認", "please_enter_username": "請輸入帳號", "question_1": "問題1", "question_2": "問題2", "question_3": "問題3", "answer_1": "回答1", "answer_2": "回答2", "answer_3": "回答3", "please_enter_answer_1": "請輸入回答1", "please_enter_answer_2": "請輸入回答2", "please_enter_answer_3": "請輸入回答3", "use_answer_1_recover": "使用回答1找回密碼", "use_answer_2_recover": "使用回答2找回密碼", "use_answer_3_recover": "使用回答3找回密碼", "please_enter_answer": "請輸入答案", "new_password_copied": "新密碼正在複製到剪切板", "answer_incorrect": "答案不正確", "password": "密碼" }, "game_download": { "title": "下載", "download_links_obtained": "獲取到下載鏈接, 您只需要選擇其中一個" }, "game_info": { "download": "下載", "details": "詳情", "comments": "評論" }, "games": { "title": "遊戲" }, "import_from_off": { "title": "導入", "import_success": "導入成功", "import_failed": "導入失敗" }, "modify_password": { "title": "修改密碼", "please_wait": "請稍後", "old_password": "舊密碼", "new_password": "新密碼", "repeat_new_password": "重複輸入新密碼", "not_filled": "未填寫", "please_enter_old_password": "請輸入舊密碼", "please_enter_new_password": "請輸入新密碼", "please_repeat_new_password": "請重複輸入新密碼", "new_password_mismatch": "新密碼不匹配", "modify_success": "修改成功", "failed": "失敗", "confirm": "確認" }, "network_settings": { "title": "網路設置" }, "pkz_reader": { "reading_downloaded_comic": "您閱讀的是下載漫畫" }, "pro": { "title": "發電中心", "power_center": "發電中心", "power_status": "發電狀態", "powered": "發電中", "not_powered": "未發電", "pat_membership": "PAT入會", "pat_status": "PAT狀態", "pat_normal": "PAT正常", "pat_bind_hint": "請點擊這裡綁定到當前賬號發電", "pat_rebind_hint": "請點換綁到當前賬號發電", "pat_not_detected": "未檢測到入會, 請到下載頁入會", "i_have_powered": "我曾經發過電", "i_just_powered": "我剛才發了電", "enter_code": "輸入代碼", "power_method": "發電方式", "wind_power": "風力發電", "hydro_power": "水力發電", "solar_power": "光伏發電", "nuclear_power": "核能發電", "choose_power_method": "選擇發電方式", "sign_in_exchange": "簽到/兌換", "click_pat_to_change": "點擊下面的PAT會籍進行變更", "update_pat_status": "更新PAT發電狀態", "bind_to_account": "綁定到此賬號", "change_pat_key": "更換PAT密鑰", "clear_pat_info": "清除PAT信息", "click_to_bind": "點擊綁定", "enter_auth_code": "請輸入授權代碼", "please_wait": "請稍後", "key_recorded": "密鑰 : 已錄入", "pat_account": "PAT賬號", "bind_pika_account": "綁定PIKA賬號", "bind_account_time": "綁定賬號時間", "rebind_time": "可以換綁時間", "power_features": "發電小功能: 多線程下載 / 批量導入導出下載", "power_guide": "去\"關於\"界面找到維護地址可獲得發電指引\n\n \"我曾經發過電\"可同步相應發電狀態\n \"我剛才發了電\"兌換神秘代碼\n \"發電方式\"可以在網絡不通時嘗試更換\n \"PAT入會\"是獨立的發電方式" }, "rankings": { "title": "排行榜", "day": "天", "week": "周", "month": "月", "knight": "騎", "refresh": "刷新", "comics_count": "本" }, "random_comics": { "title": "隨機本子" }, "register": { "title": "註冊", "registering": "註冊中", "register_success": "註冊成功", "register_failed": "註冊失敗", "account_exists": "賬號已存在", "name_exists": "暱稱已存在", "check_form": "請檢查表單, 不允許留空", "account": "賬號", "password": "密碼", "nickname": "暱稱", "gender": "性別", "birthday": "生日", "question_1": "問題1", "answer_1": "回答1", "question_2": "問題2", "answer_2": "回答2", "question_3": "問題3", "answer_3": "回答3", "not_set": "未設置", "please_enter_account": "請輸入賬號", "please_enter_password": "請輸入密碼", "please_enter_nickname": "請輸入暱稱", "please_enter_question_1": "請輸入問題1", "please_enter_answer_1": "請輸入回答1", "please_enter_question_2": "請輸入問題2", "please_enter_answer_2": "請輸入回答2", "please_enter_question_3": "請輸入問題3", "please_enter_answer_3": "請輸入回答3", "account_desc": "(小寫字母+數字/登錄使用)", "password_desc": "(大小寫字母+數字/8位或以上)", "nickname_desc": "(可使用中文/2-50字)", "choose_gender": "選擇您的性別", "futa": "扶她", "male": "公", "female": "母", "register_success_desc": "您已經註冊成功, 請返回登錄", "account_label": "賬號", "nickname_label": "暱稱" }, "search": { "title": "搜索", "search_hint": "搜索", "choose_category": "請選擇分類" }, "search_author": { "title": "按作者搜索", "search_hint": "搜索 按作者 + ", "by_author": "按作者: " }, "space": { "title": "我的", "logout": "退出登錄", "logout_confirm": "您確認要退出當前賬號嗎?", "my_favourites": "我的收藏", "view_history": "瀏覽記錄", "my_downloads": "我的下載" }, "theme": { "title": "主題設置", "theme": "主題", "dark_mode_different_theme": "深色模式下使用不同的主題", "dark_mode_theme": "主題 (深色模式)" }, "view_logs": { "title": "瀏覽記錄", "clear_all": "您要清除所有瀏覽記錄嗎? ", "clear_all_desc": "將會同時刪除瀏覽進度!", "clear_one": "您要清除這條瀏覽記錄嗎? ", "clear_one_desc": "將會同時刪除瀏覽進度!", "clear_selected": "您要清除選中的瀏覽記錄嗎? ", "clear_selected_desc": "將會同時刪除瀏覽進度!", "categories": "分類" }, "web_server": { "title": "下載 - Web服務器", "loading": "加載中", "get_ip_failed": "獲取IP失敗", "getting_ip": "正在獲取IP", "port": "端口號:8080", "usage_instruction": "在瀏覽器中輸入\"http://本設備ip:8080/\"訪問下載的漫畫", "leave_notice": "離開頁面後服務器將關閉" } }, "components": { "comic_info_card": { "categories": "分類", "finished": "完結", "viewed": "看過" }, "comic_list": { "shadow": "被封印的本子" }, "common": { "display_mode": "顯示模式", "shadow_mode": "封印模式", "shadow_list": "封印列表", "batch_download": "批量下載" }, "image_reader": { "already_at_the_end": "已經到頭了", "click_to_next_chapter": "再次點擊跳轉到下一章", "reload_page": "重載頁面", "next_chapter": "下一章", "end_reading": "結束閱讀", "reload_image": "重新加載圖片", "save_image_in_this_page": "保存本頁的圖片", "image_load_failed": "圖片加載失敗" } } } ================================================ FILE: lib/basic/Channels.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:flutter/services.dart'; // EventChannel // 由于Flutter的EventChannel只能订阅一次, 且为了和golang的的通信, 这里实现了多次订阅的分发和平铺 // 根据eventName订阅和取消订阅 var _eventChannel = const EventChannel("flatEvent"); StreamSubscription? _eventChannelListen; Map _eventMap = {}; void registerEvent(void Function(String args) eventHandler, String eventName) { if (_eventMap.containsKey(eventHandler)) { throw 'once register'; } _eventMap[eventHandler] = eventName; if (_eventMap.length == 1) { _eventChannelListen = _eventChannel.receiveBroadcastStream().listen(_onFlatEvent); } } void unregisterEvent(void Function(String args) eventHandler) { if (!_eventMap.containsKey(eventHandler)) { throw 'no register'; } _eventMap.remove(eventHandler); if (_eventMap.isEmpty) { _eventChannelListen?.cancel(); } } void _onFlatEvent(dynamic t) { _FlatEvent e = _FlatEvent.fromJson(jsonDecode(t)); _eventMap.forEach((key, value) { if (value == e.function) { key(e.content); } }); } class _FlatEvent { late String function; late String content; _FlatEvent.fromJson(Map json) { this.function = json["function"]; this.content = json["content"]; } } ================================================ FILE: lib/basic/Common.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; import 'package:pikapika/screens/AccessKeyReplaceScreen.dart'; import 'package:uni_links/uni_links.dart'; import 'package:uri_to_file/uri_to_file.dart'; import '../screens/ComicInfoScreen.dart'; import '../screens/DownloadOnlyImportScreen.dart'; import '../screens/PkzArchiveScreen.dart'; import 'config/IconLoading.dart'; import 'config/TimeOffsetHour.dart'; /// 默认的图片尺寸 double coverWidth = 210; double coverHeight = 315; String categoryTitle(String? categoryTitle) { return categoryTitle ?? tr('categories.all'); } /// 显示一个toast void defaultToast(BuildContext context, String title) { showToast( title, context: context, position: StyledToastPosition.center, animation: StyledToastAnimation.scale, reverseAnimation: StyledToastAnimation.fade, duration: const Duration(seconds: 4), animDuration: const Duration(seconds: 1), curve: Curves.elasticOut, reverseCurve: Curves.linear, ); } /// 显示一个确认框, 用户关闭弹窗以及选择否都会返回false, 仅当用户选择确定时返回true Future confirmDialog( BuildContext context, String title, String content) async { return await showDialog( context: context, builder: (context) => AlertDialog( title: Text(title), content: SingleChildScrollView( child: ListBody( children: [Text(content)], ), ), actions: [ MaterialButton( child: Text(tr('app.cancel')), onPressed: () { Navigator.of(context).pop(false); }, ), MaterialButton( child: Text(tr('app.confirm')), onPressed: () { Navigator.of(context).pop(true); }, ), ], )) ?? false; } /// 显示一个消息提示框 Future alertDialog(BuildContext context, String title, String content) { return showDialog( context: context, builder: (context) => AlertDialog( title: Text(title), content: SingleChildScrollView( child: ListBody( children: [ Text(content), ], ), ), actions: [ MaterialButton( child: Text(tr('app.confirm')), onPressed: () { Navigator.of(context).pop(); }, ), ], ), ); } /// stream-filter的替代方法 List filteredList(List list, bool Function(T) filter) { List result = []; for (var element in list) { if (filter(element)) { result.add(element); } } return result; } /// 创建一个单选对话框, 用户取消选择返回null, 否则返回所选内容 Future chooseListDialog( BuildContext context, String title, List items, {String? tips}) async { return showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: Text(title), children: [ ...items.map((e) => SimpleDialogOption( onPressed: () { Navigator.of(context).pop(e); }, child: Text('$e'), )), ...tips != null ? [ Container( padding: const EdgeInsets.fromLTRB(15, 5, 15, 15), child: Text(tips), ), ] : [], ], ); }, ); } /// 创建一个单选对话框, 用户取消选择返回null, 否则返回所选内容(value) Future chooseMapDialog( BuildContext buildContext, Map values, String title) async { return await showDialog( context: buildContext, builder: (BuildContext context) { return SimpleDialog( title: Text(title), children: values.entries .map((e) => SimpleDialogOption( child: Text(e.key), onPressed: () { Navigator.of(context).pop(e.value); }, )) .toList(), ); }, ); } /// 输入对话框1 var _controller = TextEditingController.fromValue(const TextEditingValue(text: '')); Future displayTextInputDialog(BuildContext context, {String? title, String src = "", String? hint, String? desc, bool isPasswd = false}) { _controller.text = src; return showDialog( context: context, builder: (context) { return AlertDialog( title: title == null ? null : Text(title), content: SingleChildScrollView( child: ListBody( children: [ TextField( controller: _controller, decoration: InputDecoration(hintText: hint), obscureText: isPasswd, obscuringCharacter: '\u2022', ), ...(desc == null ? [] : [ Container( padding: const EdgeInsets.only(top: 20, bottom: 10), child: Text( desc, style: TextStyle( fontSize: 12, color: Theme.of(context) .textTheme .bodyText1 ?.color ?.withOpacity(.5)), ), ) ]), ], ), ), actions: [ MaterialButton( child: Text(tr('app.cancel')), onPressed: () { Navigator.of(context).pop(); }, ), MaterialButton( child: Text(tr('app.confirm')), onPressed: () { Navigator.of(context).pop(_controller.text); }, ), ], ); }, ); } /// 将字符串前面加0直至满足len位 String add0(int num, int len) { var rsp = "$num"; while (rsp.length < len) { rsp = "0$rsp"; } return rsp; } /// 格式化时间 2012-34-56 String formatTimeToDate(String str) { try { var c = DateTime.parse(str).add(Duration(hours: currentTimeOffsetHour())); return "${add0(c.year, 4)}-${add0(c.month, 2)}-${add0(c.day, 2)}"; } catch (e) { return "-"; } } /// 格式化时间 2012-34-56 12:34:56 String formatTimeToDateTime(String str) { try { var c = DateTime.parse(str).add(Duration(hours: currentTimeOffsetHour())); return "${add0(c.year, 4)}-${add0(c.month, 2)}-${add0(c.day, 2)} ${add0(c.hour, 2)}:${add0(c.minute, 2)}"; } catch (e) { return "-"; } } /// 输入对话框2 final TextEditingController _textEditController = TextEditingController(text: ''); Future inputString(BuildContext context, String title, {String hint = "", String? defaultValue}) async { if (defaultValue != null) { _textEditController.text = defaultValue; } else { _textEditController.clear(); } return showDialog( context: context, builder: (context) { return AlertDialog( content: Card( child: SingleChildScrollView( child: ListBody( children: [ Text(title), TextField( controller: _textEditController, decoration: InputDecoration( labelText: hint, ), ), ], ), ), ), actions: [ MaterialButton( onPressed: () { Navigator.pop(context); }, child: Text(tr('app.cancel')), ), MaterialButton( onPressed: () { Navigator.pop(context, _textEditController.text); }, child: Text(tr('app.confirm')), ), ], ); }, ); } StreamSubscription linkSubscript(BuildContext context) { return linkStream.listen((uri) async { if (uri == null) return; var parsed = Uri.parse(uri); if (RegExp(r"^pika://access_key/([0-9A-z:\-]+)/$").allMatches(uri).isNotEmpty) { String accessKey = RegExp(r"^pika://access_key/([0-9A-z:\-]+)/$") .allMatches(uri) .first .group(1)!; Navigator.of(context).push( mixRoute( builder: (BuildContext context) => AccessKeyReplaceScreen(accessKey: accessKey), ), ); } else if (RegExp(r"^pika://comic/([0-9A-z]+)/$").allMatches(uri).isNotEmpty) { String comicId = RegExp(r"^pika://comic/([0-9A-z]+)/$") .allMatches(uri) .first .group(1)!; Navigator.of(context).push( mixRoute( builder: (BuildContext context) => ComicInfoScreen(comicId: comicId), ), ); } else if (RegExp(r"^https?://pika/comic/([0-9A-z]+)/$").allMatches(uri).isNotEmpty) { String comicId = RegExp(r"^https?://pika/comic/([0-9A-z]+)/$") .allMatches(uri) .first .group(1)!; Navigator.of(context).push( mixRoute( builder: (BuildContext context) => ComicInfoScreen(comicId: comicId), ), ); } else if (RegExp(r"^.*\.pkz$").allMatches(parsed.path).isNotEmpty) { File file = await toFile(uri); Navigator.of(context).push( mixRoute( builder: (BuildContext context) => PkzArchiveScreen(pkzPath: file.path), ), ); } else if (RegExp(r"^.*\.((pki)|(zip))$").allMatches(parsed.path).isNotEmpty) { File file = await toFile(uri); Navigator.of(context).push( mixRoute( builder: (BuildContext context) => DownloadOnlyImportScreen(path: file.path), ), ); } }); } ================================================ FILE: lib/basic/Cross.dart ================================================ /// 与平台交互的操作 import 'dart:io'; import 'package:clipboard/clipboard.dart'; import 'package:pikapika/i18.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/config/CopySkipConfirm.dart'; import 'package:pikapika/basic/config/Platform.dart'; import 'package:url_launcher/url_launcher.dart'; import 'Method.dart'; import 'config/ChooserRoot.dart'; /// 复制内容到剪切板 void copyToClipBoard(BuildContext context, String string) { FlutterClipboard.copy(string); defaultToast( context, tr('app.copied_to_clipboard'), ); } void copyToClipBoardTips(BuildContext context, String string) { FlutterClipboard.copy(string); defaultToast(context, tr('app.copied_to_clipboard') + " :\n$string"); } /// 打开web页面 Future openUrl(String url) async { if (await canLaunch(url)) { await launch( url, forceSafariVC: false, ); } } /// 保存图片 Future saveImage(String path, BuildContext context) async { Future? future; if (Platform.isIOS) { future = method.iosSaveFileToImage(path); } else if (Platform.isAndroid) { future = _saveImageAndroid(path, context); } else if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { String? folder = await chooseFolder(context); if (folder != null) { future = method.convertImageToJPEG100(path, folder); } } else { defaultToast(context, tr('app.not_supported_platform')); return; } if (future == null) { defaultToast(context, tr('app.save_cancel')); return; } try { await future; defaultToast(context, tr('app.save_success')); } catch (e, s) { print("$e\n$s"); defaultToast(context, tr('app.save_failed')); } } /// 保存图片且保持静默, 用于批量导出到相册 Future saveImageQuiet(String path, BuildContext context) async { if (Platform.isIOS) { return method.iosSaveFileToImage(path); } else if (Platform.isAndroid) { return _saveImageAndroid(path, context); } else { throw Exception("only mobile"); } } Future _saveImageAndroid(String path, BuildContext context) async { late bool g; if (androidVersion < 30) { g = await Permission.storage.request().isGranted; } else { g = await Permission.manageExternalStorage.request().isGranted; } if (!g) { return; } return method.androidSaveFileToImage(path); } /// 选择一个文件夹用于保存文件 Future chooseFolder(BuildContext context) async { return FilePicker.platform.getDirectoryPath( dialogTitle: tr('app.choose_folder'), initialDirectory: Directory.fromUri(Uri.file(await currentChooserRoot())).absolute.path, ); } /// 复制对话框 void confirmCopy(BuildContext context, String content) async { if (copySkipConfirm()) { copyToClipBoardTips(context, content); } else { if (await confirmDialog(context, tr('app.copy'), content)) { copyToClipBoard(context, content); } } } ================================================ FILE: lib/basic/Entities.dart ================================================ import 'dart:convert'; /// 图片 class RemoteImageInfo { late String originalName; late String path; late String fileServer; RemoteImageInfo.fromJson(Map json) { this.originalName = json["originalName"]; this.path = json["path"]; this.fileServer = json["fileServer"]; } Map toJson() { final _data = {}; _data['originalName'] = originalName; _data['path'] = path; _data['fileServer'] = fileServer; return _data; } } /// 用户基本信息 class BasicUser { late String id; late String gender; late String name; late String title; late bool verified; late int exp; late int level; late List characters; late RemoteImageInfo avatar; late String? slogan; BasicUser.fromJson(Map json) { this.id = json["_id"]; this.gender = json["gender"]; this.name = json["name"]; this.title = json["title"]; this.verified = json["verified"]; this.exp = json["exp"]; this.level = json["level"]; this.characters = json["characters"] == null ? [] : List.of(json["characters"]).map((e) => "$e").toList(); this.avatar = RemoteImageInfo.fromJson(Map.of(json["avatar"])); this.slogan = json["slogan"]; } } /// 用户自己的信息 class UserProfile extends BasicUser { late String birthday; late String email; late String createdAt; late bool isPunched; UserProfile.fromJson(Map json) : super.fromJson(json) { this.birthday = json["birthday"]; this.email = json["email"]; this.createdAt = json["created_at"]; this.isPunched = json["isPunched"]; } } /// 分页 class Page { late int total; late int limit; late int page; late int pages; Page.fromJson(Map json) { this.total = json["total"]; this.limit = json["limit"]; this.page = json["page"]; this.pages = json["pages"]; } Page.of(this.total, this.limit, this.page, this.pages); } /// 分类 class Category { late String id; late String title; late String description; late RemoteImageInfo thumb; late bool isWeb; late bool active; late String link; Category.fromJson(Map json) { this.id = json["_id"]; this.title = json["title"]; this.description = json["description"]; this.thumb = RemoteImageInfo.fromJson(json["thumb"]); this.isWeb = json["isWeb"]; this.active = json["active"]; this.link = json["link"]; } } /// 漫画分页 class ComicsPage extends Page { late List docs; ComicsPage.fromJson(Map json) : super.fromJson(json) { this.docs = List.from(json["docs"]) .map((e) => Map.from(e)) .map((e) => ComicSimple.fromJson(e)) .toList(); } } /// 漫画基本信息 class ComicSimple { late String id; late String title; late String author; late int pagesCount; late int epsCount; late bool finished; late List categories; late RemoteImageInfo thumb; late int likesCount; ComicSimple.fromJson(Map json) { this.id = json["_id"]; this.title = json["title"]; this.author = json["author"]; this.pagesCount = json["pagesCount"]; this.epsCount = json["epsCount"]; this.finished = json["finished"]; this.categories = List.from(json["categories"]); this.thumb = RemoteImageInfo.fromJson(json["thumb"]); this.likesCount = json["likesCount"]; } } /// 漫画详情 class ComicInfo extends ComicSimple { late String description; late String chineseTeam; late List tags; late String updatedAt; late String createdAt; late bool allowDownload; late int viewsCount; late bool isFavourite; late bool isLiked; late int commentsCount; late Creator creator; ComicInfo.fromJson(Map json) : super.fromJson(json) { this.description = json["description"]; this.chineseTeam = json["chineseTeam"]; this.tags = List.from(json["tags"]); this.updatedAt = (json["updated_at"]); this.createdAt = (json["created_at"]); this.allowDownload = json["allowDownload"]; this.viewsCount = json["viewsCount"]; this.isFavourite = json["isFavourite"]; this.isLiked = json["isLiked"]; this.commentsCount = json["commentsCount"]; this.creator = Creator.fromJson(Map.of(json["_creator"])); } } /// 漫画创建人信息 class Creator extends BasicUser { late String role; late String character; Creator.fromJson(Map json) : super.fromJson(json) { this.role = json["role"]; this.character = json["character"]; } } /// 漫画章节 class Ep { late String id; late String title; late int order; late String updatedAt; Ep.fromJson(Map json) { this.id = json["_id"]; this.title = json["title"]; this.order = json["order"]; this.updatedAt = (json["updated_at"]); } } /// 漫画章节分页 class EpPage extends Page { late List docs; EpPage.fromJson(Map json) : super.fromJson(json) { this.docs = List.from(json["docs"]) .map((e) => Map.from(e)) .map((e) => Ep.fromJson(e)) .toList(); } } /// 漫画图片分页 class PicturePage extends Page { late List docs; PicturePage.fromJson(Map json) : super.fromJson(json) { this.docs = List.from(json["docs"]) .map((e) => Map.from(e)) .map((e) => Picture.fromJson(e)) .toList(); } } /// 漫画图片信息 class Picture { late String id; late RemoteImageInfo media; Picture.fromJson(Map json) { this.id = json["_id"]; this.media = RemoteImageInfo.fromJson(json["media"]); } } /// 显示图片数据 class RemoteImageData { late int fileSize; late String format; late int width; late int height; late String finalPath; RemoteImageData.forData( this.fileSize, this.format, this.width, this.height, this.finalPath, ); RemoteImageData.fromJson(Map json) { this.fileSize = json["fileSize"]; this.format = json["format"]; this.width = json["width"]; this.height = json["height"]; this.finalPath = json["finalPath"]; } } /// 漫画评论分页 class CommentPage extends Page { late List docs; CommentPage.fromJson(Map json) : super.fromJson(json) { this.docs = List.from(json["docs"]) .map((e) => Map.from(e)) .map((e) => Comment.fromJson(e)) .toList(); } } class CommentBase { late String id; late String content; late CommentUser user; late bool isTop; late bool hide; late String createdAt; late int likesCount; late int commentsCount; late bool isLiked; CommentBase.fromJson(Map json) { this.id = json["_id"]; this.content = json["content"]; this.user = CommentUser.fromJson(Map.of(json["_user"])); this.isTop = json["isTop"]; this.hide = json["hide"]; this.createdAt = json["created_at"]; this.likesCount = json["likesCount"]; this.commentsCount = json["commentsCount"]; this.isLiked = json["isLiked"]; } } /// 子评论 class ChildOfComment extends CommentBase { late String parent; ChildOfComment.fromJson(Map json) : super.fromJson(json) { this.parent = json["_parent"]; } } /// 漫画评论详情 class Comment extends CommentBase { late String comic; Comment.fromJson(Map json) : super.fromJson(json) { this.comic = json["_comic"]; } } /// 评论的用户信息 class CommentUser extends BasicUser { late String role; CommentUser.fromJson(Map json) : super.fromJson(json) { this.role = json["role"]; } } /// 已下载图片的信息 class DownloadPicture { late int rankInEp; late String fileServer; late String path; late String localPath; late int width; late int height; late String format; late int fileSize; DownloadPicture.fromJson(Map json) { this.rankInEp = json["rankInEp"]; this.fileServer = json["fileServer"]; this.path = json["path"]; this.localPath = json["localPath"]; this.width = json["width"]; this.height = json["height"]; this.format = json["format"]; this.fileSize = json["fileSize"]; } } /// 浏览历史记录 class ViewLog { late String id; late String title; late String author; late int pagesCount; late int epsCount; late bool finished; late String categories; late String thumbOriginalName; late String thumbFileServer; late String thumbPath; late String description; late String chineseTeam; late String tags; late String lastViewTime; late int lastViewEpOrder; late String lastViewEpTitle; late int lastViewPictureRank; ViewLog.fromJson(Map json) { this.id = json["id"]; this.title = json["title"]; this.author = json["author"]; this.pagesCount = json["pagesCount"]; this.epsCount = json["epsCount"]; this.finished = json["finished"]; this.categories = json["categories"]; this.thumbOriginalName = json["thumbOriginalName"]; this.thumbFileServer = json["thumbFileServer"]; this.thumbPath = json["thumbPath"]; this.description = json["description"]; this.chineseTeam = json["chineseTeam"]; this.tags = json["tags"]; this.lastViewTime = json["lastViewTime"]; this.lastViewEpOrder = json["lastViewEpOrder"]; this.lastViewEpTitle = json["lastViewEpTitle"]; this.lastViewPictureRank = json["lastViewPictureRank"]; } } /// 已下载漫画的信息 class DownloadComic { late String id; late String createdAt; late String updatedAt; late String title; late String author; late int pagesCount; late int epsCount; late bool finished; late String categories; late String thumbOriginalName; late String thumbFileServer; late String thumbPath; late String thumbLocalPath; late String description; late String chineseTeam; late String tags; late int selectedEpCount; late int selectedPictureCount; late int downloadEpCount; late int downloadPictureCount; late bool downloadFinished; late String downloadFinishedTime; late bool downloadFailed; late bool deleting; void copy(DownloadComic other) { this.id = other.id; this.createdAt = other.createdAt; this.updatedAt = other.updatedAt; this.title = other.title; this.author = other.author; this.pagesCount = other.pagesCount; this.epsCount = other.epsCount; this.finished = other.finished; this.categories = other.categories; this.thumbOriginalName = other.thumbOriginalName; this.thumbFileServer = other.thumbFileServer; this.thumbPath = other.thumbPath; this.description = other.description; this.chineseTeam = other.chineseTeam; this.tags = other.tags; this.selectedEpCount = other.selectedEpCount; this.selectedPictureCount = other.selectedPictureCount; this.downloadEpCount = other.downloadEpCount; this.downloadPictureCount = other.downloadPictureCount; this.downloadFinished = other.downloadFinished; this.downloadFinishedTime = other.downloadFinishedTime; this.downloadFailed = other.downloadFailed; this.thumbLocalPath = other.thumbLocalPath; // this.deleting = other.deleting; } DownloadComic.fromJson(Map json) { this.id = json["id"]; this.createdAt = (json["createdAt"]); this.updatedAt = (json["updatedAt"]); this.title = json["title"]; this.author = json["author"]; this.pagesCount = json["pagesCount"]; this.epsCount = json["epsCount"]; this.finished = json["finished"]; this.categories = json["categories"]; this.thumbOriginalName = json["thumbOriginalName"]; this.thumbFileServer = json["thumbFileServer"]; this.thumbPath = json["thumbPath"]; this.description = json["description"]; this.chineseTeam = json["chineseTeam"]; this.tags = json["tags"]; this.selectedEpCount = json["selectedEpCount"]; this.selectedPictureCount = json["selectedPictureCount"]; this.downloadEpCount = json["downloadEpCount"]; this.downloadPictureCount = json["downloadPictureCount"]; this.downloadFinished = json["downloadFinished"]; this.downloadFinishedTime = json["downloadFinishedTime"]; this.downloadFailed = json["downloadFailed"]; this.deleting = json["deleting"]; this.thumbLocalPath = json["thumbLocalPath"]; } } /// 已下载的章节信息 class DownloadEp { late String comicId; late String id; late String updatedAt; late int epOrder; late String title; late bool fetchedPictures; late int selectedPictureCount; late int downloadPictureCount; late bool downloadFinish; late String downloadFinishTime; late bool downloadFailed; DownloadEp.fromJson(Map json) { this.comicId = json["comicId"]; this.id = json["id"]; this.epOrder = json["epOrder"]; this.title = json["title"]; this.fetchedPictures = json["fetchedPictures"]; this.selectedPictureCount = json["selectedPictureCount"]; this.downloadPictureCount = json["downloadPictureCount"]; this.downloadFinish = json["downloadFinish"]; this.downloadFinishTime = json["downloadFinishTime"]; this.downloadFailed = json["downloadFailed"]; } } /// 游戏的分页 class GamePage extends Page { late List docs; GamePage.fromJson(Map json) : super.fromJson(json) { this.docs = List.of(json["docs"]) .map((e) => Map.of(e)) .map((e) => GameSimple.fromJson(e)) .toList(); } } /// 游戏的简要信息 class GameSimple { late String id; late String title; late String version; late RemoteImageInfo icon; late String publisher; late bool adult; late bool suggest; late int likesCount; late bool android; late bool ios; GameSimple.fromJson(Map json) { this.id = json["_id"]; this.title = json["title"]; this.version = json["version"]; this.icon = RemoteImageInfo.fromJson(json["icon"]); this.publisher = json["publisher"]; this.adult = json["adult"]; this.suggest = json["suggest"]; this.likesCount = json["likesCount"]; this.android = json["android"]; this.ios = json["ios"]; } } /// 游戏详情 class GameInfo extends GameSimple { late String description; late String updateContent; late String videoLink; late List screenshots; late int commentsCount; late int downloadsCount; late bool isLiked; late List androidLinks; late double androidSize; late List iosLinks; late double iosSize; late String updatedAt; late String createdAt; GameInfo.fromJson(Map json) : super.fromJson(json) { this.description = json["description"]; this.updateContent = json["updateContent"]; this.videoLink = json["videoLink"]; this.screenshots = List.of(json["screenshots"]) .map((e) => Map.of(e)) .map((e) => RemoteImageInfo.fromJson(e)) .toList(); this.commentsCount = json["commentsCount"]; this.downloadsCount = json["downloadsCount"]; this.isLiked = json["isLiked"]; this.androidLinks = List.of(json["androidLinks"]).map((e) => "$e").toList(); this.androidSize = double.parse(json["androidSize"].toString()); this.iosLinks = List.of(json["iosLinks"]).map((e) => "$e").toList(); this.iosSize = double.parse(json["iosSize"].toString()); this.updatedAt = json["updated_at"]; this.createdAt = json["created_at"]; } } /// 我的评论页面分页 class MyCommentsPage extends Page { late List docs; MyCommentsPage.fromJson(Map json) : super.fromJson(json) { this.docs = List.of(json["docs"]).map((e) => MyComment.fromJson(e)).toList(); } } /// 我的评论 class MyComment { late String id; late String content; late bool hide; late String createdAt; late int likesCount; late int commentsCount; late bool isLiked; late MyCommentComic comic; MyComment.fromJson(Map json) { this.id = json["_id"]; this.content = json["content"]; this.hide = json["hide"]; this.createdAt = json["created_at"]; this.likesCount = json["likesCount"]; this.commentsCount = json["commentsCount"]; this.isLiked = json["isLiked"]; this.comic = MyCommentComic.fromJson(json["_comic"]); } } /// 我的评论漫画简要信息 class MyCommentComic { late String id; late String title; MyCommentComic.fromJson(Map json) { this.id = json["_id"]; this.title = json["title"]; } } /// 子评论分页 class CommentChildrenPage extends Page { late List docs; CommentChildrenPage.fromJson(Map json) : super.fromJson(json) { this.docs = []; if (json["docs"] != null) { docs.addAll( List.of(json["docs"]).map((e) => CommentChild.fromJson(e)).toList()); } } } /// 子评论 class CommentChild extends ChildOfComment { late String comic; CommentChild.fromJson(Map json) : super.fromJson(json) { this.comic = json["_comic"]; } } /// 漫画评论分页 class GameCommentPage extends Page { late List docs; GameCommentPage.fromJson(Map json) : super.fromJson(json) { this.docs = List.from(json["docs"]) .map((e) => Map.from(e)) .map((e) => GameComment.fromJson(e)) .toList(); } } /// 游戏评论 class GameComment extends CommentBase { late String game; GameComment.fromJson(Map json) : super.fromJson(json) { this.game = json["_game"]; } } /// 子评论分页 class GameCommentChildrenPage extends Page { late List docs; GameCommentChildrenPage.fromJson(Map json) : super.fromJson(json) { this.docs = []; if (json["docs"] != null) { docs.addAll(List.of(json["docs"]) .map((e) => GameCommentChild.fromJson(e)) .toList()); } } } /// 子评论 class GameCommentChild extends ChildOfComment { late String game; GameCommentChild.fromJson(Map json) : super.fromJson(json) { this.game = json["_game"]; } } class Collection { late String title; late List comics; Collection.fromJson(Map json) { this.title = json["title"]; this.comics = List.from(json["comics"]) .map((e) => Map.from(e)) .map((e) => ComicSimple.fromJson(e)) .toList(); } } class PkzArchive { PkzArchive({ required this.coverPath, required this.authorAvatarPath, required this.comics, required this.comicCount, required this.volumesCount, required this.chapterCount, required this.pictureCount, }); late final String coverPath; late final String authorAvatarPath; late final List comics; late final int comicCount; late final int volumesCount; late final int chapterCount; late final int pictureCount; PkzArchive.fromJson(Map json) { coverPath = json['cover_path']; authorAvatarPath = json['author_avatar_path']; comics = List.from(json['comics']).map((e) => PkzComic.fromJson(e)).toList(); comicCount = json['comic_count']; volumesCount = json['volumes_count']; chapterCount = json['chapter_count']; pictureCount = json['picture_count']; } Map toJson() { final _data = {}; _data['cover_path'] = coverPath; _data['author_avatar_path'] = authorAvatarPath; _data['comics'] = comics.map((e) => e.toJson()).toList(); _data['comic_count'] = comicCount; _data['volumes_count'] = volumesCount; _data['chapter_count'] = chapterCount; _data['picture_count'] = pictureCount; return _data; } } class PkzComic { PkzComic({ required this.id, required this.title, required this.categories, required this.tags, required this.updatedAt, required this.createdAt, required this.description, required this.chineseTeam, required this.finished, required this.coverPath, required this.authorAvatarPath, required this.volumes, required this.volumesCount, required this.chapterCount, required this.pictureCount, required this.idx, }); late final String id; late final String title; late final List categories; late final List tags; late final int updatedAt; late final int createdAt; late final String description; late final String chineseTeam; late final bool finished; late final String coverPath; late final String authorAvatarPath; late final List volumes; late final int volumesCount; late final int chapterCount; late final int pictureCount; late final int idx; late final String author; late final String authorId; PkzComic.fromJson(Map json) { id = json['id']; title = json['title']; categories = List.castFrom(json['categories']); tags = List.castFrom(json['tags']); updatedAt = json['updated_at']; createdAt = json['created_at']; description = json['description']; chineseTeam = json['chinese_team']; finished = json['finished']; coverPath = json['cover_path']; authorAvatarPath = json['author_avatar_path']; volumes = List.from(json['volumes']).map((e) => PkzVolume.fromJson(e)).toList(); volumesCount = json['volumes_count']; chapterCount = json['chapter_count']; pictureCount = json['picture_count']; idx = json['idx']; author = json['author']; authorId = json['author_id']; } Map toJson() { final _data = {}; _data['id'] = id; _data['title'] = title; _data['categories'] = categories; _data['tags'] = tags; _data['updated_at'] = updatedAt; _data['created_at'] = createdAt; _data['description'] = description; _data['chinese_team'] = chineseTeam; _data['finished'] = finished; _data['cover_path'] = coverPath; _data['author_avatar_path'] = authorAvatarPath; _data['volumes'] = volumes.map((e) => e.toJson()).toList(); _data['volumes_count'] = volumesCount; _data['chapter_count'] = chapterCount; _data['picture_count'] = pictureCount; _data['idx'] = idx; _data['author'] = author; _data['author_id'] = authorId; return _data; } } class PkzVolume { PkzVolume({ required this.id, required this.title, required this.updatedAt, required this.createdAt, required this.coverPath, required this.chapters, required this.chapterCount, required this.pictureCount, required this.idx, }); late final String id; late final String title; late final int updatedAt; late final int createdAt; late final String coverPath; late final List chapters; late final int chapterCount; late final int pictureCount; late final int idx; PkzVolume.fromJson(Map json) { id = json['id']; title = json['title']; updatedAt = json['updated_at']; createdAt = json['created_at']; coverPath = json['cover_path']; chapters = List.from(json['chapters']).map((e) => PkzChapter.fromJson(e)).toList(); chapterCount = json['chapter_count']; pictureCount = json['picture_count']; idx = json['idx']; } Map toJson() { final _data = {}; _data['id'] = id; _data['title'] = title; _data['updated_at'] = updatedAt; _data['created_at'] = createdAt; _data['cover_path'] = coverPath; _data['chapters'] = chapters.map((e) => e.toJson()).toList(); _data['chapter_count'] = chapterCount; _data['picture_count'] = pictureCount; _data['idx'] = idx; return _data; } } class PkzChapter { PkzChapter({ required this.id, required this.title, required this.updatedAt, required this.createdAt, required this.coverPath, required this.pictures, required this.pictureCount, required this.idx, }); late final String id; late final String title; late final int updatedAt; late final int createdAt; late final String coverPath; late final List pictures; late final int pictureCount; late final int idx; PkzChapter.fromJson(Map json) { id = json['id']; title = json['title']; updatedAt = json['updated_at']; createdAt = json['created_at']; coverPath = json['cover_path']; pictures = List.from(json['pictures']).map((e) => PkzPicture.fromJson(e)).toList(); pictureCount = json['picture_count']; idx = json['idx']; } Map toJson() { final _data = {}; _data['id'] = id; _data['title'] = title; _data['updated_at'] = updatedAt; _data['created_at'] = createdAt; _data['cover_path'] = coverPath; _data['pictures'] = pictures.map((e) => e.toJson()).toList(); _data['picture_count'] = pictureCount; _data['idx'] = idx; return _data; } } class PkzPicture { PkzPicture({ required this.id, required this.title, required this.width, required this.height, required this.format, required this.picturePath, required this.idx, }); late final String id; late final String title; late final int width; late final int height; late final String format; late final String picturePath; late final int idx; PkzPicture.fromJson(Map json) { id = json['id']; title = json['title']; width = json['width']; height = json['height']; format = json['format']; picturePath = json['picture_path']; idx = json['idx']; } Map toJson() { final _data = {}; _data['id'] = id; _data['title'] = title; _data['width'] = width; _data['height'] = height; _data['format'] = format; _data['picture_path'] = picturePath; _data['idx'] = idx; return _data; } } class Knight extends BasicUser { late final String role; late final String character; late final int comicsUploaded; Knight.fromJson(Map json) : super.fromJson(json) { role = json['role']; character = json['character']; comicsUploaded = json['comicsUploaded']; } } class PkzComicViewLog { PkzComicViewLog({ required this.fileName, required this.lastViewComicId, required this.filePath, required this.lastViewComicTitle, required this.lastViewEpId, required this.lastViewEpName, required this.lastViewPictureRank, required this.lastViewTime, }); late final String fileName; late final String lastViewComicId; late final String filePath; late final String lastViewComicTitle; late final String lastViewEpId; late final String lastViewEpName; late final int lastViewPictureRank; late final String lastViewTime; PkzComicViewLog.fromJson(Map json) { fileName = json['fileName']; lastViewComicId = json['lastViewComicId']; filePath = json['filePath']; lastViewComicTitle = json['lastViewComicTitle']; lastViewEpId = json['lastViewEpId']; lastViewEpName = json['lastViewEpName']; lastViewPictureRank = json['lastViewPictureRank']; lastViewTime = json['lastViewTime']; } Map toJson() { final _data = {}; _data['fileName'] = fileName; _data['lastViewComicId'] = lastViewComicId; _data['filePath'] = filePath; _data['lastViewComicTitle'] = lastViewComicTitle; _data['lastViewEpId'] = lastViewEpId; _data['lastViewEpName'] = lastViewEpName; _data['lastViewPictureRank'] = lastViewPictureRank; _data['lastViewTime'] = lastViewTime; return _data; } } class ProInfoAll { ProInfoAll({ required this.proInfoAf, required this.proInfoPat, }); late final ProInfoAf proInfoAf; late final ProInfoPat proInfoPat; ProInfoAll.fromJson(Map json) { proInfoAf = ProInfoAf.fromJson(json['pro_info_af']); proInfoPat = ProInfoPat.fromJson(json['pro_info_pat']); } Map toJson() { final _data = {}; _data['pro_info_normal'] = proInfoAf.toJson(); _data['pro_info_pat'] = proInfoPat.toJson(); return _data; } } class ProInfoAf { ProInfoAf({ required this.isPro, required this.expire, }); late final bool isPro; late final int expire; ProInfoAf.fromJson(Map json) { isPro = json['is_pro']; expire = json['expire']; } Map toJson() { final _data = {}; _data['is_pro'] = isPro; _data['expire'] = expire; return _data; } } class ProInfoPat { ProInfoPat({ required this.isPro, required this.patId, required this.bindUid, required this.requestDelete, required this.reBind, required this.errorType, required this.errorMsg, required this.accessKey, }); late final bool isPro; late final String patId; late final String bindUid; late final int requestDelete; late final int reBind; late final int errorType; late final String errorMsg; late final String accessKey; ProInfoPat.fromJson(Map json) { isPro = json['is_pro']; patId = json['pat_id']; bindUid = json['bind_uid']; requestDelete = json['request_delete']; reBind = json['re_bind']; errorType = json['error_type']; errorMsg = json['error_msg']; accessKey = json['access_key']; } Map toJson() { final _data = {}; _data['is_pro'] = isPro; _data['pat_id'] = patId; _data['bind_uid'] = bindUid; _data['request_delete'] = requestDelete; _data['re_bind'] = reBind; _data['error_type'] = errorType; _data['error_msg'] = errorMsg; _data['access_key'] = accessKey; return _data; } } class ForgotPasswordResult { ForgotPasswordResult({ required this.question1, required this.question2, required this.question3, }); late final String question1; late final String question2; late final String question3; ForgotPasswordResult.fromJson(Map json) { question1 = json['question1']; question2 = json['question2']; question3 = json['question3']; } Map toJson() { final _data = {}; _data['question1'] = question1; _data['question2'] = question2; _data['question3'] = question3; return _data; } } class ResetPasswordResult { ResetPasswordResult({ required this.password, }); late final String password; ResetPasswordResult.fromJson(Map json) { password = json['password']; } Map toJson() { final _data = {}; _data['password'] = password; return _data; } } /// 订阅 class ComicSubscribe { late String id; late String title; late String author; late int pagesCount; late int epsCount; late bool finished; late String categories; late String thumbOriginalName; late String thumbFileServer; late String thumbPath; late String description; late String chineseTeam; late String tags; late int likesCount; late String subscribeTime; late String updateSubscribeTime; late int newEpCount; ComicSubscribe.fromJson(Map json) { print(json); this.id = json["id"]; this.title = json["title"]; this.author = json["author"]; this.pagesCount = json["pagesCount"]; this.epsCount = json["epsCount"]; this.finished = json["finished"]; this.categories = json["categories"]; this.thumbOriginalName = json["thumbOriginalName"]; this.thumbFileServer = json["thumbFileServer"]; this.thumbPath = json["thumbPath"]; this.description = json["description"]; this.chineseTeam = json["chineseTeam"]; this.tags = json["tags"]; this.likesCount = json["likesCount"]; this.subscribeTime = json["subscribeTime"]; this.updateSubscribeTime = json["updateSubscribeTime"]; this.newEpCount = json["newEpCount"]; } Map toSimpleJson() { final _data = {}; _data['id'] = id; _data['_id'] = id; _data['title'] = title; _data['author'] = author; _data['pagesCount'] = pagesCount; _data['epsCount'] = epsCount; _data['finished'] = finished; _data['categories'] = jsonDecode(categories); _data['thumbOriginalName'] = thumbOriginalName; _data['thumbFileServer'] = thumbFileServer; _data['thumbPath'] = thumbPath; _data['description'] = description; _data['chineseTeam'] = chineseTeam; _data['tags'] = tags; _data['likesCount'] = likesCount; _data['thumb'] = jsonDecode(jsonEncode(RemoteImageInfo.fromJson({ "originalName": thumbOriginalName, "fileServer": thumbFileServer, "path": thumbPath }))); _data['subscribeTime'] = subscribeTime; _data['updateSubscribeTime'] = updateSubscribeTime; _data['newEpCount'] = newEpCount; return _data; } } /// 本地收藏夹文件夹 class LocalFavoriteFolder { late String id; late String name; late int createdAt; late int updatedAt; late int deletedAt; LocalFavoriteFolder.fromJson(Map json) { this.id = json["id"]; this.name = json["name"]; this.createdAt = json["createdAt"]; this.updatedAt = json["updatedAt"]; this.deletedAt = json["deletedAt"]; } Map toJson() { final _data = {}; _data['id'] = id; _data['name'] = name; _data['createdAt'] = createdAt; _data['updatedAt'] = updatedAt; _data['deletedAt'] = deletedAt; return _data; } } /// 本地收藏夹漫画 class LocalFavoriteComic { late String comicId; late String folderId; String? info; late int createdAt; late int updatedAt; late int deletedAt; LocalFavoriteComic.fromJson(Map json) { this.comicId = json["comicId"]; this.folderId = json["folderId"]; this.info = json["info"]; this.createdAt = json["createdAt"]; this.updatedAt = json["updatedAt"]; this.deletedAt = json["deletedAt"]; } Map toJson() { final _data = {}; _data['comicId'] = comicId; _data['folderId'] = folderId; _data['info'] = info; _data['createdAt'] = createdAt; _data['updatedAt'] = updatedAt; _data['deletedAt'] = deletedAt; return _data; } } ================================================ FILE: lib/basic/Method.dart ================================================ import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:pikapika/basic/Entities.dart'; /// 使用MethodChannel与平台通信 final method = Method._(); class Method { /// 禁止其他页面构造此类 Method._(); /// channel final MethodChannel _channel = const MethodChannel("method"); /// 平铺调用, 为了直接与golang进行通信 Future _flatInvoke(String method, dynamic params) { return _channel.invokeMethod("flatInvoke", { "method": method, "params": params is String ? params : jsonEncode(params), }); } /// 读取配置文件 Future loadProperty(String propertyName, String defaultValue) async { return await _flatInvoke("loadProperty", { "name": propertyName, "defaultValue": defaultValue, }); } /// 远程推荐链接(来自 pikapika-config) Future> configLinks() async { final rsp = await _flatInvoke("configLinks", ""); final decoded = jsonDecode(rsp); if (decoded is! Map) { return {}; } return decoded.map((key, value) => MapEntry("$key", "$value")); } /// 应用配置(版本信息、下载地址等) Future> appConfig() async { final rsp = await _flatInvoke("appConfig", ""); final decoded = jsonDecode(rsp); if (decoded is! Map) { return {}; } return Map.from(decoded); } /// 保存配置文件 Future saveProperty(String propertyName, String value) { return _flatInvoke("saveProperty", { "name": propertyName, "value": value, }); } /// 获取当前的分流 Future getSwitchAddress() async { return await _flatInvoke("getSwitchAddress", ""); } /// 更换分流 Future setSwitchAddress(String switchAddress) async { return await _flatInvoke("setSwitchAddress", switchAddress); } /// 获取当前的图片分流 Future getImageSwitchAddress() async { return await _flatInvoke("getImageSwitchAddress", ""); } /// 更换图片分流 Future setImageSwitchAddress(String switchAddress) async { return await _flatInvoke("setImageSwitchAddress", switchAddress); } Future getUseApiClientLoadImage() async { return await _flatInvoke("getUseApiClientLoadImage", ""); } Future setUseApiClientLoadImage(String switchAddress) async { return await _flatInvoke("setUseApiClientLoadImage", switchAddress); } /// 获取代理 Future getProxy() async { return await _flatInvoke("getProxy", ""); } /// 更换当前的代理 Future setProxy(String proxy) async { return await _flatInvoke("setProxy", proxy); } /// 获取用户名 Future getUsername() async { return await _flatInvoke("getUsername", ""); } /// 设置用户名 Future setUsername(String username) async { return await _flatInvoke("setUsername", username); } /// 获取密码 Future getPassword() async { return await _flatInvoke("getPassword", ""); } /// 设置密码 Future setPassword(String password) async { return await _flatInvoke("setPassword", password); } /// 预登录, 程序启用时会调用 /// 如果又账号密码或token, 且登录成功, 将返回true Future preLogin() async { String rsp = await _flatInvoke("preLogin", ""); return rsp == "true"; } /// 登录 Future login() async { return _flatInvoke("login", ""); } /// 注册 Future register( String email, String name, String password, String gender, String birthday, String question1, String answer1, String question2, String answer2, String question3, String answer3) { return _flatInvoke("register", { "email": email, "name": name, "password": password, "gender": gender, "birthday": birthday, "question1": question1, "answer1": answer1, "question2": question2, "answer2": answer2, "question3": question3, "answer3": answer3, }); } /// 退出登录 Future clearToken() { return _flatInvoke("clearToken", ""); } /// 获取用户自身基础信息 Future userProfile() async { String rsp = await _flatInvoke("userProfile", ""); return UserProfile.fromJson(json.decode(rsp)); } /// 打卡 Future punchIn() { return _flatInvoke("punchIn", ""); } /// 使用服务器地址以及路径获取图片用户显示 /// 如果本地有缓存会返回路径, 如果本地没有缓存会下载再返回路径, 没有下载成功则会抛出异常 Future remoteImageData( String fileServer, String path) async { var data = await _flatInvoke("remoteImageData", { "fileServer": fileServer, "path": path, }); return RemoteImageData.fromJson(json.decode(data)); } /// 功能同上, 用于预加载 Future remoteImagePreload(String fileServer, String path) async { return _flatInvoke("remoteImagePreload", { "fileServer": fileServer, "path": path, }); } /// 获取已经下载好图片的保存位置 Future downloadImagePath(String path) async { return await _flatInvoke("downloadImagePath", path); } /// 获取分类 Future> categories() async { String rsp = await _flatInvoke("categories", ""); List list = json.decode(rsp); return list.map((e) => Category.fromJson(e)).toList(); } /// 列出漫画 /// [sort] 排序方式 /// [page] 页数 /// [category] 分类 /// [tag] 标签 /// [creatorId] 创建人ID /// [chineseTeam] 汉化组名称 /// * 几种条件使用且的关系 Future comics( String sort, int page, { String category = "", String tag = "", String author = "", String creatorId = "", String chineseTeam = "", }) async { String rsp = await _flatInvoke("comics", { "category": category, "tag": tag, "author": author, "creatorId": creatorId, "chineseTeam": chineseTeam, "sort": sort, "page": page, }); return ComicsPage.fromJson(json.decode(rsp)); } /// 搜索漫画 Future searchComics(String keyword, String sort, int page) { return searchComicsInCategories(keyword, sort, page, []); } /// 搜索漫画, 在多个分类中 Future searchComicsInCategories( String keyword, String sort, int page, List categories) async { String rsp = await _flatInvoke("searchComics", { "keyword": keyword, "sort": sort, "page": page, "categories": categories, }); return ComicsPage.fromJson(json.decode(rsp)); } /// 随机漫画 Future> randomComics() async { String data = await _flatInvoke("randomComics", ""); return List.of(jsonDecode(data)) .map((e) => Map.of(e)) .map((e) => ComicSimple.fromJson(e)) .toList(); } /// 漫画榜单 /// [type] 榜单类型 H24 D7 D30 Future> leaderboard(String type) async { String data = await _flatInvoke("leaderboard", type); return List.of(jsonDecode(data)) .map((e) => Map.of(e)) .map((e) => ComicSimple.fromJson(e)) .toList(); } /// 获取漫画详情 Future comicInfo(String comicId, bool ignoreHistory) async { String rsp = await _flatInvoke(ignoreHistory ? "comicInfoA" : "comicInfo", comicId); return ComicInfo.fromJson(json.decode(rsp)); } /// 分页获取漫画的章节 Future comicEpPage(String comicId, int page) async { String rsp = await _flatInvoke("comicEpPage", { "comicId": comicId, "page": page, }); return EpPage.fromJson(json.decode(rsp)); } /// 分页获取一个章节的图片, 并且需要图片的质量参数 Future comicPicturePageWithQuality( String comicId, int epOrder, int page, String quality) async { String data = await _flatInvoke("comicPicturePageWithQuality", { "comicId": comicId, "epOrder": epOrder, "page": page, "quality": quality, }); return PicturePage.fromJson(json.decode(data)); } /// 对漫画进行点赞/取消点赞操作 Future switchLike(String comicId) async { return await _flatInvoke("switchLike", comicId); } /// 对漫画进行收藏/取消收藏操作 Future switchFavourite(String comicId) async { return await _flatInvoke("switchFavourite", comicId); } /// 收藏漫画列表 Future favouriteComics(String sort, int page) async { var rsp = await _flatInvoke("favouriteComics", { "sort": sort, "page": page, }); return ComicsPage.fromJson(json.decode(rsp)); } /// 看了此漫画的人还看了... Future> recommendation(String comicId) async { String rsp = await _flatInvoke("recommendation", comicId); List list = json.decode(rsp); return list.map((e) => ComicSimple.fromJson(e)).toList(); } /// 对漫画发送评论 Future postComment(String comicId, String content) { return _flatInvoke("postComment", { "comicId": comicId, "content": content, }); } /// 发送子评论 Future postChildComment(String commentId, String content) { return _flatInvoke("postChildComment", { "commentId": commentId, "content": content, }); } /// 漫画的评论列表 Future comments(String comicId, int page) async { var rsp = await _flatInvoke("comments", { "comicId": comicId, "page": page, }); return CommentPage.fromJson(json.decode(rsp)); } /// 拉取子评论 Future commentChildren( String comicId, String commentId, int page, ) async { var rsp = await _flatInvoke("commentChildren", { "comicId": comicId, "commentId": commentId, "page": page, }); return CommentChildrenPage.fromJson(json.decode(rsp)); } /// 喜欢/取消喜欢 一条评论 Future switchLikeComment(String commentId, String comicId) { return _flatInvoke("switchLikeComment", { "commentId": commentId, "comicId": comicId, }); } /// 我的评论列表 Future myComments(int page) async { String response = await _flatInvoke("myComments", "$page"); return MyCommentsPage.fromJson(jsonDecode(response)); } /// 浏览记录 Future> viewLogPage(int offset, int limit) async { var data = await _flatInvoke("viewLogPage", { "offset": offset, "limit": limit, }); List list = json.decode(data); return list.map((e) => ViewLog.fromJson(e)).toList(); } /// 清除所有的浏览记录 Future clearAllViewLog() { return _flatInvoke("clearAllViewLog", ""); } /// 删除一个漫画的浏览记录 Future deleteViewLog(String id) { return _flatInvoke("deleteViewLog", id); } /// 删除一个漫画的浏览记录 Future viewComic(String id) { return _flatInvoke("viewComic", id); } /// 游戏列表 Future games(int page) async { var data = await _flatInvoke("games", "$page"); return GamePage.fromJson(json.decode(data)); } /// 游戏详情 Future game(String gameId) async { var data = await _flatInvoke("game", gameId); return GameInfo.fromJson(json.decode(data)); } /// 游戏的评论列表 Future gameComments(String gameId, int page) async { var rsp = await _flatInvoke("gameComments", { "gameId": gameId, "page": page, }); return GameCommentPage.fromJson(json.decode(rsp)); } /// 对游戏发送评论 Future postGameComment(String gameId, String content) { return _flatInvoke("postGameComment", { "gameId": gameId, "content": content, }); } /// 拉取游戏子评论 Future gameCommentChildren( String gameId, String commentId, int page, ) async { var rsp = await _flatInvoke("gameCommentChildren", { "gameId": gameId, "commentId": commentId, "page": page, }); return GameCommentChildrenPage.fromJson(json.decode(rsp)); } /// 喜欢/取消喜欢 一条游戏评论 Future switchLikeGameComment(String commentId, String gameId) { return _flatInvoke("switchLikeGameComment", { "commentId": commentId, "gameId": gameId, }); } /// 发送游戏子评论 Future postGameChildComment(String commentId, String content) { return _flatInvoke("postGameChildComment", { "commentId": commentId, "content": content, }); } /// 清理网络缓存 Future cleanNetworkCache() { return _flatInvoke("cleanNetworkCache", ""); } /// 清理图片缓存 Future cleanImageCache() { return _flatInvoke("cleanImageCache", ""); } /// 清理缓存 Future clean() { return _flatInvoke("clean", ""); } /// 清理[expireSec]秒以前的缓存 Future autoClean(String expireSec) { return _flatInvoke("autoClean", expireSec); } /// 保存当前浏览器的进度 Future storeViewEp( String comicId, int epOrder, String epTitle, int pictureRank) { return _flatInvoke("storeViewEp", { "comicId": comicId, "epOrder": epOrder, "epTitle": epTitle, "pictureRank": pictureRank, }); } /// 加载浏览进度 Future loadView(String comicId) async { String data = await _flatInvoke("loadView", comicId); if (data == "") { return null; } return ViewLog.fromJson(jsonDecode(data)); } /// 下载是否在后台运行 Future downloadRunning() async { String rsp = await _flatInvoke("downloadRunning", ""); return rsp == "true"; } /// 暂停/继续 下载 Future setDownloadRunning(bool status) async { return _flatInvoke("setDownloadRunning", "$status"); } /// 下载漫画 Future createDownload( Map comic, List> epList) async { return _flatInvoke("createDownload", { "comic": comic, "epList": epList, }); } /// 追加下载的章节 Future addDownload( Map comic, List> epList) async { await _flatInvoke("addDownload", { "comic": comic, "epList": epList, }); } /// 下载详情 Future loadDownloadComic(String comicId) async { var data = await _flatInvoke("loadDownloadComic", comicId); // 未找到 且 未异常 if (data == "") { return null; } return DownloadComic.fromJson(json.decode(data)); } /// 所有下载 Future> allDownloads(String search, {String customFolder = ""}) async { var data = await _flatInvoke("allDownloads", { "search": search, "customFolder": customFolder, }); data = jsonDecode(data); if (data == null) { return []; } List list = data; return list.map((e) => DownloadComic.fromJson(e)).toList(); } Future> allCustomFolders() async { var data = await _flatInvoke("allCustomFolders", ""); return List.of(jsonDecode(data)).map((e) => e.toString()).toList(); } /// 删除一个下载 Future deleteDownloadComic(String comicId) async { return _flatInvoke("deleteDownloadComic", comicId); } Future moveDownloadComic( List comicIdList, String customFolder) { return _flatInvoke("moveDownloadComic", { "comicIdList": comicIdList, "customFolder": customFolder, }); } /// 所有下载的EP Future> downloadEpList(String comicId) async { var data = await _flatInvoke("downloadEpList", comicId); List list = json.decode(data); return list.map((e) => DownloadEp.fromJson(e)).toList(); } /// 下载漫画这个EP下的图片 Future> downloadPicturesByEpId(String epId) async { var data = await _flatInvoke("downloadPicturesByEpId", epId); List list = json.decode(data); return list.map((e) => DownloadPicture.fromJson(e)).toList(); } /// 重置所有下载失败的漫画 Future resetFailed() async { return _flatInvoke("resetAllDownloads", ""); } /// 导出下载的漫画到zip Future exportComicDownload(String comicId, String dir, String name) { return _flatInvoke("exportComicDownload", { "comicId": comicId, "dir": dir, "name": name, }); } /// 导出下载的漫画到pki Future exportComicDownloadToPki( String comicId, String dir, String name) { return _flatInvoke("exportComicDownloadToPki", { "comicId": comicId, "dir": dir, "name": name, }); } /// 导出下载的图片到HTML+JPG (即使没有下载完成) Future exportComicJpegsEvenNotFinish( String comicId, String dir, String name, ) { return _flatInvoke("exportComicJpegsEvenNotFinish", { "comicId": comicId, "dir": dir, "name": name, }); } /// 导出下载的图片到HTML+JPG Future exportComicDownloadToJPG( String comicId, String dir, String name, ) { return _flatInvoke("exportComicDownloadToJPG", { "comicId": comicId, "dir": dir, "name": name, }); } /// 导出下载的图片到HTML+JPG Future exportComicDownloadToPDF( String comicId, String dir, String name, ) { return _flatInvoke("exportComicDownloadToPDF", { "comicId": comicId, "dir": dir, "name": name, }); } /// 导出下载的图片到HTML+JPG Future exportComicDownloadToEpub( String comicId, String dir, String name, ) { return _flatInvoke("exportComicDownloadToEpub", { "comicId": comicId, "dir": dir, "name": name, }); } /// 导出下载的图片到PDF文件夹 Future exportComicDownloadToPDFFolder( String comicId, String dir, String name, ) { return _flatInvoke("exportComicDownloadToPDFFolder", { "comicId": comicId, "dir": dir, "name": name, }); } /// 导出下载的图片到HTML+JPG Future exportComicDownloadJpegZip( String comicId, String dir, String name, ) { return _flatInvoke("exportComicDownloadJpegZip", { "comicId": comicId, "dir": dir, "name": name, }); } /// 导出下载的图片到PKZ Future exportComicDownloadToPkz( List comicIds, String dir, String name, ) { return _flatInvoke("exportComicDownloadToPkz", { "comicIds": comicIds, "dir": dir, "name": name, }); } /// 导出zip Future exportAnyComicDownloadsToZip( List comicIds, String dir, ) { return _flatInvoke("exportAnyComicDownloadsToZip", { "comicIds": comicIds, "dir": dir, }); } /// 导出pki Future exportAnyComicDownloadsToPki( List comicIds, String dir, ) { return _flatInvoke("exportAnyComicDownloadsToPki", { "comicIds": comicIds, "dir": dir, }); } /// 导出下载的漫画到cbzs.zip Future exportComicDownloadToCbzsZip( String comicId, String dir, String name) { return _flatInvoke("exportComicDownloadToCbzsZip", { "comicId": comicId, "dir": dir, "name": name, }); } /// 导入文件夹所有的文件 Future importComicDownloadDir( String dir, ) { return _flatInvoke("importComicDownloadDir", dir); } /// 使用网络将下载传输到其他设备 Future exportComicUsingSocket(String comicId) async { return int.parse(await _flatInvoke("exportComicUsingSocket", comicId)); } /// 传输窗口关闭时调用, 令socket关闭(如果传输没有结束) Future exportComicUsingSocketExit() { return _flatInvoke("exportComicUsingSocketExit", ""); } /// 从zip导入漫画 Future importComicDownload(String zipPath) { return _flatInvoke("importComicDownload", zipPath); } /// 从pki导入漫画 Future importComicDownloadPki(String zipPath) { return _flatInvoke("importComicDownloadPki", zipPath); } /// 从网络接收漫画 Future importComicDownloadUsingSocket(String addr) { return _flatInvoke("importComicDownloadUsingSocket", addr); } /// 获取本机的所有ip地址 Future clientIpSet() async { return await _flatInvoke("clientIpSet", ""); } /// 获取一个游戏的下载地址 Future> downloadGame(String url) async { if (url.startsWith("https://game.eroge.xyz/hhh.php")) { var data = await _flatInvoke("downloadGame", url); return [ url, ...List.of(jsonDecode(data)).map((e) => e.toString()), ]; } return [url]; } /// 保存图片(ios) Future iosSaveFileToImage(String path) async { return _channel.invokeMethod("iosSaveFileToImage", { "path": path, }); } /// 保存图片(android) Future androidSaveFileToImage(String path) async { return _channel.invokeMethod("androidSaveFileToImage", { "path": path, }); } /// 保存图片(PC) Future convertImageToJPEG100(String path, String dir) async { return _flatInvoke("convertImageToJPEG100", { "path": path, "dir": dir, }); } /// 获取安卓的屏幕刷新率 Future> loadAndroidModes() async { return List.of(await _channel.invokeMethod("androidGetModes")) .map((e) => "$e") .toList(); } /// 设置安卓的屏幕刷新率 Future setAndroidMode(String androidDisplayMode) { return _channel .invokeMethod("androidSetMode", {"mode": androidDisplayMode}); } /// 获取安卓的版本 Future androidGetVersion() async { return await _channel.invokeMethod("androidGetVersion", {}); } /// 数据文件保存位置 Future dataLocal() async { return await _channel.invokeMethod("dataLocal", {}); } /// 获取安卓支持的文件保存路径 Future> androidGetExtendDirs() async { String? tmp = await _channel.invokeMethod("androidGetExtendDirs", {}); if (tmp != null && tmp.isNotEmpty) { return tmp.split("|"); } return []; } /// 安卓文件迁移 Future migrate(String path) async { return _channel.invokeMethod("migrate", {"path": path}); } /// 下载的同时导出-配置获取 Future loadDownloadAndExportPath() { return _flatInvoke("loadDownloadAndExportPath", ""); } /// 下载的同时导出-设置 Future saveDownloadAndExportPath(String folder) { return _flatInvoke("saveDownloadAndExportPath", folder); } /// 使用下载缓存-配置获取 Future loadDownloadCachePath() { return _flatInvoke("loadDownloadCachePath", ""); } /// 使用下载缓存-设置 Future saveDownloadCachePath(String folder) { return _flatInvoke("saveDownloadCachePath", folder); } /// 切换安全策略 (安卓禁止截图, 录屏, 不显示在任务视图) Future androidSecureFlag(bool flag) { return _channel.invokeMethod("androidSecureFlag", { "flag": flag, }); } /// 获取下载线程数量 Future loadDownloadThreadCount() async { var strValue = await _flatInvoke("loadDownloadThreadCount", {}); return int.parse(strValue); } /// 设置下载线程数 Future saveDownloadThreadCount(int value) { return _flatInvoke("saveDownloadThreadCount", "$value"); } /// HTTP-GET-STRING Future defaultHttpClientGet(String url) async { return await _flatInvoke("defaultHttpClientGet", url); } /// 更新签名 Future updateSlogan(String input) async { return await _flatInvoke("updateSlogan", input); } /// 更新签名 Future updateAvatar(String data) async { return await _flatInvoke("updateAvatar", data); } /// 修改密码 Future updatePassword(String oldPassword, String newPassword) { return _flatInvoke("updatePassword", { "oldPassword": oldPassword, "newPassword": newPassword, }); } Future> loadViewedList(List list) async { return List.of(jsonDecode(await _flatInvoke("loadViewedList", list))) .cast(); } Future> collections() async { String rsp = await _flatInvoke("collections", ""); List list = json.decode(rsp); return list.map((e) => Collection.fromJson(e)).toList(); } Future verifyAuthentication() async { return await _channel.invokeMethod("verifyAuthentication"); } Future pkzInfo(String pkzPath) async { return PkzArchive.fromJson( jsonDecode(await _flatInvoke("pkzInfo", pkzPath))); } Future loadPkzFile(String pkzPath, String path) async { return base64Decode(await _flatInvoke("loadPkzFile", { "pkzPath": pkzPath, "path": path, })); } Future> pkzComicViewLogs( String fileName, String comicId, ) async { return List.of(jsonDecode(await _flatInvoke("pkzComicViewLogs", fileName))) .map((e) => PkzComicViewLog.fromJson(e)) .toList(); } Future pkzComicViewLogByPkzNameAndId( String fileName, String comicId, ) async { String data = await _flatInvoke("pkzComicViewLogByPkzNameAndId", { "fileName": fileName, "comicId": comicId, }); if (data == "" || data == "nil" || data == "null") { return null; } return PkzComicViewLog.fromJson(jsonDecode(data)); } Future viewPkz( String fileName, String filePath, ) async { return _flatInvoke("viewPkz", { "fileName": fileName, "filePath": filePath, }); } Future viewPkzComic( String fileName, String filePath, String comicId, String comicTitle, ) async { return _flatInvoke("viewPkzComic", { "fileName": fileName, "filePath": filePath, "comicId": comicId, "comicTitle": comicTitle, }); } Future viewPkzEpAndPicture( String fileName, String filePath, String comicId, String comicTitle, String epId, String epTitle, int pictureRank, ) async { return _flatInvoke("viewPkzEpAndPicture", { "fileName": fileName, "filePath": filePath, "comicId": comicId, "comicTitle": comicTitle, "epId": epId, "epTitle": epTitle, "pictureRank": pictureRank, }); } Future> leaderboardOfKnight() async { return List.of(jsonDecode(await _flatInvoke("leaderboardOfKnight", ""))) .map((e) => Knight.fromJson(e)) .toList(); } Future proInfoAll() async { return ProInfoAll.fromJson(jsonDecode(await _flatInvoke("proInfoAll", ""))); } Future reloadPro() { return _flatInvoke("reloadPro", ""); } Future inputCdKey(String cdKey) { return _flatInvoke("inputCdKey", cdKey); } Future reloadSwitchAddress() { return _flatInvoke("reloadSwitchAddress", ""); } Future resetSwitchAddress() { return _flatInvoke("resetSwitchAddress", ""); } Future iosGetDocumentDir() async { return await _channel.invokeMethod('iosGetDocumentDir', ''); } /// 找回密码1 Future forgotPassword(email) async { String data = await _flatInvoke("forgotPassword", email); return ForgotPasswordResult.fromJson(jsonDecode(data)); } /// 找回密码2 Future resetPassword( String email, int questionNo, String answer, ) async { String data = await _flatInvoke("resetPassword", { "email": email, "questionNo": questionNo, "answer": answer, }); return ResetPasswordResult.fromJson(jsonDecode(data)); } Future mergeHistoriesFromWebDav( String root, String username, String password, String file, String direction, ) { return _flatInvoke("mergeHistoriesFromWebDav", { "root": root, "username": username, "password": password, "file": file, "direction": direction, }); } Future mergeHistoriesFromLocal(String localPath) { return _flatInvoke("mergeHistoriesFromLocal", localPath); } Future ping(String idx) async { String ms = await _flatInvoke("ping", idx); return int.parse(ms); } Future pingImg(String idx) async { String ms = await _flatInvoke("pingImg", idx); return int.parse(ms); } Future androidStorageRoot() async { return await _channel.invokeMethod("androidStorageRoot"); } Future importComicViewFormOff(String dbPath) { return _flatInvoke("importComicViewFormOff", dbPath); } Future startWebServer() { return _flatInvoke("startWebServer", ""); } Future stopWebServer() { return _flatInvoke("stopWebServer", ""); } Future androidDefaultExportsDir() async { return await _channel.invokeMethod("androidDefaultExportsDir"); } Future getHomeDir() { return _flatInvoke("getHomeDir", ""); } Future mkdirs(String path) { return _flatInvoke("mkdirs", path); } Future androidMkdirs(String path) async { return await _channel.invokeMethod("androidMkdirs", path); } Future downloadAll(List comicIds) { return _flatInvoke("downloadAll", comicIds); } Future setPatAccessKey(String accessKey) { return _flatInvoke("setPatAccessKey", accessKey); } Future reloadPatAccount() { return _flatInvoke("reloadPatAccount", ""); } Future bindThisAccount() { return _flatInvoke("bindThisAccount", ""); } Future clearPat() { return _flatInvoke("clearPat", ""); } Future getProServerName() async { return await _flatInvoke("getProServerName", ""); } Future setProServerName(String serverName) { return _flatInvoke("setProServerName", serverName); } /// 加载已订阅 Future loadSubscribed(String comicId) async { String data = await _flatInvoke("loadSubscribed", comicId); if (data == "") { return null; } return ComicSubscribe.fromJson(jsonDecode(data)); } Future addSubscribed(String comicId) async { return _flatInvoke("addSubscribed", comicId); } Future removeAllSubscribed() async { return _flatInvoke("removeAllSubscribed", ""); } Future removeSubscribed(String comicId) async { return _flatInvoke("removeSubscribed", comicId); } Future> allSubscribed() async { var data = await _flatInvoke("allSubscribed", ""); List list = json.decode(data); return list.map((e) => ComicSubscribe.fromJson(e)).toList(); } Future updateSubscribed() async { return _flatInvoke("updateSubscribed", ""); } Future updateSubscribedForce() async { return _flatInvoke("updateSubscribedForce", ""); } Future> fontList() async { return await _channel.invokeMethod( "fontList", {}).then((value) => List.of(value).cast()); } // 本地收藏夹方法 Future createLocalFavoriteFolder(String name) async { String data = await _flatInvoke("createLocalFavoriteFolder", {"name": name}); return LocalFavoriteFolder.fromJson(jsonDecode(data)); } Future updateLocalFavoriteFolder(LocalFavoriteFolder folder) async { return _flatInvoke("updateLocalFavoriteFolder", folder.toJson()); } Future deleteLocalFavoriteFolder(String folderId) async { return _flatInvoke("deleteLocalFavoriteFolder", folderId); } Future getLocalFavoriteFolder(String folderId) async { String data = await _flatInvoke("getLocalFavoriteFolder", folderId); return LocalFavoriteFolder.fromJson(jsonDecode(data)); } Future> listLocalFavoriteFolders() async { String data = await _flatInvoke("listLocalFavoriteFolders", ""); List list = jsonDecode(data); return list.map((e) => LocalFavoriteFolder.fromJson(e)).toList(); } Future countLocalFavoriteFolders() async { String data = await _flatInvoke("countLocalFavoriteFolders", ""); return int.parse(data); } Future addLocalFavoriteComic(String comicId, String folderId, {String info = ""}) async { return _flatInvoke("addLocalFavoriteComic", { "comicId": comicId, "folderId": folderId, "info": info, }); } Future removeLocalFavoriteComic(String comicId) async { return _flatInvoke("removeLocalFavoriteComic", comicId); } Future moveLocalFavoriteComics(List comicIds, String folderId) async { return _flatInvoke("moveLocalFavoriteComics", { "comicIds": comicIds, "folderId": folderId, }); } Future getLocalFavoriteComic(String comicId) async { try { String data = await _flatInvoke("getLocalFavoriteComic", comicId); if (data == "") { return null; } return LocalFavoriteComic.fromJson(jsonDecode(data)); } catch (e) { return null; } } Future> listLocalFavoriteComics( String folderId) async { String data = await _flatInvoke("listLocalFavoriteComics", {"folderId": folderId}); List list = jsonDecode(data); return list.map((e) => LocalFavoriteComic.fromJson(e)).toList(); } Future> listAllLocalFavoriteComics() async { String data = await _flatInvoke("listAllLocalFavoriteComics", ""); List list = jsonDecode(data); return list.map((e) => LocalFavoriteComic.fromJson(e)).toList(); } Future mergeLocalFavoritesFromWebDav( String webdavRoot, String webdavUsername, String webdavPassword, ) async { return _flatInvoke("mergeLocalFavoritesFromWebDav", { "webdavRoot": webdavRoot, "webdavUsername": webdavUsername, "webdavPassword": webdavPassword, }); } } ================================================ FILE: lib/basic/Navigator.dart ================================================ /// 导航相关 import 'dart:async'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/config/IconLoading.dart'; // 用于监听返回到当前页面的事件 // (await Navigator.push 会在子页面pushReplacement时结束阻塞) final RouteObserver> routeObserver = RouteObserver>(); // 路径深度计数 const _depthMax = 15; var _depth = 0; var navigatorObserver = _NavigatorObserver(); class _NavigatorObserver extends NavigatorObserver { @override void didPop(Route route, Route? previousRoute) { _depth--; print("DEPTH : $_depth"); super.didPop(route, previousRoute); } @override void didPush(Route route, Route? previousRoute) { _depth++; print("DEPTH : $_depth"); super.didPush(route, previousRoute); } } // 路径达到一定深度的时候使用 pushReplacement Future navPushOrReplace( BuildContext context, WidgetBuilder builder) async { if (_depth < _depthMax) { return Navigator.push( context, mixRoute(builder: builder), ); } else { return Navigator.pushReplacement( context, mixRoute(builder: builder), ); } } ================================================ FILE: lib/basic/config/Address.dart ================================================ /// 分流地址 // addr = "172.67.7.24:443" // addr = "104.20.180.50:443" // addr = "172.67.208.169:443" import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/config/ImageAddress.dart'; import 'package:pikapika/basic/config/UseApiLoadImage.dart'; import '../Method.dart'; var _addresses = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", ]; late String _currentAddress; Future initAddress() async { _currentAddress = await method.getSwitchAddress(); } String currentAddress() => _currentAddress; String currentAddressName() => _currentAddress == "0" ? tr('net.no_address') : tr('net.address') + _currentAddress; Widget switchAddressSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr('net.address')), subtitle: Text(currentAddressName()), onTap: () async { await chooseAddressAndSwitch(context); setState(() {}); }, ); }, ); } Widget reloadSwitchAddressSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr('net.address_sync')), onTap: () async { String? choose = await chooseListDialog(context, tr('net.address_sync'), [ tr('net.address_sync_from_server'), tr('net.address_sync_reset'), ]); if (choose != null) { if (choose == tr('net.address_sync_from_server')) { try { await method.reloadSwitchAddress(); defaultToast(context, tr('net.address_sync_success')); } catch (e, s) { print("$e\n$s"); defaultToast(context, tr('net.address_sync_failed')); } } else if (choose == tr('net.address_sync_reset')) { try { await method.resetSwitchAddress(); defaultToast(context, tr('net.address_sync_reset_success')); } catch (e, s) { print("$e\n$s"); defaultToast(context, tr('net.address_sync_reset_failed')); } } } }, ); }, ); } Future chooseAddressAndSwitch(BuildContext context) async { String? choose = await showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: Text(tr('net.choose_address')), children: [ ..._addresses.map( (e) => SimpleDialogOption( child: ApiOptionRow( e == "0" ? tr('net.no_address') : tr('net.address') + e, e, key: Key("API:$e"), ), onPressed: () { Navigator.of(context).pop(e); }, ), ), SimpleDialogOption( child: Text(tr('net.address_sync')), onPressed: () { Navigator.of(context).pop(tr('net.address_sync')); }, ) ], ); }, ); if (choose != null) { if (tr('net.address_sync') == choose) { try { await method.reloadSwitchAddress(); defaultToast(context, tr('net.address_sync_success')); } catch (e, s) { print("$e\n$s"); defaultToast(context, tr('net.address_sync_failed')); } return; } await method.setSwitchAddress(choose); _currentAddress = choose; } } Widget addressPopMenu(BuildContext context) { return PopupMenuButton( icon: const Icon(Icons.webhook), itemBuilder: (BuildContext context) => >[ PopupMenuItem( value: 0, child: ListTile( leading: const Icon(Icons.share), title: Text("${tr('net.address')} (${currentAddressName()})"), ), ), PopupMenuItem( value: 1, child: ListTile( leading: const Icon(Icons.image_search), title: Text("${tr('net.image_address')} (${currentImageAddressName()})"), ), ), PopupMenuItem( value: 2, child: ListTile( leading: const Icon(Icons.network_ping), title: Text("${tr('net.use_api_load_image')} (${currentUseApiLoadImageName()})"), ), ), ], onSelected: (int value) { switch (value) { case 0: chooseAddressAndSwitch(context); break; case 1: chooseImageAddress(context); break; case 2: chooseUseApiLoadImage(context); break; } }, ); } class ApiOptionRow extends StatefulWidget { final String title; final String value; const ApiOptionRow(this.title, this.value, {Key? key}) : super(key: key); @override State createState() => _ApiOptionRowState(); } class _ApiOptionRowState extends State { late Future _feature; @override void initState() { super.initState(); _feature = method.ping(widget.value); } @override Widget build(BuildContext context) { return Row( children: [ Text(widget.title), Expanded(child: Container()), FutureBuilder( future: _feature, builder: ( BuildContext context, AsyncSnapshot snapshot, ) { if (snapshot.connectionState != ConnectionState.done) { return PingStatus( tr('net.ping_testing'), Colors.blue, ); } if (snapshot.hasError) { return PingStatus( tr('net.ping_failed'), Colors.red, ); } int ping = snapshot.requireData; if (ping <= 200) { return PingStatus( "${ping}ms", Colors.green, ); } if (ping <= 500) { return PingStatus( "${ping}ms", Colors.yellow, ); } return PingStatus( "${ping}ms", Colors.orange, ); }, ), ], ); } } class PingStatus extends StatelessWidget { final String title; final Color color; const PingStatus(this.title, this.color, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Row( children: [ Text( '\u2022', style: TextStyle( color: color, ), ), Text(" $title"), ], ); } } ================================================ FILE: lib/basic/config/AndroidDisplayMode.dart ================================================ /// 显示模式, 仅安卓有效 import 'dart:io'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/i18.dart'; import '../Common.dart'; import 'IsPro.dart'; const _propertyName = "androidDisplayMode"; List _modes = []; String _androidDisplayMode = ""; Future initAndroidDisplayMode() async { if (Platform.isAndroid) { _androidDisplayMode = await method.loadProperty(_propertyName, ""); _modes = await method.loadAndroidModes(); await _changeMode(); } } Future _changeMode() async { await method.setAndroidMode(_androidDisplayMode); } Future _chooseAndroidDisplayMode(BuildContext context) async { if (Platform.isAndroid) { List list = [""]; list.addAll(_modes); String? result = await chooseListDialog( context, tr('settings.android_display_mode.dialog_title'), list, ); if (result != null) { await method.saveProperty(_propertyName, result); _androidDisplayMode = result; await _changeMode(); } } } Widget androidDisplayModeSetting() { if (Platform.isAndroid) { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text( tr('settings.android_display_mode.title') + (!isPro ? "(${tr('app.pro')})" : ""), style: TextStyle( color: !isPro ? Colors.grey : null, ), ), subtitle: Text(_androidDisplayMode), onTap: () async { if (!isPro) { defaultToast(context, tr('app.pro_required')); return; } await _chooseAndroidDisplayMode(context); setState(() {}); }, ); }, ); } return Container(); } ================================================ FILE: lib/basic/config/AndroidSecureFlag.dart ================================================ /// 音量键翻页 import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; import 'IsPro.dart'; const _propertyName = "androidSecureFlag"; late bool _androidSecureFlag; Future initAndroidSecureFlag() async { if (Platform.isAndroid) { _androidSecureFlag = (await method.loadProperty(_propertyName, "false")) == "true"; if (_androidSecureFlag) { await method.androidSecureFlag(true); } } } Widget androidSecureFlagSetting() { if (Platform.isAndroid) { return StatefulBuilder(builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _androidSecureFlag, title: Text( tr("settings.android_secure_flag") + (!isPro ? "(${tr('settings.app.pro')})" : ""), style: TextStyle( color: !isPro ? Colors.grey : null, ), ), onChanged: (target) async { if (!isPro) { defaultToast(context, tr('app.pro_required')); return; } await method.saveProperty(_propertyName, "$target"); _androidSecureFlag = target; await method.androidSecureFlag(_androidSecureFlag); setState(() {}); }); }); } return Container(); } ================================================ FILE: lib/basic/config/AppOrientation.dart ================================================ // AppOrientation.dart import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Method.dart'; const _propertyName = "appOrientation"; late AppOrientation _appOrientation; enum AppOrientation { normal, landscape, portrait, } String appOrientationName(AppOrientation type) { switch (type) { case AppOrientation.normal: return tr('settings.app_orientation.normal'); case AppOrientation.landscape: return tr('settings.app_orientation.landscape'); case AppOrientation.portrait: return tr('settings.app_orientation.portrait'); } } Future initAppOrientation() async { _appOrientation = _fromString(await method.loadProperty( _propertyName, AppOrientation.normal.toString())); _set(); } AppOrientation _fromString(String valueForm) { for (var value in AppOrientation.values) { if (value.toString() == valueForm) { return value; } } return AppOrientation.values.first; } AppOrientation get currentAppOrientation => _appOrientation; Future chooseAppOrientation(BuildContext context) async { final Map map = {}; for (var element in AppOrientation.values) { map[appOrientationName(element)] = element; } final newAppOrientation = await chooseMapDialog( context, map, tr('settings.app_orientation.choose'), ); if (newAppOrientation != null) { await method.saveProperty(_propertyName, "$newAppOrientation"); _appOrientation = newAppOrientation; _set(); } } Widget appOrientationWidget() { if (!Platform.isAndroid && !Platform.isIOS) { return const SizedBox.shrink(); } return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr('settings.app_orientation.title')), subtitle: Text(appOrientationName(_appOrientation)), onTap: () async { await chooseAppOrientation(context); setState(() {}); }, ); }, ); } void _set() { if (Platform.isAndroid || Platform.isIOS) { switch (_appOrientation) { case AppOrientation.normal: SystemChrome.setPreferredOrientations([]); break; case AppOrientation.landscape: SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); break; case AppOrientation.portrait: SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, ]); break; } } } ================================================ FILE: lib/basic/config/Authentication.dart ================================================ import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/config/Platform.dart'; import 'package:pikapika/screens/DesktopAuthenticationScreen.dart'; import '../Common.dart'; import '../Method.dart'; const _propertyName = "authentication"; late bool _authentication; Future initAuthentication() async { if (Platform.isIOS || androidVersion >= 29) { _authentication = (await method.loadProperty(_propertyName, "false")) == "true"; } else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { _authentication = await needDesktopAuthentication(); } else { _authentication = false; } } bool currentAuthentication() { return _authentication; } Future verifyAuthentication(BuildContext context) async { if (Platform.isIOS || androidVersion >= 29) { return await method.verifyAuthentication(); } if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { return await Navigator.of(context).push( MaterialPageRoute(builder: (context) => const VerifyPassword())) == true; } return false; } Widget authenticationSetting() { if (Platform.isIOS || androidVersion >= 29) { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _authentication, title: Text(tr("settings.authentication")), onChanged: (target) async { await method.saveProperty(_propertyName, "$target"); _authentication = target; setState(() {}); }, ); }, ); } if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { return StatefulBuilder(builder: ( BuildContext context, void Function(void Function()) setState, ) { return ListTile( title: Text(tr("settings.set_password")), onTap: () async { await Navigator.of(context).push( MaterialPageRoute(builder: (context) => const SetPassword())); await initAuthentication(); }, ); }); } return Container(); } ================================================ FILE: lib/basic/config/AutoClean.dart ================================================ /// 自动清理 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Method.dart'; // const _autoCleanMap = { // "一个月前": "${3600 * 24 * 30}", // "一周前": "${3600 * 24 * 7}", // "一天前": "${3600 * 24 * 1}", // "不自动清理": "${0}", // }; final _autoCleanMap = {}; late String _autoCleanSec; Future initAutoClean() async { _autoCleanMap.putIfAbsent(tr("settings.auto_clean.one_month_ago"), () => "${3600 * 24 * 30}"); _autoCleanMap.putIfAbsent(tr("settings.auto_clean.one_week_ago"), () => "${3600 * 24 * 7}"); _autoCleanMap.putIfAbsent(tr("settings.auto_clean.one_day_ago"), () => "${3600 * 24 * 1}"); _autoCleanMap.putIfAbsent(tr("settings.auto_clean.no_auto_clean"), () => "${0}"); _autoCleanSec = await method.loadProperty("autoCleanSec", "${3600 * 24 * 30}"); if ("0" != _autoCleanSec) { await method.autoClean(_autoCleanSec); } } String _currentAutoCleanSec() { for (var value in _autoCleanMap.entries) { if (value.value == _autoCleanSec) { return value.key; } } return ""; } Future _chooseAutoCleanSec(BuildContext context) async { String? choose = await showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: Text(tr("settings.auto_clean.title")), children: [ ..._autoCleanMap.entries.map( (e) => SimpleDialogOption( child: Text(e.key), onPressed: () { Navigator.of(context).pop(e.value); }, ), ), ], ); }, ); if (choose != null) { await method.saveProperty("autoCleanSec", choose); _autoCleanSec = choose; } } Widget autoCleanSecSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.auto_clean.title")), subtitle: Text(_currentAutoCleanSec()), onTap: () async { await _chooseAutoCleanSec(context); setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/AutoDeleteDownloadOnUnfavorite.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/i18.dart'; import '../Common.dart'; import '../Method.dart'; import 'IsPro.dart'; const _propertyName = "autoDeleteDownloadOnUnfavorite"; late bool _autoDeleteDownloadOnUnfavorite; Future initAutoDeleteDownloadOnUnfavorite() async { _autoDeleteDownloadOnUnfavorite = (await method.loadProperty(_propertyName, "false")) == "true"; if (_autoDeleteDownloadOnUnfavorite && !isPro) { _autoDeleteDownloadOnUnfavorite = false; await method.saveProperty(_propertyName, "false"); } } bool autoDeleteDownloadOnUnfavorite() { return _autoDeleteDownloadOnUnfavorite; } Widget autoDeleteDownloadOnUnfavoriteSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _autoDeleteDownloadOnUnfavorite, title: Text( tr("settings.auto_delete_download_on_unfavorite") + (!isPro ? "(${tr("app.pro")})" : ""), style: TextStyle( color: !isPro ? Colors.grey : null, ), ), subtitle: !isPro ? Text(tr("app.pro_required")) : null, onChanged: (value) async { if (!isPro) { defaultToast(context, tr("app.pro_required")); return; } await method.saveProperty(_propertyName, "$value"); _autoDeleteDownloadOnUnfavorite = value; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/AutoDownloadOnFavorite.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/i18.dart'; import '../Common.dart'; import '../Method.dart'; import 'IsPro.dart'; const _propertyName = "autoDownloadOnFavorite"; late bool _autoDownloadOnFavorite; Future initAutoDownloadOnFavorite() async { _autoDownloadOnFavorite = (await method.loadProperty(_propertyName, "false")) == "true"; if (_autoDownloadOnFavorite && !isPro) { _autoDownloadOnFavorite = false; await method.saveProperty(_propertyName, "false"); } } bool autoDownloadOnFavorite() { return _autoDownloadOnFavorite; } Widget autoDownloadOnFavoriteSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _autoDownloadOnFavorite, title: Text( tr("settings.auto_download_on_favorite") + (!isPro ? "(${tr("app.pro")})" : ""), style: TextStyle( color: !isPro ? Colors.grey : null, ), ), subtitle: !isPro ? Text(tr("app.pro_required")) : null, onChanged: (value) async { if (!isPro) { defaultToast(context, tr("app.pro_required")); return; } await method.saveProperty(_propertyName, "$value"); _autoDownloadOnFavorite = value; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/AutoFullScreen.dart ================================================ /// 自动全屏 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; const _propertyName = "autoFullScreen"; late bool _autoFullScreen; Future initAutoFullScreen() async { _autoFullScreen = (await method.loadProperty(_propertyName, "false")) == "true"; } bool currentAutoFullScreen() { return _autoFullScreen; } Widget autoFullScreenSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _autoFullScreen, title: Text(tr("settings.auto_full_screen.title")), onChanged: (a) async { await method.saveProperty(_propertyName, "$a"); _autoFullScreen = a; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/AutoFullScreenOnForward.dart ================================================ /// 前进时自动全屏 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; const _propertyName = "autoFullScreenOnForward"; late bool _autoFullScreenOnForward; Future initAutoFullScreenOnForward() async { _autoFullScreenOnForward = (await method.loadProperty(_propertyName, "false")) == "true"; } bool currentAutoFullScreenOnForward() { return _autoFullScreenOnForward; } Future setAutoFullScreenOnForward(bool value) async { _autoFullScreenOnForward = value; await method.saveProperty(_propertyName, "$value"); } Widget autoFullScreenOnForwardSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _autoFullScreenOnForward, title: Text(tr("settings.auto_full_screen_on_forward.title")), onChanged: (a) async { await method.saveProperty(_propertyName, "$a"); _autoFullScreenOnForward = a; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/CategoriesColumnCount.dart ================================================ /// 多线程下载并发数 import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Method.dart'; String _propertyName = "categoriesColumnCount"; late int categoriesColumnCount; Event categoriesColumnCountEvent = Event(); Future initCategoriesColumnCount() async { categoriesColumnCount = int.parse(await method.loadProperty(_propertyName, "0")); } Widget categoriesColumnCountSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text( tr('settings.categories_column_count.title'), ), subtitle: Text(categoriesColumnCount == 0 ? tr('settings.categories_column_count.auto') : "$categoriesColumnCount"), onTap: () async { int? value = await chooseMapDialog( context, { tr('settings.categories_column_count.auto'): 0, "2": 2, "3": 3, "4": 4, "5": 5, }, tr('settings.categories_column_count.choose')); if (value != null) { await method.saveProperty(_propertyName, "$value"); categoriesColumnCount = value; setState(() {}); categoriesColumnCountEvent.broadcast(); } }, ); }, ); } ================================================ FILE: lib/basic/config/CategoriesSort.dart ================================================ import 'dart:convert'; import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/screens/CategoriesSortScreen.dart'; import '../Method.dart'; const _propertyName = "categoriesSort"; List _categoriesSort = []; Future initCategoriesSort() async { var json = await method.loadProperty(_propertyName, "[]"); _categoriesSort = List.from(jsonDecode(json)); } Future saveCategoriesSort(List categoriesSort) async { _categoriesSort = categoriesSort; await method.saveProperty(_propertyName, jsonEncode(categoriesSort)); categoriesSortEvent.broadcast(); } List getCategoriesSort() { return _categoriesSort; } var categoriesSortEvent = Event(); Widget categoriesSortSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( onTap: () { Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) { return const CategoriesSortScreen(); }, )); }, title: Text( tr('settings.categories_sort.title'), ), ); }, ); } ================================================ FILE: lib/basic/config/ChooserRoot.dart ================================================ /// 文件夹选择器的根路径 import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; import '../Common.dart'; import '../Method.dart'; import 'Platform.dart'; const _propertyName = "chooserRoot"; late String _chooserRoot; Future initChooserRoot() async { _chooserRoot = await method.loadProperty(_propertyName, ""); if (_chooserRoot.isEmpty) { if (Platform.isAndroid) { try { _chooserRoot = await method.androidStorageRoot(); } catch (e) { _chooserRoot = "/sdcard"; } } else if (Platform.isMacOS || Platform.isLinux) { _chooserRoot = await method.getHomeDir(); } else if (Platform.isWindows) { _chooserRoot = "/"; } } } Future currentChooserRoot() async { if (Platform.isAndroid) { late bool g; if (androidVersion < 30) { g = await Permission.storage.request().isGranted; } else { g = await Permission.manageExternalStorage.request().isGranted; } if (!g) { throw Exception(tr('app.permission_denied')); } } return _chooserRoot; } Future _inputChooserRoot(BuildContext context) async { String? input = await displayTextInputDialog( context, src: _chooserRoot, title: tr('settings.chooser_root.title'), hint: tr('settings.chooser_root.hint'), desc: tr('settings.chooser_root.desc'), ); if (input != null) { await method.saveProperty(_propertyName, input); _chooserRoot = input; } } Widget chooserRootSetting() { if (Platform.isIOS) { return Container(); } return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr('settings.chooser_root.title')), subtitle: Text(_chooserRoot), onTap: () async { await _inputChooserRoot(context); setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/ContentFailedReloadAction.dart ================================================ /// 全屏操作 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; enum ContentFailedReloadAction { PULL_DOWN, TOUCH_LOADER, } const _propertyName = "contentFailedReloadAction"; late ContentFailedReloadAction contentFailedReloadAction; Map _contentFailedReloadActionMap = {}; Future initContentFailedReloadAction() async { _contentFailedReloadActionMap = { tr("settings.content_failed_reload_action.pull_down"): ContentFailedReloadAction.PULL_DOWN, tr("settings.content_failed_reload_action.touch_loader"): ContentFailedReloadAction.TOUCH_LOADER, }; contentFailedReloadAction = _contentFailedReloadActionFromString(await method.loadProperty( _propertyName, ContentFailedReloadAction.PULL_DOWN.toString(), )); } ContentFailedReloadAction _contentFailedReloadActionFromString(String string) { for (var value in ContentFailedReloadAction.values) { if (string == value.toString()) { return value; } } return ContentFailedReloadAction.PULL_DOWN; } String _currentContentFailedReloadActionName() { for (var e in _contentFailedReloadActionMap.entries) { if (e.value == contentFailedReloadAction) { return e.key; } } return ''; } Future _chooseContentFailedReloadAction(BuildContext context) async { ContentFailedReloadAction? result = await chooseMapDialog( context, _contentFailedReloadActionMap, tr("settings.content_failed_reload_action.choose")); if (result != null) { await method.saveProperty(_propertyName, result.toString()); contentFailedReloadAction = result; } } Widget contentFailedReloadActionSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.content_failed_reload_action.title")), subtitle: Text(_currentContentFailedReloadActionName()), onTap: () async { await _chooseContentFailedReloadAction(context); setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/CopyFullName.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _propertyName = "copyFullName"; late bool _copyFullName; Future initCopyFullName() async { _copyFullName = (await method.loadProperty(_propertyName, "false")) == "true"; } bool copyFullName() { return _copyFullName; } Widget copyFullNameSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("settings.copy_full_name.title")), value: _copyFullName, onChanged: (value) async { await method.saveProperty(_propertyName, "$value"); _copyFullName = value; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/CopyFullNameTemplate.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; const _propertyName = "copyFullNameTemplate"; late String _copyFullNameTemplate; Future initCopyFullNameTemplate() async { _copyFullNameTemplate = await method.loadProperty(_propertyName, "[{author}] {title}"); } String copyFullNameTemplate() { return _copyFullNameTemplate; } Widget copyFullNameTemplateSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.copy_full_name_template.title")), subtitle: Text(_copyFullNameTemplate), onTap: () async { var result = await displayTextInputDialog( context, title: tr("settings.copy_full_name_template.title"), hint: tr("settings.copy_full_name_template.hint"), src: _copyFullNameTemplate, ); if (result == null) { return; } await method.saveProperty(_propertyName, result); _copyFullNameTemplate = result; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/CopySkipConfirm.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _propertyName = "copySkipConfirm"; late bool _copySkipConfirm; Future initCopySkipConfirm() async { _copySkipConfirm = (await method.loadProperty(_propertyName, "false")) == "true"; } bool copySkipConfirm() { return _copySkipConfirm; } Widget copySkipConfirmSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("settings.copy_skip_confirm.title")), value: _copySkipConfirm, onChanged: (value) async { await method.saveProperty(_propertyName, "$value"); _copySkipConfirm = value; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/DisableAutoDownloadOnMobile.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/i18.dart'; import '../Common.dart'; import '../Method.dart'; import 'IsPro.dart'; const _propertyName = "disableAutoDownloadOnMobile"; late bool _disableAutoDownloadOnMobile; Future initDisableAutoDownloadOnMobile() async { _disableAutoDownloadOnMobile = (await method.loadProperty(_propertyName, "false")) == "true"; if (_disableAutoDownloadOnMobile && !isPro) { _disableAutoDownloadOnMobile = false; await method.saveProperty(_propertyName, "false"); } } bool disableAutoDownloadOnMobile() { return _disableAutoDownloadOnMobile; } Widget disableAutoDownloadOnMobileSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _disableAutoDownloadOnMobile, title: Text( tr("settings.disable_auto_download_on_mobile") + (!isPro ? "(${tr("app.pro")})" : ""), style: TextStyle( color: !isPro ? Colors.grey : null, ), ), subtitle: !isPro ? Text(tr("app.pro_required")) : null, onChanged: (value) async { if (!isPro) { defaultToast(context, tr("app.pro_required")); return; } await method.saveProperty(_propertyName, "$value"); _disableAutoDownloadOnMobile = value; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/DownloadAndExportPath.dart ================================================ /// 下载的同时导出到文件系统 import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Cross.dart'; import '../Method.dart'; late String _downloadAndExportPath; Future initDownloadAndExportPath() async { if (Platform.isWindows || Platform.isMacOS || Platform.isAndroid || Platform.isLinux) { _downloadAndExportPath = await method.loadDownloadAndExportPath(); } } Widget downloadAndExportPathSetting() { if (Platform.isWindows || Platform.isMacOS || Platform.isAndroid || Platform.isLinux) { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.download_and_export_path.title")), subtitle: Text(_downloadAndExportPath), onTap: () async { if (_downloadAndExportPath == "") { bool b = await confirmDialog( context, tr("settings.download_and_export_path.confirm"), tr("settings.download_and_export_path.desc"), ); if (b) { late String? folder; try { folder = await chooseFolder(context); } catch (e) { defaultToast(context, "$e"); return; } if (folder != null) { await method.saveDownloadAndExportPath(folder); _downloadAndExportPath = folder; setState(() {}); } } } else { bool b = await confirmDialog( context, tr("settings.download_and_export_path.confirm"), tr("settings.download_and_export_path.desc"), ); if (b) { var folder = ""; await method.saveDownloadAndExportPath(folder); _downloadAndExportPath = folder; setState(() {}); } } }, ); }, ); } return Container(); } ================================================ FILE: lib/basic/config/DownloadCachePath.dart ================================================ /// 下载的同时导出到文件系统 import 'dart:convert'; import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:file_picker/file_picker.dart'; import 'package:filesystem_picker/filesystem_picker.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Cross.dart'; import '../../screens/ImportFromOffScreen.dart'; import '../Method.dart'; import 'ChooserRoot.dart'; import 'IconLoading.dart'; late String _downloadCachePath; Future initDownloadCachePath() async { if (Platform.isWindows || Platform.isMacOS || Platform.isAndroid || Platform.isLinux) { _downloadCachePath = await method.loadDownloadCachePath(); } } Widget downloadCachePathSetting() { if (Platform.isWindows || Platform.isMacOS || Platform.isAndroid || Platform.isLinux) { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.download_cache_path.title")), subtitle: Text(_downloadCachePath), onTap: () async { if (_downloadCachePath == "") { bool b = await confirmDialog( context, tr("settings.download_cache_path.confirm"), tr("settings.download_cache_path.desc") + "\n\n${String.fromCharCodes(base64Decode("L0FuZHJvaWQvZGF0YS9jb20ucGljYWNvbWljLmZyZWdhdGEvZmlsZXMv"))}", ); if (b) { late String? folder; try { folder = await chooseFolder(context); } catch (e) { defaultToast(context, "$e"); return; } if (folder != null) { await method.saveDownloadCachePath(folder); _downloadCachePath = folder; setState(() {}); } } } else { bool b = await confirmDialog( context, tr("settings.download_cache_path.confirm"), tr("settings.download_cache_path.cancel_desc"), ); if (b) { var folder = ""; await method.saveDownloadCachePath(folder); _downloadCachePath = folder; setState(() {}); } } }, ); }, ); } return Container(); } Widget importViewLogFromOff() { if (Platform.isWindows || Platform.isMacOS || Platform.isAndroid || Platform.isLinux) { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.download_cache_path.import_view_log_from_off.title")), subtitle: Text(_downloadCachePath), onTap: () async { bool b = await confirmDialog( context, tr("settings.download_cache_path.import_view_log_from_off.title"), tr('settings.download_cache_path.import_view_log_from_off.desc')+ "\n\n${String.fromCharCodes(base64Decode("L2RhdGEvZGF0YS9jb20ucGljYWNvbWljLmdyZWdhdGEvZGF0YWJhc2VzL2NvbV9waWNhY29taWNfZnJlZ2F0YS5kYg=="))}", ); if (b) { late String chooseRoot; try { chooseRoot = await currentChooserRoot(); } catch (e) { defaultToast(context, "$e"); return; } String? path; if (Platform.isAndroid) { path = await FilesystemPicker.open( title: 'Open file', context: context, rootDirectory: Directory(chooseRoot), fsType: FilesystemType.file, folderIconColor: Colors.teal, allowedExtensions: ['.db'], fileTileSelectMode: FileTileSelectMode.wholeTile, ); } else { var ls = await FilePicker.platform.pickFiles( dialogTitle: tr("settings.download_cache_path.import_view_log_from_off.choose_file_dialog_title"), allowMultiple: false, initialDirectory: chooseRoot, type: FileType.custom, allowedExtensions: ['db'], allowCompression: false, ); path = ls != null && ls.count > 0 ? ls.paths[0] : null; } if (path != null) { if (path.endsWith(".db")) { Navigator.of(context).push( mixRoute( builder: (BuildContext context) => ImportFromOffScreen(dbPath: path!), ), ); } } } }, ); }, ); } return Container(); } ================================================ FILE: lib/basic/config/DownloadThreadCount.dart ================================================ /// 多线程下载并发数 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Method.dart'; import 'IsPro.dart'; late int _downloadThreadCount; const _values = [1, 2, 3, 4, 5]; Future initDownloadThreadCount() async { _downloadThreadCount = await method.loadDownloadThreadCount(); } Widget downloadThreadCountSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text( tr("settings.download_thread_count.title") + (!isPro ? "(${tr("app.pro")})" : ""), style: TextStyle( color: !isPro ? Colors.grey : null, ), ), subtitle: Text("$_downloadThreadCount"), onTap: () async { if (!isPro) { defaultToast(context, tr("app.pro_required")); return; } int? value = await chooseListDialog(context, tr("settings.download_thread_count.choose"), _values); if (value != null) { await method.saveDownloadThreadCount(value); _downloadThreadCount = value; setState(() {}); } }, ); }, ); } ================================================ FILE: lib/basic/config/DragRegionLock.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; const _propertyName = "dragRegionLock"; late bool _dragRegionLock; Future initDragRegionLock() async { _dragRegionLock = (await method.loadProperty(_propertyName, "true")) == "true"; } bool dragRegionLock() { return _dragRegionLock; } Future setDragRegionLock(bool value) async { await method.saveProperty(_propertyName, "$value"); _dragRegionLock = value; } Widget dragRegionLockSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _dragRegionLock, title: Text(tr("settings.drag_region_lock.title")), onChanged: (target) async { await method.saveProperty(_propertyName, "$target"); _dragRegionLock = target; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/EBookScrolling.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _propertyName = "eBookScrolling"; late bool _eBookScrolling; bool get eBookScrolling => _eBookScrolling; Future initEBookScrolling() async { _eBookScrolling = (await method.loadProperty(_propertyName, "false")) == "true"; } Widget eBookScrollingSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("settings.ebook_scrolling.title")), value: _eBookScrolling, onChanged: (value) async { await method.saveProperty(_propertyName, "$value"); _eBookScrolling = value; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/EBookScrollingRange.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _propertyName = "eBookScrollingRange"; late int _eBookScrollingRange; Future initEBookScrollingRange() async { _eBookScrollingRange = int.parse((await method.loadProperty(_propertyName, "80"))); } double get eBookScrollingRange => _eBookScrollingRange / 100; Widget eBookScrollingRangeSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.ebook_scrolling_range.title") + " - " + tr("settings.ebook_scrolling_range.desc") + " : $_eBookScrollingRange%${tr("settings.ebook_scrolling_range.screen_height")}"), subtitle: Slider( min: 30.toDouble(), max: 80.toDouble(), value: _eBookScrollingRange.toDouble(), onChanged: (double value) async { final va = value.toInt(); await method.loadProperty(_propertyName, "$va"); setState(() { _eBookScrollingRange = va; }); }, divisions: (80 - 30), ), ); }, ); } ================================================ FILE: lib/basic/config/EBookScrollingTrigger.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _propertyName = "eBookScrollingTrigger"; late double _eBookScrollingTrigger; Future initEBookScrollingTrigger() async { _eBookScrollingTrigger = double.parse((await method.loadProperty(_propertyName, "0.3"))); } double get eBookScrollingTrigger => _eBookScrollingTrigger; Widget eBookScrollingTriggerSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.ebook_scrolling_trigger.title") + " - " + tr("settings.ebook_scrolling_trigger.desc") + " : $_eBookScrollingTrigger ${tr("settings.ebook_scrolling_trigger.cm")}"), subtitle: Slider( min: 0.1.toDouble(), max: 2.0.toDouble(), value: _eBookScrollingTrigger.toDouble(), onChanged: (double value) async { await method.saveProperty(_propertyName, "$value"); setState((){ _eBookScrollingTrigger = value; }); }, divisions: (20 - 1), ), ); }, ); } ================================================ FILE: lib/basic/config/ExportPath.dart ================================================ /// 文件夹选择器的根路径 import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; import '../Cross.dart'; import '../Method.dart'; import 'Platform.dart'; const _propertyName = "exportPath"; late String _exportPath; Future initExportPath() async { _exportPath = await method.loadProperty(_propertyName, ""); if (_exportPath.isEmpty) { if (Platform.isAndroid) { try { _exportPath = await method.androidDefaultExportsDir(); } catch (e) { _exportPath = "/sdcard/Download/pikapika/exports"; } } else if (Platform.isMacOS || Platform.isLinux) { _exportPath = await method.getHomeDir(); if (Platform.isMacOS) { _exportPath = _exportPath + "/Downloads"; } } else if (Platform.isWindows) { _exportPath = "exports"; } } } Future attachExportPath() async { late String path; if (Platform.isIOS) { path = await method.iosGetDocumentDir(); } else { if (Platform.isAndroid) { late bool g; if (androidVersion < 30) { g = await Permission.storage.request().isGranted; }else{ g = await Permission.manageExternalStorage.request().isGranted; } if (!g) { throw Exception("申请权限被拒绝"); } } path = _exportPath; } if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { await method.mkdirs(path); } else if (Platform.isAndroid) { await method.androidMkdirs(path); } return path; } String showExportPath() { if (Platform.isIOS) { return "\n\n"+tr("settings.export_path.ios_desc") ; } return "\n\n$_exportPath"; } Future _setExportPath(String folder) async { await method.saveProperty(_propertyName, folder); _exportPath = folder; } Widget displayExportPathInfo() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { if (Platform.isIOS) { return Container( margin: const EdgeInsets.all(15), padding: const EdgeInsets.all(15), color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.01), child: Text(tr("settings.export_path.ios_desc2")), ); } return Column(children: [ MaterialButton( onPressed: () async { String? choose = await chooseFolder(context); if (choose != null) { _setExportPath(choose); } setState(() {}); }, child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Container( width: constraints.maxWidth, padding: const EdgeInsets.only(top: 15, bottom: 15), color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.05), child: Text( tr("settings.export_path.export_path_desc") + ":\n" "$_exportPath", textAlign: TextAlign.center, ), ); }, ), ), ...Platform.isAndroid ? [ Container(height: 15), Container( margin: const EdgeInsets.all(15), padding: const EdgeInsets.all(15), color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.01), child: Text( tr("settings.export_path.android_desc"), ), ), ] : [], ]); }, ); } ================================================ FILE: lib/basic/config/ExportRename.dart ================================================ /// 自动全屏 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; import 'IsPro.dart'; const _propertyName = "exportRename"; late bool _exportRename; Future initExportRename() async { _exportRename = (await method.loadProperty(_propertyName, "false")) == "true"; } bool currentExportRename() { return _exportRename; } Widget exportRenameSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("settings.export_rename.title")), value: _exportRename, onChanged: (value) async { if (!isPro) { defaultToast(context, tr("app.pro_required")); return; } await method.saveProperty(_propertyName, "$value"); _exportRename = value; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/FullScreenAction.dart ================================================ /// 全屏操作 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; enum FullScreenAction { TOUCH_ONCE, CONTROLLER, TOUCH_DOUBLE, TOUCH_DOUBLE_ONCE_NEXT, THREE_AREA, } // Map _fullScreenActionMap = { // "点击屏幕一次全屏": FullScreenAction.TOUCH_ONCE, // "使用控制器全屏": FullScreenAction.CONTROLLER, // "双击屏幕全屏": FullScreenAction.TOUCH_DOUBLE, // "双击屏幕全屏 + 单击屏幕下一页": FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT, // "将屏幕划分成三个区域 (上一页, 下一页, 全屏)": FullScreenAction.THREE_AREA, // }; Map _fullScreenActionMap = {}; const _defaultController = FullScreenAction.TOUCH_ONCE; const _propertyName = "fullScreenAction"; late FullScreenAction _fullScreenAction; Future initFullScreenAction() async { _fullScreenActionMap.addAll({ tr("settings.full_screen_action.touch_once"): FullScreenAction.TOUCH_ONCE, tr("settings.full_screen_action.controller"): FullScreenAction.CONTROLLER, tr("settings.full_screen_action.touch_double"): FullScreenAction.TOUCH_DOUBLE, tr("settings.full_screen_action.touch_double_once_next"): FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT, tr("settings.full_screen_action.three_area"): FullScreenAction.THREE_AREA, }); _fullScreenAction = _fullScreenActionFromString(await method.loadProperty( _propertyName, FullScreenAction.TOUCH_ONCE.toString(), )); } FullScreenAction currentFullScreenAction() { return _fullScreenAction; } FullScreenAction _fullScreenActionFromString(String string) { for (var value in FullScreenAction.values) { if (string == value.toString()) { return value; } } return _defaultController; } String currentFullScreenActionName() { for (var e in _fullScreenActionMap.entries) { if (e.value == _fullScreenAction) { return e.key; } } return ''; } Future chooseFullScreenAction(BuildContext context) async { FullScreenAction? result = await chooseMapDialog( context, _fullScreenActionMap, tr("settings.full_screen_action.choose")); if (result != null) { await method.saveProperty(_propertyName, result.toString()); _fullScreenAction = result; } } Widget fullScreenActionSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.full_screen_action.title")), subtitle: Text(currentFullScreenActionName()), onTap: () async { await chooseFullScreenAction(context); setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/FullScreenUI.dart ================================================ /// 全屏操作 import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../Common.dart'; import '../Method.dart'; enum FullScreenUI { NO, HIDDEN_BOTTOM, ALL, } // Map fullScreenUIMap = { // "不使用": FullScreenUI.NO, // "去除虚拟控制器": FullScreenUI.HIDDEN_BOTTOM, // "全屏": FullScreenUI.ALL, // }; final Map fullScreenUIMap = {}; const _propertyName = "fullScreenUI"; late FullScreenUI fullScreenUI; Future initFullScreenUI() async { fullScreenUIMap.addAll({ tr("settings.full_screen_ui.no"): FullScreenUI.NO, tr("settings.full_screen_ui.hidden_bottom"): FullScreenUI.HIDDEN_BOTTOM, tr("settings.full_screen_ui.all"): FullScreenUI.ALL, }); fullScreenUI = _fullScreenUIFromString(await method.loadProperty( _propertyName, FullScreenUI.NO.toString(), )); SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( systemStatusBarContrastEnforced: true, systemNavigationBarContrastEnforced: true, )); SystemChrome.setEnabledSystemUIMode( SystemUiMode.edgeToEdge, overlays: SystemUiOverlay.values, ); switchFullScreenUI(); } FullScreenUI _fullScreenUIFromString(String string) { for (var value in FullScreenUI.values) { if (string == value.toString()) { return value; } } return FullScreenUI.NO; } String currentFullScreenUIName() { for (var e in fullScreenUIMap.entries) { if (e.value == fullScreenUI) { return e.key; } } return ''; } Future chooseFullScreenUI(BuildContext context) async { FullScreenUI? result = await chooseMapDialog(context, fullScreenUIMap, tr("settings.full_screen_ui.choose")); if (result != null) { await method.saveProperty(_propertyName, result.toString()); fullScreenUI = result; switchFullScreenUI(); } } void switchFullScreenUI() { List list = [...SystemUiOverlay.values]; switch (fullScreenUI) { case FullScreenUI.HIDDEN_BOTTOM: SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: [SystemUiOverlay.top], ); break; case FullScreenUI.ALL: SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: [], ); break; case FullScreenUI.NO: SystemChrome.setEnabledSystemUIMode( SystemUiMode.edgeToEdge, overlays: SystemUiOverlay.values, ); break; } if (Platform.isAndroid || Platform.isIOS) { SystemChrome.setEnabledSystemUIMode( SystemUiMode.edgeToEdge, overlays: list, ); } } Widget fullScreenUISetting() { if (Platform.isAndroid || Platform.isIOS) { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.full_screen_ui.title")), subtitle: Text(currentFullScreenUIName()), onTap: () async { await chooseFullScreenUI(context); setState(() {}); }, ); }, ); } return Container(); } ================================================ FILE: lib/basic/config/GalleryPreloadCount.dart ================================================ /// 相册模式下预加载图片数量 const galleryPrePreloadCount = 1; const galleryPreloadCount = 2; ================================================ FILE: lib/basic/config/GestureSpeed.dart ================================================ import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/i18.dart'; const _propertyName = "gestureSpeed"; late double _gestureSpeed; Future initGestureSpeed() async { _gestureSpeed = double.parse(await method.loadProperty(_propertyName, "1.0")); } double currentGestureSpeed() { return _gestureSpeed; } Future setGestureSpeed(double value) async { _gestureSpeed = value; await method.saveProperty(_propertyName, "$value"); } Widget gestureSpeedSetting() { return StatefulBuilder(builder: (BuildContext context, StateSetter setState) { return ListTile( title: Text(tr("settings.gesture_speed.title")), subtitle: Text("${currentGestureSpeed().toStringAsFixed(1)}x"), onTap: () async { double value = currentGestureSpeed(); await showDialog( context: context, builder: (context) { return StatefulBuilder( builder: (context, setState) { return AlertDialog( title: Text(tr("settings.gesture_speed.title")), content: SizedBox( height: 100, child: Column( children: [ Text("${value.toStringAsFixed(1)}x"), Slider( min: 0.1, max: 5.0, divisions: 49, value: value, onChanged: (v) { setState(() { value = v; }); }, ), ], ), ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text(tr("app.cancel")), ), TextButton( onPressed: () async { await setGestureSpeed(value); Navigator.of(context).pop(); }, child: Text(tr("app.confirm")), ), ], ); }, ); }, ); setState(() {}); }, ); }); } ================================================ FILE: lib/basic/config/HiddenFdIcon.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _propertyName = "hiddenFdIcon"; late bool _hiddenFdIcon; bool get hiddenFdIcon => _hiddenFdIcon; var hiddenFdIconEvent = Event(); Future initHiddenFdIcon() async { _hiddenFdIcon = (await method.loadProperty(_propertyName, "false")) == "true"; } Widget hiddenFdIconSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("settings.hidden_fd_icon.title")), value: _hiddenFdIcon, onChanged: (value) async { await method.saveProperty(_propertyName, "$value"); _hiddenFdIcon = value; setState(() {}); hiddenFdIconEvent.broadcast(); }, ); }, ); } ================================================ FILE: lib/basic/config/HiddenSearchPersion.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _propertyName = "hiddenSearchPersion"; late bool _hiddenSearchPersion; bool get hiddenSearchPersion => _hiddenSearchPersion; var hiddenSearchPersionEvent = Event(); Future initHiddenSearchPersion() async { _hiddenSearchPersion = (await method.loadProperty(_propertyName, "false")) == "true"; } Widget hiddenSearchPersionSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("settings.hidden_search_persion.title")), value: _hiddenSearchPersion, onChanged: (value) async { await method.saveProperty(_propertyName, "$value"); _hiddenSearchPersion = value; setState(() {}); hiddenSearchPersionEvent.broadcast(); await method.removeAllSubscribed(); }, ); }, ); } ================================================ FILE: lib/basic/config/HiddenSubIcon.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _propertyName = "hiddenSubIcon"; late bool _hiddenSubIcon; bool get hiddenSubIcon => _hiddenSubIcon; var hiddenSubIconEvent = Event(); Future initHiddenSubIcon() async { _hiddenSubIcon = (await method.loadProperty(_propertyName, "false")) == "true"; } Widget hiddenSubIconSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("settings.hidden_sub_icon.title")), value: _hiddenSubIcon, onChanged: (value) async { await method.saveProperty(_propertyName, "$value"); _hiddenSubIcon = value; setState(() {}); hiddenSubIconEvent.broadcast(); await method.removeAllSubscribed(); }, ); }, ); } ================================================ FILE: lib/basic/config/HiddenViewed.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _propertyName = "hiddenViewed"; late bool _hiddenViewed; bool get hiddenViewed => _hiddenViewed; var hiddenViewedEvent = Event(); Future initHiddenViewed() async { _hiddenViewed = (await method.loadProperty(_propertyName, "false")) == "true"; } Widget hiddenViewedSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("settings.hidden_viewed.title")), value: _hiddenViewed, onChanged: (value) async { await method.saveProperty(_propertyName, "$value"); _hiddenViewed = value; setState(() {}); hiddenViewedEvent.broadcast(); await method.removeAllSubscribed(); }, ); }, ); } ================================================ FILE: lib/basic/config/HiddenWords.dart ================================================ import 'dart:convert'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../../screens/HiddenWordsScreen.dart'; import '../Method.dart'; const _key = "hiddenWords"; final List _hiddenWords = []; List get hiddenWords => _hiddenWords; Future initHiddenWords() async { final words = await method.loadProperty(_key, "[]"); _hiddenWords.clear(); _hiddenWords.addAll((jsonDecode(words) as List).cast()); return words; } Future saveHiddenWords(List words) async { _hiddenWords.clear(); _hiddenWords.addAll(words); await method.saveProperty(_key, jsonEncode(words)); } Future addHiddenWord(String word) async { if (word.trim().isEmpty) return; if (!_hiddenWords.contains(word)) { _hiddenWords.add(word); await method.saveProperty(_key, jsonEncode(_hiddenWords)); } } Future removeHiddenWord(String word) async { _hiddenWords.remove(word); await method.saveProperty(_key, jsonEncode(_hiddenWords)); } Future clearHiddenWords() async { _hiddenWords.clear(); await method.saveProperty(_key, "[]"); } Widget hiddenWordsSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.hidden_words.title")), subtitle: Text(subString(jsonEncode(_hiddenWords))), onTap: () async { await Navigator.of(context).push(MaterialPageRoute( builder: (context) => const HiddenWordsScreen(), )); setState(() {}); }, ); }, ); } String subString(String str) { if (str.length > 20) { return str.substring(0, 20) + "..."; } return str; } ================================================ FILE: lib/basic/config/HideOnlineFavorite.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _propertyName = "hideOnlineFavorite"; late bool _hideOnlineFavorite; bool get hideOnlineFavorite => _hideOnlineFavorite; var hideOnlineFavoriteEvent = Event(); Future initHideOnlineFavorite() async { _hideOnlineFavorite = (await method.loadProperty(_propertyName, "false")) == "true"; } Widget hideOnlineFavoriteSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("settings.hide_online_favorite.title")), subtitle: Text(tr("settings.hide_online_favorite.desc")), value: _hideOnlineFavorite, onChanged: (value) async { await method.saveProperty(_propertyName, "$value"); _hideOnlineFavorite = value; setState(() {}); hideOnlineFavoriteEvent.broadcast(); }, ); }, ); } ================================================ FILE: lib/basic/config/IconLoading.dart ================================================ /// 自动全屏 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; const _propertyName = "iconLoading"; // 启动图始终显示 late bool _iconLoading = false; Future initIconLoading() async { _iconLoading = (await method.loadProperty(_propertyName, "false")) == "true"; } bool currentIconLoading() { return _iconLoading; } Widget iconLoadingSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("settings.icon_loading.title")), value: _iconLoading, onChanged: (value) async { await method.saveProperty(_propertyName, "$value"); _iconLoading = value; setState(() {}); }, ); }, ); } Route mixRoute({required WidgetBuilder builder}) { if (currentIconLoading()) { return PageRouteBuilder( pageBuilder: (context, animation1, animation2) => builder.call(context), transitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero, ); } return MaterialPageRoute(builder: builder); } ================================================ FILE: lib/basic/config/IgnoreInfoHistory.dart ================================================ /// 自动全屏 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; const _propertyName = "ignoreInfoHistory"; late bool _ignoreInfoHistory; Future initIgnoreInfoHistory() async { _ignoreInfoHistory = (await method.loadProperty(_propertyName, "false")) == "true"; } bool currentIgnoreInfoHistory() { return _ignoreInfoHistory; } Widget ignoreInfoHistorySetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _ignoreInfoHistory, title: Text(tr("settings.ignore_info_history.title")), onChanged: (a) async { await method.saveProperty(_propertyName, "$a"); _ignoreInfoHistory = a; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/IgnoreUpgradeConfirm.dart ================================================ /// 音量键翻页 import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; import 'IsPro.dart'; const _propertyName = "ignoreUpgradeConfirm"; late bool _ignoreUpgradeConfirm; bool get ignoreUpgradeConfirm => _ignoreUpgradeConfirm; Future initIgnoreUpgradeConfirm() async { _ignoreUpgradeConfirm = (await method.loadProperty(_propertyName, "false")) == "true"; if (_ignoreUpgradeConfirm && !isPro) { _ignoreUpgradeConfirm = false; await method.saveProperty(_propertyName, "false"); } } Widget ignoreUpgradeConfirmSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _ignoreUpgradeConfirm, title: Text( tr("settings.ignore_upgrade_confirm.title") + (!isPro ? "(${tr("app.pro")})" : ""), style: TextStyle( color: !isPro ? Colors.grey : null, ), ), onChanged: (target) async { if (!isPro) { defaultToast(context, tr("app.pro_required")); return; } await method.saveProperty(_propertyName, "$target"); _ignoreUpgradeConfirm = target; setState(() {}); }); }); } ================================================ FILE: lib/basic/config/ImageAddress.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; import 'Address.dart'; var _imageAddresses = [ "0", "1", "2", "3", "4", "5", "6", ]; late String _currentImageAddress; Future initImageAddress() async { _currentImageAddress = await method.getImageSwitchAddress(); } int currentImageAddress() { return int.parse(_currentImageAddress); } String currentImageAddressName() => _currentImageAddress == "0" ? tr('net.no_address') : tr('net.address') + _currentImageAddress; Future chooseImageAddress(BuildContext context) async { String? choose = await showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: Text(tr('settings.image_address.title')), children: [ ..._imageAddresses.map( (e) => SimpleDialogOption( child: ApiOptionRowImg( e == "0" ? tr('net.no_address') : tr('net.address') + e, e, key: Key("API:${e}"), ), onPressed: () { Navigator.of(context).pop(e); }, ), ), ], ); }, ); if (choose != null) { await method.setImageSwitchAddress(choose); _currentImageAddress = choose; } } Widget imageSwitchAddressSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr('settings.image_address.title')), subtitle: Text(currentImageAddressName()), onTap: () async { await chooseImageAddress(context); setState(() {}); }, ); }, ); } class ApiOptionRowImg extends StatefulWidget { final String title; final String value; const ApiOptionRowImg(this.title, this.value, {Key? key}) : super(key: key); @override State createState() => _ApiOptionRowImgState(); } class _ApiOptionRowImgState extends State { late Future _feature; @override void initState() { super.initState(); if ("0" != widget.value) { _feature = method.pingImg(widget.value); } else { _feature = method.ping(currentAddress()); } } @override Widget build(BuildContext context) { return Row( children: [ Text(widget.title), Expanded(child: Container()), FutureBuilder( future: _feature, builder: ( BuildContext context, AsyncSnapshot snapshot, ) { if (snapshot.connectionState != ConnectionState.done) { return PingStatus( tr('settings.image_address.pinging'), Colors.blue, ); } if (snapshot.hasError) { return PingStatus( tr('settings.image_address.failed'), Colors.red, ); } int ping = snapshot.requireData; if (ping <= 200) { return PingStatus( "${ping}ms", Colors.green, ); } if (ping <= 500) { return PingStatus( "${ping}ms", Colors.yellow, ); } return PingStatus( "${ping}ms", Colors.orange, ); }, ), ], ); } } ================================================ FILE: lib/basic/config/ImageFilter.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; const _propertyName = "imageFilter"; late ImageFilter imageFilter; Widget processImageFilter(Widget child) => imageFilter.process(child); Future initImageFilter() async { imageFilter = _imageFilterFromString(await method.loadProperty( _propertyName, _filters[0].name, )); } ImageFilter _imageFilterFromString(String string) { for (var value in _filters) { if (string == value.name) { return value; } } return _filters[0]; } class ImageFilter { final String name; final Widget Function(Widget widget) process; ImageFilter(this.name, this.process); } final List _filters = [ ImageFilter( tr("settings.image_filter.normal"), (child) { return child; }, ), ImageFilter( tr("settings.image_filter.gray"), (child) { return ColorFiltered( colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.color), child: child, ); }, ), ImageFilter( tr("settings.image_filter.brown"), (child) { return ColorFiltered( colorFilter: const ColorFilter.matrix([ 0.393, 0.769, 0.189, 0, 0, 0.349, 0.686, 0.168, 0, 0, 0.272, 0.534, 0.131, 0, 0, 0, 0, 0, 1, 0, ]), child: child, ); }, ), ImageFilter( "srgbToLinearGamma", (child) { return ColorFiltered( colorFilter: const ColorFilter.srgbToLinearGamma(), child: child, ); }, ), ImageFilter( "linearToSrgbGamma", (child) { return ColorFiltered( colorFilter: const ColorFilter.linearToSrgbGamma(), child: child, ); }, ), ]; Future chooseImageFilter(BuildContext context) async { Map map = {}; for (var element in _filters) { map[element.name] = element; } ImageFilter? result = await chooseMapDialog( context, map, tr("settings.image_filter.choose"), ); if (result != null) { await method.saveProperty(_propertyName, result.name); imageFilter = result; } } Widget imageFilterSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.image_filter.title")), subtitle: Text(imageFilter.name), onTap: () async { await chooseImageFilter(context); setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/ImportNotice.dart ================================================ import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; Widget importNotice(BuildContext context) { if (Platform.isAndroid) { return Container( margin: const EdgeInsets.all(15), padding: const EdgeInsets.all(15), color: (Theme .of(context) .textTheme .bodyText1 ?.color ?? Colors.black) .withOpacity(.01), child: Text( tr("settings.import_notice.android_desc"), ), ); } return Container(); } ================================================ FILE: lib/basic/config/IsPro.dart ================================================ import 'package:event/event.dart'; import 'package:pikapika/basic/Method.dart'; import '../Entities.dart'; bool get isPro { return _proInfoAll.proInfoAf.isPro || _proInfoAll.proInfoPat.isPro; } ProInfoAf get proInfoAf => _proInfoAll.proInfoAf; ProInfoPat get proInfoPat => _proInfoAll.proInfoPat; final proEvent = Event(); late ProInfoAll _proInfoAll; Future reloadIsPro() async { _proInfoAll = await method.proInfoAll(); proEvent.broadcast(); } ================================================ FILE: lib/basic/config/KeyboardController.dart ================================================ /// 上下键翻页 import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; const _propertyName = "keyboardController"; late bool keyboardController; Future initKeyboardController() async { keyboardController = (await method.loadProperty(_propertyName, "false")) == "true"; } Widget keyboardControllerSetting() { if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: keyboardController, title: Text(tr("settings.keyboard_controller.title")), onChanged: (target) async { await method.saveProperty(_propertyName, "$target"); keyboardController = target; setState(() {}); }, ); }, ); } return Container(); } ================================================ FILE: lib/basic/config/ListLayout.dart ================================================ /// 列表页的布局 import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; enum ListLayout { INFO_CARD, ONLY_IMAGE, COVER_AND_TITLE, } // const Map _listLayoutMap = { // '详情': ListLayout.INFO_CARD, // '封面': ListLayout.ONLY_IMAGE, // '封面+标题': ListLayout.COVER_AND_TITLE, // }; final Map _listLayoutMap = {}; const _propertyName = "listLayout"; late ListLayout currentLayout; var listLayoutEvent = Event(); Future initListLayout() async { _listLayoutMap.addAll({ tr('settings.list_layout.info_card'): ListLayout.INFO_CARD, tr('settings.list_layout.only_image'): ListLayout.ONLY_IMAGE, tr('settings.list_layout.cover_and_title'): ListLayout.COVER_AND_TITLE, }); currentLayout = _listLayoutFromString(await method.loadProperty( _propertyName, ListLayout.INFO_CARD.toString(), )); } ListLayout _listLayoutFromString(String layoutString) { for (var value in ListLayout.values) { if (layoutString == value.toString()) { return value; } } return ListLayout.INFO_CARD; } void _chooseListLayout(BuildContext context) async { ListLayout? layout = await chooseMapDialog(context, _listLayoutMap, tr('settings.list_layout.choose')); if (layout != null) { await method.saveProperty(_propertyName, layout.toString()); currentLayout = layout; listLayoutEvent.broadcast(); } } IconButton chooseLayoutActionButton(BuildContext context) => IconButton( onPressed: () { _chooseListLayout(context); }, icon: const Icon(Icons.view_quilt), ); const chooseListLayout = _chooseListLayout; ================================================ FILE: lib/basic/config/LocalHistorySync.dart ================================================ import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart'; import 'package:permission_handler/permission_handler.dart'; import '../Common.dart'; import '../Method.dart'; import 'ChooserRoot.dart'; const _dirPathPropertyName = "localHistorySyncRoot"; const _autoSavePropertyName = "localHistorySyncAuto"; late String _localHistorySyncRoot; late bool _localHistorySyncAuto; Future initLocalHistorySync() async { _localHistorySyncRoot = await method.loadProperty( _dirPathPropertyName, "", ); _localHistorySyncAuto = await method.loadProperty( _autoSavePropertyName, "false", ) == "true"; if (_localHistorySyncAuto) { localSync(); } } Future localSync() async { if (_localHistorySyncRoot.isEmpty) { return; } return await method.mergeHistoriesFromLocal(join( _localHistorySyncRoot, "pk.histories", )); } List localHistorySyncTiles() => [ localHistorySyncPathTile(), localHistorySyncAutoTile(), localHistorySyncManualTile(), ]; Widget localHistorySyncPathTile() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( onLongPress: () async { bool? clean = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(tr("settings.local_history_sync.clear_path")), content: Text(tr("settings.local_history_sync.clear_path_desc")), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(false); }, child: Text(tr("app.cancel")), ), TextButton( onPressed: () { Navigator.of(context).pop(true); }, child: Text(tr("app.confirm")), ), ], ); }, ); if (clean != null && clean == true) { await method.saveProperty(_dirPathPropertyName, ""); setState(() { _localHistorySyncRoot = ""; }); } }, onTap: () async { if (Platform.isAndroid) { final pState = await Permission.manageExternalStorage.request(); if (!pState.isGranted) { return; } } var dir = await FilePicker.platform.getDirectoryPath( dialogTitle: tr("settings.local_history_sync.choose_dir"), initialDirectory: Directory.fromUri(Uri.file(await currentChooserRoot())) .absolute .path, ); if (dir != null) { await method.saveProperty(_dirPathPropertyName, dir); setState(() { _localHistorySyncRoot = dir; }); } }, title: Text(tr("settings.local_history_sync.sync_to_local")), subtitle: Text(_localHistorySyncRoot.isEmpty ? tr("settings.local_history_sync.not_set") : _localHistorySyncRoot), ); }, ); } Widget localHistorySyncManualTile() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( onTap: () async { if (_localHistorySyncRoot.isEmpty) { defaultToast(context, tr("settings.local_history_sync.not_set")); return; } try { await localSync(); defaultToast(context, tr("settings.local_history_sync.sync_success")); } catch (e, s) { print("$e\n$s"); defaultToast(context, tr("settings.local_history_sync.sync_failed")); } }, title: Text(tr("settings.local_history_sync.sync_to_local")), ); }, ); } Widget localHistorySyncAutoTile() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _localHistorySyncAuto, onChanged: (bool value) async { await method.saveProperty( _autoSavePropertyName, value ? "true" : "false", ); setState(() { _localHistorySyncAuto = value; }); if (value) { localSync(); } }, title: Text(tr("settings.local_history_sync.auto_sync")), subtitle: Text(tr("settings.local_history_sync.auto_sync_desc")), ); }, ); } ================================================ FILE: lib/basic/config/NoAnimation.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; const _propertyName = "noAnimation"; late bool _noAnimation; Future initNoAnimation() async { _noAnimation = (await method.loadProperty(_propertyName, "false")) == "true"; } bool noAnimation() { return _noAnimation; } Future setNoAnimation(bool value) async { await method.saveProperty(_propertyName, "$value"); _noAnimation = value; } Widget noAnimationSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _noAnimation, title: Text(tr("settings.no_animation.title")), onChanged: (target) async { await method.saveProperty(_propertyName, "$target"); _noAnimation = target; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/PagerAction.dart ================================================ /// 列表页下一页的行为 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; enum PagerAction { CONTROLLER, STREAM, } // Map _pagerActionMap = { // "使用按钮": PagerAction.CONTROLLER, // "瀑布流": PagerAction.STREAM, // }; Map _pagerActionMap = {}; const _propertyName = "pagerAction"; late PagerAction _pagerAction; Future initPagerAction() async { _pagerActionMap.addAll({ tr("settings.pager_action.controller"): PagerAction.CONTROLLER, tr("settings.pager_action.stream"): PagerAction.STREAM, }); _pagerAction = _pagerActionFromString(await method.loadProperty( _propertyName, PagerAction.CONTROLLER.toString())); } PagerAction currentPagerAction() { return _pagerAction; } PagerAction _pagerActionFromString(String string) { for (var value in PagerAction.values) { if (string == value.toString()) { return value; } } return PagerAction.CONTROLLER; } String _currentPagerActionName() { for (var e in _pagerActionMap.entries) { if (e.value == _pagerAction) { return e.key; } } return ''; } Future _choosePagerAction(BuildContext context) async { PagerAction? result = await chooseMapDialog(context, _pagerActionMap, tr("settings.pager_action.choose")); if (result != null) { await method.saveProperty(_propertyName, result.toString()); _pagerAction = result; } } Widget pagerActionSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.pager_action.title")), subtitle: Text(_currentPagerActionName()), onTap: () async { await _choosePagerAction(context); setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/Platform.dart ================================================ /// 平台信息 import 'dart:io'; import '../Method.dart'; int androidVersion = 0; Future initPlatform()async{ if (Platform.isAndroid) { androidVersion = await method.androidGetVersion(); } } ================================================ FILE: lib/basic/config/Proxy.dart ================================================ /// 代理设置 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; late String _currentProxy; Future initProxy() async { _currentProxy = await method.getProxy(); return null; } String currentProxyName() { return _currentProxy == "" ? tr("settings.proxy.no_proxy") : _currentProxy; } Future inputProxy(BuildContext context) async { String? input = await displayTextInputDialog( context, src: _currentProxy, title: tr("settings.proxy.title"), hint: tr("settings.proxy.hint"), desc: tr("settings.proxy.desc"), ); if (input != null) { await method.setProxy(input); _currentProxy = input; } } Widget proxySetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.proxy.title")), subtitle: Text(currentProxyName()), onTap: () async { await inputProxy(context); setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/Quality.dart ================================================ /// 图片质量 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _ImageQualityOriginal = "original"; const _ImageQualityLow = "low"; const _ImageQualityMedium = "medium"; const _ImageQualityHigh = "high"; // const _LabelOriginal = "原图"; // const _LabelLow = "低"; // const _LabelMedium = "中"; // const _LabelHigh = "高"; // var _qualities = { // _LabelOriginal: _ImageQualityOriginal, // _LabelLow: _ImageQualityLow, // _LabelMedium: _ImageQualityMedium, // _LabelHigh: _ImageQualityHigh, // }; Map _qualities = {}; const _propertyName = "quality"; late String _currentQualityCode; const _defaultValue = _ImageQualityOriginal; Future initQuality() async { _qualities.addAll({ tr("settings.quality.original"): _ImageQualityOriginal, tr("settings.quality.low"): _ImageQualityLow, tr("settings.quality.medium"): _ImageQualityMedium, tr("settings.quality.high"): _ImageQualityHigh, }); _currentQualityCode = await method.loadProperty(_propertyName, _defaultValue); } String currentQualityCode() { return _currentQualityCode; } String currentQualityName() { for (var e in _qualities.entries) { if (e.value == _currentQualityCode) { return e.key; } } return ''; } Future chooseQuality(BuildContext context) async { String? code = await showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: Text(tr("settings.quality.choose")), children: [ ..._qualities.entries.map( (e) => SimpleDialogOption( child: Text(e.key), onPressed: () { Navigator.of(context).pop(e.value); }, ), ), ], ); }, ); if (code != null) { method.saveProperty(_propertyName, code); _currentQualityCode = code; } } Widget qualitySetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.quality.title")), subtitle: Text(currentQualityName()), onTap: () async { await chooseQuality(context); setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/ReaderBackgroundColor.dart ================================================ import 'dart:ui'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; final List _colors = []; // [ // ReaderBackgroundColor( // "黑色", // Colors.black, // ), // ReaderBackgroundColor( // "灰度", // Colors.grey, // ), // ReaderBackgroundColor( // "白色", // Colors.white, // ), // ]; const _propertyName = "readerBackgroundColor"; late ReaderBackgroundColor readerBackgroundColor; Color get readerBackgroundColorObj => readerBackgroundColor.color; Future initReaderBackgroundColor() async { _colors.addAll([ ReaderBackgroundColor(tr("settings.reader_background_color.black"), Colors.black), ReaderBackgroundColor(tr("settings.reader_background_color.gray"), Colors.grey), ReaderBackgroundColor(tr("settings.reader_background_color.white"), Colors.white), ]); readerBackgroundColor = _readerBackgroundColorFromString(await method.loadProperty( _propertyName, _colors[0].name, )); } ReaderBackgroundColor _readerBackgroundColorFromString(String string) { for (var value in _colors) { if (string == value.name) { return value; } } return _colors[0]; } class ReaderBackgroundColor { final String name; final Color color; ReaderBackgroundColor(this.name, this.color); } Future chooseReaderBackgroundColor(BuildContext context) async { Map map = {}; for (var element in _colors) { map[element.name] = element; } ReaderBackgroundColor? result = await chooseMapDialog( context, map, tr("settings.reader_background_color.choose"), ); if (result != null) { await method.saveProperty(_propertyName, result.name); readerBackgroundColor = result; } } Widget readerBackgroundColorSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.reader_background_color.title")), subtitle: Text(readerBackgroundColor.name), onTap: () async { await chooseReaderBackgroundColor(context); setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/ReaderDirection.dart ================================================ /// 阅读器的方向 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Method.dart'; enum ReaderDirection { TOP_TO_BOTTOM, LEFT_TO_RIGHT, RIGHT_TO_LEFT, } // const _types = { // '从上到下': ReaderDirection.TOP_TO_BOTTOM, // '从左到右': ReaderDirection.LEFT_TO_RIGHT, // '从右到左': ReaderDirection.RIGHT_TO_LEFT, // }; Map _types = {}; const _propertyName = "readerDirection"; late ReaderDirection gReaderDirection; Future initReaderDirection() async { _types.addAll({ tr("settings.reader_direction.top_to_bottom"): ReaderDirection.TOP_TO_BOTTOM, tr("settings.reader_direction.left_to_right"): ReaderDirection.LEFT_TO_RIGHT, tr("settings.reader_direction.right_to_left"): ReaderDirection.RIGHT_TO_LEFT, }); gReaderDirection = _pagerDirectionFromString(await method.loadProperty( _propertyName, ReaderDirection.TOP_TO_BOTTOM.toString())); } ReaderDirection _pagerDirectionFromString(String pagerDirectionString) { for (var value in ReaderDirection.values) { if (pagerDirectionString == value.toString()) { return value; } } return ReaderDirection.TOP_TO_BOTTOM; } String _currentReaderDirectionName() { for (var e in _types.entries) { if (e.value == gReaderDirection) { return e.key; } } return ''; } var gReaderDirectionName = _currentReaderDirectionName; /// ?? to ActionButton And Event ?? Future choosePagerDirection(BuildContext buildContext) async { ReaderDirection? choose = await showDialog( context: buildContext, builder: (BuildContext context) { return SimpleDialog( title: Text(tr("settings.reader_direction.choose")), children: _types.entries .map((e) => SimpleDialogOption( child: Text(e.key), onPressed: () { Navigator.of(context).pop(e.value); }, )) .toList(), ); }, ); if (choose != null) { await method.saveProperty(_propertyName, choose.toString()); gReaderDirection = choose; } } Widget readerDirectionSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.reader_direction.title")), subtitle: Text(_currentReaderDirectionName()), onTap: () async { await choosePagerDirection(context); setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/ReaderScrollByScreenPercentage.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _propertyName = "readerScrollByScreenPercentage"; late int _readerScrollByScreenPercentage; Future initReaderScrollByScreenPercentage() async { _readerScrollByScreenPercentage = int.parse((await method.loadProperty(_propertyName, "80"))); } double get readerScrollByScreenPercentage => _readerScrollByScreenPercentage / 100; int currentReaderScrollByScreenPercentage() => _readerScrollByScreenPercentage; Future setReaderScrollByScreenPercentage(int value) async { _readerScrollByScreenPercentage = value; await method.saveProperty(_propertyName, "$value"); } Widget readerScrollByScreenPercentageSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.reader_scroll_by_screen_percentage.title") + " : $_readerScrollByScreenPercentage%" + tr("settings.reader_scroll_by_screen_percentage.screen_size")), subtitle: Slider( min: 5.toDouble(), max: 110.toDouble(), value: _readerScrollByScreenPercentage.toDouble(), onChanged: (double value) async { final va = value.toInt(); await setReaderScrollByScreenPercentage(va); setState(() {}); }, divisions: (110 - 5), ), ); }, ); } ================================================ FILE: lib/basic/config/ReaderSliderPosition.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Method.dart'; import '../Common.dart'; enum ReaderSliderPosition { BOTTOM, RIGHT, LEFT } // const _positionNames = { // ReaderSliderPosition.BOTTOM: '下方', // ReaderSliderPosition.RIGHT: '右侧', // ReaderSliderPosition.LEFT: '左侧', // }; Map _positionNames = {}; const _propertyName = "readerSliderPosition"; late ReaderSliderPosition _readerSliderPosition; Future initReaderSliderPosition() async { _positionNames.addAll({ ReaderSliderPosition.BOTTOM: tr("settings.reader_slider_position.bottom"), ReaderSliderPosition.RIGHT: tr("settings.reader_slider_position.right"), ReaderSliderPosition.LEFT: tr("settings.reader_slider_position.left"), }); _readerSliderPosition = _readerSliderPositionFromString( await method.loadProperty(_propertyName, ""), ); } ReaderSliderPosition _readerSliderPositionFromString(String str) { for (var value in ReaderSliderPosition.values) { if (str == value.toString()) return value; } return ReaderSliderPosition.BOTTOM; } ReaderSliderPosition currentReaderSliderPosition() => _readerSliderPosition; String currentReaderSliderPositionName() => _positionNames[_readerSliderPosition] ?? ""; Future chooseReaderSliderPosition(BuildContext context) async { Map map = {}; _positionNames.forEach((key, value) { map[value] = key; }); ReaderSliderPosition? result = await chooseMapDialog(context, map, tr("settings.reader_slider_position.choose")); if (result != null) { await method.saveProperty(_propertyName, result.toString()); _readerSliderPosition = result; } } Widget readerSliderPositionSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.reader_slider_position.title")), subtitle: Text(currentReaderSliderPositionName()), onTap: () async { await chooseReaderSliderPosition(context); setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/ReaderTwoPageDirection.dart ================================================ /// 阅读器的方向 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Method.dart'; enum ReaderTwoPageDirection { CLOSE_TO, PULL_AWAY, EACH_CENTERED, } // const _types = { // '靠近': ReaderTwoPageDirection.CLOSE_TO, // '远离': ReaderTwoPageDirection.PULL_AWAY, // '各自居中': ReaderTwoPageDirection.EACH_CENTERED, // }; Map _types = {}; const _propertyName = "readerTwoPageDirection"; late ReaderTwoPageDirection gReaderTwoPageDirection; Future initReaderTwoPageDirection() async { _types.addAll({ ReaderTwoPageDirection.CLOSE_TO: tr("settings.reader_two_page_direction.close_to"), ReaderTwoPageDirection.PULL_AWAY: tr("settings.reader_two_page_direction.pull_away"), ReaderTwoPageDirection.EACH_CENTERED: tr("settings.reader_two_page_direction.each_centered"), }); gReaderTwoPageDirection = _pagerDirectionFromString(await method.loadProperty( _propertyName, ReaderTwoPageDirection.CLOSE_TO.toString())); } ReaderTwoPageDirection _pagerDirectionFromString(String pagerDirectionString) { for (var value in ReaderTwoPageDirection.values) { if (pagerDirectionString == value.toString()) { return value; } } return ReaderTwoPageDirection.CLOSE_TO; } String _currentReaderTwoPageDirectionName() { for (var e in _types.entries) { if (e.key == gReaderTwoPageDirection) { return e.value; } } return ''; } var gReaderTwoPageDirectionName = _currentReaderTwoPageDirectionName; /// ?? to ActionButton And Event ?? Future chooseTwoPagerDirection(BuildContext buildContext) async { ReaderTwoPageDirection? choose = await showDialog( context: buildContext, builder: (BuildContext context) { return SimpleDialog( title: Text(tr("settings.reader_two_page_direction.choose")), children: _types.entries .map((e) => SimpleDialogOption( child: Text(e.value), onPressed: () { Navigator.of(context).pop(e.key); }, )) .toList(), ); }, ); if (choose != null) { await method.saveProperty(_propertyName, choose.toString()); gReaderTwoPageDirection = choose; } } Widget readerTwoPageDirectionSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.reader_two_page_direction.title")), subtitle: Text(_currentReaderTwoPageDirectionName()), onTap: () async { await chooseTwoPagerDirection(context); setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/ReaderType.dart ================================================ /// 阅读器的类型 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; enum ReaderType { WEB_TOON, WEB_TOON_ZOOM, GALLERY, // WEB_TOON_FREE_ZOOM, TWO_PAGE_GALLERY, } // const _types = { // 'WebToon (默认)': ReaderType.WEB_TOON, // 'WebToon (双击放大)': ReaderType.WEB_TOON_ZOOM, // '相册': ReaderType.GALLERY, // 'WebToon (ListView双击放大)\n(此模式进度条无效)': ReaderType.WEB_TOON_FREE_ZOOM, // '双页模式\n(实验)': ReaderType.TWO_PAGE_GALLERY, // }; Map _types = {}; enum TwoPageDirection { LEFT_TO_RIGHT, RIGHT_TO_LEFT, } String _twoPageDirectionName(TwoPageDirection direction) { switch (direction) { case TwoPageDirection.LEFT_TO_RIGHT: return tr("settings.reader_type.left_to_right"); case TwoPageDirection.RIGHT_TO_LEFT: return tr("settings.reader_type.right_to_left"); } } TwoPageDirection _twoPageDirectionFromString(String directionString) { for (var value in TwoPageDirection.values) { if (directionString == value.toString()) { return value; } } return TwoPageDirection.LEFT_TO_RIGHT; } const _propertyName = "readerType"; late ReaderType _readerType; const _twoPageDirectionPropertyName = "twoPageDirection"; late TwoPageDirection _twoPageDirection; TwoPageDirection get twoPageDirection => _twoPageDirection; Future initReaderType() async { _types.addAll({ tr("settings.reader_type.web_toon"): ReaderType.WEB_TOON, tr("settings.reader_type.web_toon_zoom"): ReaderType.WEB_TOON_ZOOM, tr("settings.reader_type.gallery"): ReaderType.GALLERY, // tr("settings.reader_type.web_toon_free_zoom"): ReaderType.WEB_TOON_FREE_ZOOM, tr("settings.reader_type.two_page_gallery"): ReaderType.TWO_PAGE_GALLERY, }); _readerType = _readerTypeFromString( await method.loadProperty(_propertyName, ReaderType.WEB_TOON.toString())); _twoPageDirection = _twoPageDirectionFromString(await method.loadProperty( _twoPageDirectionPropertyName, TwoPageDirection.LEFT_TO_RIGHT.toString())); } ReaderType currentReaderType() { return _readerType; } ReaderType _readerTypeFromString(String pagerTypeString) { for (var value in ReaderType.values) { if (pagerTypeString == value.toString()) { return value; } } return ReaderType.WEB_TOON; } String currentReaderTypeName() { for (var e in _types.entries) { if (e.value == _readerType) { return e.key; } } return ''; } Future choosePagerType(BuildContext buildContext) async { ReaderType? t = await showDialog( context: buildContext, builder: (BuildContext context) { return SimpleDialog( title: Text(tr("settings.reader_type.choose")), children: _types.entries .map((e) => SimpleDialogOption( child: Text(e.key), onPressed: () { Navigator.of(context).pop(e.value); }, )) .toList(), ); }, ); if (t != null) { await method.saveProperty(_propertyName, t.toString()); _readerType = t; } } Widget readerTypeSettings() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { List children = []; children.add(_readerTypeTile(context, setState)); // if (_readerType == ReaderType.TWO_PAGE_GALLERY) { children.add(_twoPageDirectionTile(context, setState)); // } return Column(children: children); }, ); } Widget _readerTypeTile( BuildContext context, void Function(void Function()) setState, ) { return ListTile( title: Text(tr("settings.reader_type.title")), subtitle: Text(currentReaderTypeName()), onTap: () async { await choosePagerType(context); setState(() {}); }, ); } Widget _twoPageDirectionTile( BuildContext context, void Function(void Function()) setState, ) { return ListTile( title: Text(tr("settings.reader_type.two_page_direction")), subtitle: Text(_twoPageDirectionName(_twoPageDirection)), onTap: () async { TwoPageDirection? t = await showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: Text(tr("settings.reader_type.two_page_direction_choose")), children: TwoPageDirection.values .map((e) => SimpleDialogOption( child: Text(_twoPageDirectionName(e)), onPressed: () { Navigator.of(context).pop(e); }, )) .toList(), ); }, ); if (t != null) { await method.saveProperty(_twoPageDirectionPropertyName, t.toString()); _twoPageDirection = t; setState(() {}); } }, ); } ================================================ FILE: lib/basic/config/ReaderZoomScale.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/i18.dart'; import '../Method.dart'; const _readerZoomMinPropertyName = "readerZoomMinScale"; const _readerZoomMaxPropertyName = "readerZoomMaxScale"; const _readerZoomDoubleTapPropertyName = "readerZoomDoubleTapScale"; late double _readerZoomMinScale; late double _readerZoomMaxScale; late double _readerZoomDoubleTapScale; double get readerZoomMinScale => _readerZoomMinScale; double get readerZoomMaxScale => _readerZoomMaxScale; double get readerZoomDoubleTapScale => _readerZoomDoubleTapScale; Future setReaderZoomMinScale(double value) async { _readerZoomMinScale = value; await method.saveProperty( _readerZoomMinPropertyName, value.toStringAsFixed(1), ); } Future setReaderZoomMaxScale(double value) async { _readerZoomMaxScale = value; await method.saveProperty( _readerZoomMaxPropertyName, value.toStringAsFixed(1), ); } Future setReaderZoomDoubleTapScale(double value) async { _readerZoomDoubleTapScale = value; await method.saveProperty( _readerZoomDoubleTapPropertyName, value.toStringAsFixed(1), ); } Future initReaderZoomScale() async { _readerZoomMinScale = double.tryParse(await method.loadProperty(_readerZoomMinPropertyName, "1.0")) ?? 1.0; _readerZoomMaxScale = double.tryParse(await method.loadProperty(_readerZoomMaxPropertyName, "2.0")) ?? 2.0; _readerZoomDoubleTapScale = double.tryParse(await method.loadProperty(_readerZoomDoubleTapPropertyName, "2.0")) ?? 2.0; } Widget readerZoomMinScaleSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text( "${tr("settings.reader_zoom.out_title")} : ${_readerZoomMinScale.toStringAsFixed(1)}x", ), subtitle: Slider( min: 0.1, max: 1.0, divisions: 9, value: _readerZoomMinScale.clamp(0.1, 1.0).toDouble(), label: "${_readerZoomMinScale.toStringAsFixed(1)}x", onChanged: (double value) { final newValue = (value * 10).roundToDouble() / 10; setState(() { _readerZoomMinScale = newValue; }); setReaderZoomMinScale(newValue); }, ), ); }, ); } Widget readerZoomMaxScaleSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text( "${tr("settings.reader_zoom.in_title")} : ${_readerZoomMaxScale.toStringAsFixed(1)}x", ), subtitle: Slider( min: 1.0, max: 30.0, divisions: 29, value: _readerZoomMaxScale.clamp(1.0, 30.0).toDouble(), label: "${_readerZoomMaxScale.toStringAsFixed(1)}x", onChanged: (double value) { final newValue = value.roundToDouble(); setState(() { _readerZoomMaxScale = newValue; }); setReaderZoomMaxScale(newValue); }, ), ); }, ); } Widget readerZoomDoubleTapScaleSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text( "${tr("settings.reader_zoom.double_tap_title")} : ${_readerZoomDoubleTapScale.toStringAsFixed(1)}x", ), subtitle: Slider( min: 1.5, max: 5.0, divisions: 7, value: _readerZoomDoubleTapScale.clamp(1.5, 5.0).toDouble(), label: "${_readerZoomDoubleTapScale.toStringAsFixed(1)}x", onChanged: (double value) { final newValue = (value * 2).roundToDouble() / 2; setState(() { _readerZoomDoubleTapScale = newValue; }); setReaderZoomDoubleTapScale(newValue); }, ), ); }, ); } ================================================ FILE: lib/basic/config/RecommendLinks.dart ================================================ import 'package:event/event.dart'; import '../Method.dart'; var recommendLinksEvent = Event(); Map _recommendLinks = {}; Map currentRecommendLinks() => _recommendLinks; Future initRecommendLinks() async { try { _recommendLinks = await method.configLinks(); } catch (_) { _recommendLinks = {}; } recommendLinksEvent.broadcast(); } ================================================ FILE: lib/basic/config/ShadowCategories.dart ================================================ /// 屏蔽的分类 import 'dart:convert'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; import '../store/Categories.dart'; import 'ShadowCategoriesEvent.dart'; const _propertyName = "shadowCategories"; late List shadowCategories; /// 获取封印的类型 Future> _loadShadowCategories() async { var value = await method.loadProperty(_propertyName, jsonEncode([])); return List.of(jsonDecode(value)).map((e) => "$e").toList(); } /// 保存封印的类型 Future _saveShadowCategories(List value) { return method.saveProperty(_propertyName, jsonEncode(value)); } Future initShadowCategories() async { shadowCategories = await _loadShadowCategories(); } Future _chooseShadowCategories(BuildContext context) async { final theme = Theme.of(context); final result = await showDialog>( context: context, builder: (ctx) => _ShadowCategoriesDialog( theme: theme, items: storedCategories, initialValue: shadowCategories, ), ); if (result != null) { await _saveShadowCategories(result); shadowCategories = result; shadowCategoriesEvent.broadcast(); } } Widget shadowCategoriesActionButton(BuildContext context) { return IconButton( onPressed: () { _chooseShadowCategories(context); }, icon: const Icon(Icons.hide_source), ); } Widget shadowCategoriesSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.shadow_categories.title")), subtitle: Text(jsonEncode(shadowCategories)), onTap: () async { await _chooseShadowCategories(context); setState(() {}); }, ); }, ); } const chooseShadowCategories = _chooseShadowCategories; class _ShadowCategoriesDialog extends StatefulWidget { final ThemeData theme; final List items; final List initialValue; const _ShadowCategoriesDialog({ required this.theme, required this.items, required this.initialValue, }); @override State<_ShadowCategoriesDialog> createState() => _ShadowCategoriesDialogState(); } class _ShadowCategoriesDialogState extends State<_ShadowCategoriesDialog> { late final Set _selected = {...widget.initialValue}; String _query = ""; @override Widget build(BuildContext context) { final visibleItems = widget.items .where((e) => _query.isEmpty || e.toLowerCase().contains(_query)) .toList(); return AlertDialog( backgroundColor: widget.theme.scaffoldBackgroundColor, title: Text(tr("settings.shadow_categories.title")), content: SizedBox( width: double.maxFinite, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( decoration: InputDecoration( hintText: tr("settings.shadow_categories.search_hint"), ), onChanged: (v) { setState(() { _query = v.trim().toLowerCase(); }); }, ), const SizedBox(height: 12), Flexible( child: Scrollbar( thumbVisibility: true, child: ListView.builder( shrinkWrap: true, itemCount: visibleItems.length, itemBuilder: (ctx, index) { final item = visibleItems[index]; final checked = _selected.contains(item); return CheckboxListTile( value: checked, dense: true, controlAffinity: ListTileControlAffinity.leading, title: Text(item, style: widget.theme.textTheme.bodyMedium), activeColor: widget.theme.primaryColor, onChanged: (v) { setState(() { if (v == true) { _selected.add(item); } else { _selected.remove(item); } }); }, ); }, ), ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(tr("app.cancel")), ), TextButton( onPressed: () => Navigator.of(context).pop(_selected.toList()), child: Text(tr("app.confirm")), ), ], ); } } ================================================ FILE: lib/basic/config/ShadowCategoriesEvent.dart ================================================ import 'package:event/event.dart'; var shadowCategoriesEvent = Event(); ================================================ FILE: lib/basic/config/ShadowCategoriesMode.dart ================================================ /// 屏蔽方式 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; import 'ShadowCategoriesEvent.dart'; enum ShadowCategoriesMode { BLACK_LIST, WHITE_LIST, } // Map _fullScreenActionMap = { // "黑名单": ShadowCategoriesMode.BLACK_LIST, // "白名单": ShadowCategoriesMode.WHITE_LIST, // }; Map _fullScreenActionMap = {}; const _propertyName = "shadowCategoriesMode"; late ShadowCategoriesMode _shadowCategoriesMode; Future initShadowCategoriesMode() async { _fullScreenActionMap.addAll({ tr("settings.shadow_categories_mode.black_list"): ShadowCategoriesMode.BLACK_LIST, tr("settings.shadow_categories_mode.white_list"): ShadowCategoriesMode.WHITE_LIST, }); _shadowCategoriesMode = _shadowCategoriesModeFromString(await method.loadProperty( _propertyName, ShadowCategoriesMode.BLACK_LIST.toString(), )); } ShadowCategoriesMode currentShadowCategoriesMode() { return _shadowCategoriesMode; } ShadowCategoriesMode _shadowCategoriesModeFromString(String string) { for (var value in ShadowCategoriesMode.values) { if (string == value.toString()) { return value; } } return ShadowCategoriesMode.BLACK_LIST; } String _currentShadowCategoriesMode() { for (var e in _fullScreenActionMap.entries) { if (e.value == _shadowCategoriesMode) { return e.key; } } return ''; } Future _chooseShadowCategoriesMode(BuildContext context) async { ShadowCategoriesMode? result = await chooseMapDialog( context, _fullScreenActionMap, tr("settings.shadow_categories_mode.title")); if (result != null) { await method.saveProperty(_propertyName, result.toString()); _shadowCategoriesMode = result; shadowCategoriesEvent.broadcast(); } } Widget shadowCategoriesModeSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.shadow_categories_mode.title")), subtitle: Text(_currentShadowCategoriesMode()), onTap: () async { await _chooseShadowCategoriesMode(context); setState(() {}); }, ); }, ); } Widget shadowSwitchActionButton(BuildContext context) { return IconButton( onPressed: () { _chooseShadowCategoriesMode(context); }, icon: const Icon(Icons.do_not_disturb_on_outlined), ); } const chooseShadowCategoriesMode = _chooseShadowCategoriesMode; ================================================ FILE: lib/basic/config/ShowCommentAtDownload.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; const _propertyName = "showCommentAtDownload"; late bool _showCommentAtDownload; Future initShowCommentAtDownload() async { _showCommentAtDownload = (await method.loadProperty(_propertyName, "false")) == "true"; } bool showCommentAtDownload() { return _showCommentAtDownload; } Widget showCommentAtDownloadSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _showCommentAtDownload, title: Text(tr("settings.show_comment_at_download.title")), onChanged: (target) async { await method.saveProperty(_propertyName, "$target"); _showCommentAtDownload = target; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/StartupPic.dart ================================================ import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path/path.dart' as p; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Method.dart'; Widget setStartupPicTile(BuildContext context) { return ListTile( title: Text(tr("settings.startup_pic.title")), subtitle: Text(tr("settings.startup_pic.subtitle")), onTap: () { if (Platform.isAndroid || Platform.isIOS) { _updateStartupPicPhone(context); } else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { _updateStartupPicDesktop(context); } }, ); } Widget clearStartupPicTile(BuildContext context) { return ListTile( title: Text(tr("settings.startup_pic.clear_title")), subtitle: Text(tr("settings.startup_pic.clear_subtitle")), onTap: () async { await clearStartupPic(context); defaultToast(context, tr("settings.startup_pic.clear_success")); }, ); } Future _updateStartupPicPhone(BuildContext context) async { final ImagePicker _picker = ImagePicker(); final XFile? image = await _picker.pickImage(source: ImageSource.gallery); if (image != null) { await image.saveTo(p.join(await method.dataLocal(), "startup_pic")); defaultToast(context, tr("settings.startup_pic.update_success")); } } Future _updateStartupPicDesktop(BuildContext context) async { FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: false, ); if (result != null) { final file = result.files.single; final startupPicPath = p.join(await method.dataLocal(), "startup_pic"); final destination = File(startupPicPath); await destination.create(recursive: true); await File(file.path!).copy(destination.path); defaultToast(context, tr("settings.startup_pic.update_success")); } } Future clearStartupPic(BuildContext context) async { final startupPicPath = p.join(await method.dataLocal(), "startup_pic"); final file = File(startupPicPath); if (await file.exists()) { await file.delete(); } defaultToast(context, tr("settings.startup_pic.clear_success")); } ================================================ FILE: lib/basic/config/Themes.dart ================================================ /// 主题 import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pikapika/basic/Common.dart'; import '../Method.dart'; import 'Platform.dart'; // 字体相关 const _fontFamilyProperty = "fontFamily"; List _fontFamily = []; List _fontList = []; Future initFont() async { if (Platform.isIOS) { try { _fontList = await method.fontList(); } catch (e, s) { print("获取字体列表失败: $e\n$s"); _fontList = []; } } var defaultFont = ""; _fontFamily = (await method.loadProperty(_fontFamilyProperty, defaultFont)) .split(",") .map((e) => e.trim()) .where((e) => e.isNotEmpty) .toList(); } ThemeData _fontThemeData(bool dark) { return ThemeData( brightness: dark ? Brightness.dark : Brightness.light, fontFamily: _fontFamily.isEmpty ? null : _fontFamily.first, ////fontFamilyFallback: _fontFamily.length > 1 ? _fontFamily.sublist(1) : null, ); } Future inputFont(BuildContext context) async { var font = await displayTextInputDialog( context, src: "$_fontFamily", title: tr("settings.font.title"), hint: tr("settings.font.hint"), desc: tr("settings.font.input_hint"), ); if (font != null) { await method.saveProperty(_fontFamilyProperty, font); _fontFamily = font .split(",") .map((e) => e.trim()) .where((e) => e.isNotEmpty) .toList(); _reloadTheme(); } } Future chooseFontFromList(BuildContext context) async { var font = await showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: Text(tr("settings.font.choose_hint")), children: _fontList.map((e) { return SimpleDialogOption( child: Container( padding: const EdgeInsets.all(8.0), child: Text( "$e\n 我能吞下玻璃而不伤身体\n The quick brown fox jumps over the lazy dog.", style: TextStyle(fontFamily: e, fontSize: 16), ), ), onPressed: () { Navigator.of(context).pop(e); }, ); }).toList(), ); }, ); if (font == null || font.isEmpty) { return null; } return font; } Future chooseFont(BuildContext context) async { List fonts = []; while (true) { var font = await chooseFontFromList(context); if (font == null) { break; } fonts.add(font); } await method.saveProperty(_fontFamilyProperty, fonts.join(",")); _fontFamily = fonts; _reloadTheme(); } Widget fontSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.font.title")), subtitle: Text(_fontFamily.join(",")), onTap: () async { if (_fontList.isEmpty) { await inputFont(context); } else { await chooseFont(context); } setState(() {}); }, ); }, ); } // const _enableStatusBarColorProperty = "enableStatusBarColorProperty"; var _enableStatusBarColor = false; Widget enableStatusBarColorSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("settings.theme.enable_status_bar_color")), subtitle: Text(tr("settings.theme.enable_status_restart_hint")), value: _enableStatusBarColor, onChanged: (bool value) async { await method.saveProperty( _enableStatusBarColorProperty, "$value"); _enableStatusBarColor = value; _reloadTheme(); setState(() {}); }, ); }, ); } // 主题相关 // 主题包 abstract class _ThemePackage { String code(); String name(); ThemeData themeData(ThemeData rawData); bool isDark(); } class _OriginTheme extends _ThemePackage { @override String code() => "origin"; @override String name() => tr("settings.theme.origin"); @override ThemeData themeData(ThemeData rawData) => rawData; @override bool isDark() => false; } class _BookTheme extends _ThemePackage { @override String code() => "book"; @override String name() => tr("settings.theme.book"); @override ThemeData themeData(ThemeData rawData) => rawData.copyWith( brightness: Brightness.dark, appBarTheme: AppBarTheme( elevation: 0, foregroundColor: Colors.grey.shade700, systemOverlayStyle: SystemUiOverlayStyle.light, color: Colors.transparent, iconTheme: IconThemeData( color: Colors.grey.shade700, ), shape: Border( bottom: BorderSide(color: Colors.grey.shade500, width: 2), ), titleTextStyle: TextStyle( color: Colors.grey.shade700, fontSize: 16, ), ), ); @override bool isDark() => false; } class _PinkTheme extends _ThemePackage { @override String code() => "pink"; @override String name() => tr("settings.theme.pink"); @override ThemeData themeData(ThemeData rawData) => rawData.copyWith( brightness: Brightness.light, colorScheme: ColorScheme.light( primary: Colors.pink.shade200, secondary: Colors.pink.shade200, ), appBarTheme: AppBarTheme( systemOverlayStyle: SystemUiOverlayStyle.light.copyWith( statusBarColor: _enableStatusBarColor ? Colors.pink.shade200 : null, ), color: Colors.pink.shade200, iconTheme: const IconThemeData( color: Colors.white, ), ), bottomNavigationBarTheme: BottomNavigationBarThemeData( selectedItemColor: Colors.pink[300], unselectedItemColor: Colors.grey[500], ), dividerColor: Colors.grey.shade200, primaryColor: Colors.pink.shade200, textSelectionTheme: TextSelectionThemeData( cursorColor: Colors.pink.shade200, selectionColor: Colors.pink.shade300.withAlpha(150), selectionHandleColor: Colors.pink.shade300.withAlpha(200), ), inputDecorationTheme: InputDecorationTheme( focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.pink.shade200), ), ), ); @override bool isDark() => false; } class _BlackTheme extends _ThemePackage { @override String code() => "black"; @override String name() => tr("settings.theme.black"); @override ThemeData themeData(ThemeData rawData) => rawData.copyWith( brightness: Brightness.light, colorScheme: ColorScheme.light( primary: Colors.pink.shade200, secondary: Colors.pink.shade200, ), appBarTheme: AppBarTheme( systemOverlayStyle: SystemUiOverlayStyle.light.copyWith( statusBarColor: _enableStatusBarColor ? Colors.grey.shade800 : null, ), color: Colors.grey.shade800, iconTheme: const IconThemeData( color: Colors.white, ), ), bottomNavigationBarTheme: BottomNavigationBarThemeData( selectedItemColor: Colors.white, unselectedItemColor: Colors.grey[400], backgroundColor: Colors.grey.shade800, ), dividerColor: Colors.grey.shade200, primaryColor: Colors.pink.shade200, textSelectionTheme: TextSelectionThemeData( cursorColor: Colors.pink.shade200, selectionColor: Colors.pink.shade300.withAlpha(150), selectionHandleColor: Colors.pink.shade300.withAlpha(200), ), inputDecorationTheme: InputDecorationTheme( focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.pink.shade200), ), ), ); @override bool isDark() => false; } class _DarkTheme extends _ThemePackage { @override String code() => "dark"; @override String name() => tr("settings.theme.dark"); @override ThemeData themeData(ThemeData rawData) => rawData.copyWith( brightness: Brightness.dark, colorScheme: ColorScheme.light( primary: Colors.pink.shade200, secondary: Colors.pink.shade200, ), appBarTheme: AppBarTheme( systemOverlayStyle: SystemUiOverlayStyle.light.copyWith( statusBarColor: _enableStatusBarColor ? const Color(0xFF1E1E1E) : null, ), color: const Color(0xFF1E1E1E), foregroundColor: Colors.white, iconTheme: const IconThemeData( color: Colors.white, ), ), bottomNavigationBarTheme: BottomNavigationBarThemeData( selectedItemColor: Colors.white, unselectedItemColor: Colors.grey.shade300, backgroundColor: Colors.grey.shade900, ), dividerColor: Colors.grey.shade500.withAlpha(70), primaryColor: Colors.pink.shade200, textSelectionTheme: TextSelectionThemeData( cursorColor: Colors.pink.shade200, selectionColor: Colors.pink.shade300.withAlpha(150), selectionHandleColor: Colors.pink.shade300.withAlpha(200), ), inputDecorationTheme: InputDecorationTheme( focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.pink.shade200), ), ), sliderTheme: SliderThemeData( activeTrackColor: Colors.pink.shade200, inactiveTrackColor: Colors.pink.shade300.withAlpha(150), thumbColor: Colors.pink.shade200, overlayColor: Colors.pink.shade300.withAlpha(150), thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 10), ), ); @override bool isDark() => true; } class _DustyBlueTheme extends _ThemePackage { @override String code() => "dustyBlue"; @override String name() => tr("settings.theme.dusty_blue"); @override ThemeData themeData(ThemeData rawData) => rawData.copyWith( brightness: Brightness.dark, scaffoldBackgroundColor: Color.alphaBlend( const Color(0x11999999), const Color(0xff20253b), ), cardColor: Color.alphaBlend( const Color(0x11AAAAAA), const Color(0xff20253b), ), colorScheme: ColorScheme.light( primary: Colors.blue.shade200, secondary: Colors.blue.shade200, ), appBarTheme: AppBarTheme( systemOverlayStyle: SystemUiOverlayStyle.light.copyWith( statusBarColor: _enableStatusBarColor ? const Color(0xff20253b) : null, ), color: const Color(0xff20253b), foregroundColor: Colors.white, iconTheme: const IconThemeData( color: Colors.white, ), ), dialogTheme: const DialogTheme( backgroundColor: Color(0xff20253b), ), bottomNavigationBarTheme: BottomNavigationBarThemeData( backgroundColor: const Color(0xff191b26), selectedItemColor: Colors.blue.shade200, unselectedItemColor: Colors.grey.shade500, ), dividerColor: Colors.grey.shade800, primaryColor: Colors.blue.shade200, textSelectionTheme: TextSelectionThemeData( cursorColor: Colors.blue.shade200, selectionColor: Colors.blue.shade900, selectionHandleColor: Colors.blue.shade800, ), inputDecorationTheme: InputDecorationTheme( focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blue.shade500), ), ), sliderTheme: SliderThemeData( activeTrackColor: Colors.blue.shade200, inactiveTrackColor: Colors.blue.shade300.withAlpha(150), thumbColor: Colors.blue.shade200, overlayColor: Colors.blue.shade300.withAlpha(150), thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 10), ), ); @override bool isDark() => true; } class _DarkBlackTheme extends _ThemePackage { @override String code() => "dark_black"; @override String name() => tr("settings.theme.dark_black"); @override ThemeData themeData(ThemeData rawData) => rawData.copyWith( brightness: Brightness.dark, colorScheme: ColorScheme.light( primary: Colors.pink.shade200, secondary: Colors.pink.shade200, ), scaffoldBackgroundColor: Colors.black, appBarTheme: AppBarTheme( systemOverlayStyle: SystemUiOverlayStyle.light.copyWith( statusBarColor: _enableStatusBarColor ? const Color.fromARGB(0xff, 10, 10, 10) : null, ), color: const Color.fromARGB(0xff, 10, 10, 10), foregroundColor: Colors.white, iconTheme: const IconThemeData( color: Colors.white, ), ), bottomNavigationBarTheme: BottomNavigationBarThemeData( selectedItemColor: Colors.white, unselectedItemColor: Colors.grey.shade300, backgroundColor: const Color.fromARGB(0xff, 10, 10, 10), ), primaryColor: Colors.pink.shade200, textSelectionTheme: TextSelectionThemeData( cursorColor: Colors.pink.shade200, selectionColor: Colors.pink.shade300.withAlpha(150), selectionHandleColor: Colors.pink.shade300.withAlpha(200), ), inputDecorationTheme: InputDecorationTheme( focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.pink.shade200), ), ), dividerColor: const Color.fromARGB(0xff, 64, 64, 64), ); @override bool isDark() => true; } final _themePackages = <_ThemePackage>[ _OriginTheme(), _PinkTheme(), _BlackTheme(), _DarkTheme(), _DustyBlueTheme(), _DarkBlackTheme(), _BookTheme(), ]; // 主题更换事件 var themeEvent = Event(); const _nightModePropertyName = "androidNightMode"; const _lightThemePropertyName = "theme"; const _darkThemePropertyName = "theme.dark"; const _defaultLightThemeCode = "pink"; const _defaultDarkThemeCode = "dark"; bool androidNightModeDisplay = false; bool androidNightMode = false; String? _lightThemeCode; ThemeData? _lightThemeData; String? _darkThemeCode; ThemeData? _darkThemeData; // _changeThemeByCode String _codeToName(String? code) { for (var package in _themePackages) { if (code == package.code()) { return package.name(); } } return ""; } String currentLightThemeName() { return _codeToName(_lightThemeCode); } String currentDarkThemeName() { return _codeToName(_darkThemeCode); } ThemeData? currentLightThemeData() { return _lightThemeData; } ThemeData? currentDarkThemeData() { return _darkThemeData; } // 根据Code选择主题, 并发送主题更换事件 ThemeData? _themeByCode(String? themeCode) { for (var package in _themePackages) { if (themeCode == package.code()) { return package.themeData(_fontThemeData(package.isDark())); } } return null; } void _reloadTheme() { _lightThemeData = _themeByCode(_lightThemeCode); if (androidNightMode) { _darkThemeData = _themeByCode(_darkThemeCode); } else { _darkThemeData = _lightThemeData; } themeEvent.broadcast(); } Future initTheme() async { androidNightModeDisplay = androidVersion >= 29 || Platform.isIOS; androidNightMode = await method.loadProperty(_nightModePropertyName, "true") == "true"; _lightThemeCode = await method.loadProperty( _lightThemePropertyName, _defaultLightThemeCode); _darkThemeCode = await method.loadProperty(_darkThemePropertyName, _defaultDarkThemeCode); _enableStatusBarColor = await method.loadProperty(_enableStatusBarColorProperty, "false") == "true"; _reloadTheme(); } // 选择主题的对话框 Future _chooseTheme(BuildContext buildContext) { return showDialog( context: buildContext, builder: (BuildContext context) { return StatefulBuilder( builder: (BuildContext context, StateSetter setState) { var list = []; list.addAll(_themePackages.map((e) => SimpleDialogOption( child: Text(e.name()), onPressed: () { Navigator.of(context).pop(e.code()); }, ))); return SimpleDialog( title: Text(tr("settings.theme.choose_theme")), children: list, ); }); }, ); } Future chooseLightTheme(BuildContext buildContext) async { String? theme = await _chooseTheme(buildContext); if (theme != null) { await method.saveProperty(_lightThemePropertyName, theme); _lightThemeCode = theme; _reloadTheme(); } } Future chooseDarkTheme(BuildContext buildContext) async { String? theme = await _chooseTheme(buildContext); if (theme != null) { await method.saveProperty(_darkThemePropertyName, theme); _darkThemeCode = theme; _reloadTheme(); } } Future setAndroidNightMode(bool value) async { await method.saveProperty(_nightModePropertyName, "$value"); androidNightMode = value; _reloadTheme(); } ================================================ FILE: lib/basic/config/ThreeKeepRight.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _propertyName = "threeKeepRight"; late bool _threeKeepRight; bool get threeKeepRight => _threeKeepRight; Future initThreeKeepRight() async { _threeKeepRight = (await method.loadProperty(_propertyName, "false")) == "true"; } Widget threeKeepRightSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("settings.three_keep_right.title")), value: _threeKeepRight, onChanged: (value) async { await method.saveProperty(_propertyName, "$value"); _threeKeepRight = value; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/TimeOffsetHour.dart ================================================ /// 时区设置 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; const _propertyName = "timeOffsetHour"; int _timeOffsetHour = 8; Future initTimeZone() async { _timeOffsetHour = int.parse(await method.loadProperty(_propertyName, "8")); } int currentTimeOffsetHour() { return _timeOffsetHour; } Future _chooseTimeZone(BuildContext context) async { List timeZones = []; for (var i = -12; i <= 12; i++) { var str = i.toString(); if (!str.startsWith("-")) { str = "+" + str; } timeZones.add(str); } String? result = await chooseListDialog(context, tr("settings.time_zone.title"), timeZones); if (result != null) { if (result.startsWith("+")) { result = result.substring(1); } _timeOffsetHour = int.parse(result); await method.saveProperty(_propertyName, result); } } Widget timeZoneSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { var c = "$_timeOffsetHour"; if (!c.startsWith("-")) { c = "+" + c; } return ListTile( title: Text(tr("settings.time_zone.title")), subtitle: Text(c), onTap: () async { await _chooseTimeZone(context); setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/TimeoutLock.dart ================================================ /// 自动清理 import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Method.dart'; // const _lockTimeOutMap = { // "一小时": "${60 * 60}", // "十分钟": "${60 * 10}", // "三分钟": "${60 * 3}", // "一分钟": "${60}", // "十秒": "${10}", // "一秒": "${1}", // "不锁定": "${0}", // }; Map _lockTimeOutMap = {}; late String _lockTimeOutSec; int get timeoutLock => int.tryParse(_lockTimeOutSec) ?? 0; Future initLockTimeOut() async { _lockTimeOutMap.addAll({ tr("settings.timeout_lock.1_hour"): "${60 * 60}", tr("settings.timeout_lock.10_minutes"): "${60 * 10}", tr("settings.timeout_lock.3_minutes"): "${60 * 3}", tr("settings.timeout_lock.1_minute"): "${60}", tr("settings.timeout_lock.10_seconds"): "${10}", tr("settings.timeout_lock.1_second"): "${1}", tr("settings.timeout_lock.no_lock"): "${0}", }); _lockTimeOutSec = await method.loadProperty("lockTimeOutSec", "${0}"); } String _currentLockTimeOutSec() { for (var value in _lockTimeOutMap.entries) { if (value.value == _lockTimeOutSec) { return value.key; } } return "$_lockTimeOutSec seconds"; } Future _chooseLockTimeOutSec(BuildContext context) async { String? choose = await showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: Text(tr("settings.timeout_lock.title")), children: [ ..._lockTimeOutMap.entries.map( (e) => SimpleDialogOption( child: Text(e.key), onPressed: () { Navigator.of(context).pop(e.value); }, ), ), ], ); }, ); if (choose != null) { await method.saveProperty("lockTimeOutSec", choose); _lockTimeOutSec = choose; } } Widget lockTimeOutSecSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.timeout_lock.title")), subtitle: Text(_currentLockTimeOutSec()), onTap: () async { await _chooseLockTimeOutSec(context); setState(() {}); }, ); }, ); } Widget lockTimeOutSecNotice() { return ListTile( subtitle: Text(tr("settings.timeout_lock.notice")), ); } ================================================ FILE: lib/basic/config/UseApiLoadImage.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; late bool _currentUseApiLoadImage; Future initUseApiLoadImage() async { _currentUseApiLoadImage = await method.getUseApiClientLoadImage() == "true"; } String currentUseApiLoadImageName() => _currentUseApiLoadImage ? tr("app.yes") : tr("app.no"); Widget useApiLoadImageSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("net.use_api_load_image")), subtitle: Text(currentUseApiLoadImageName()), value: _currentUseApiLoadImage, onChanged: (bool value) async { _currentUseApiLoadImage = !_currentUseApiLoadImage; await method .setUseApiClientLoadImage(_currentUseApiLoadImage.toString()); setState(() {}); }, ); }, ); } Future chooseUseApiLoadImage(BuildContext context) async { String? choose = await showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: Text(tr("net.use_api_load_image")), children: [ SimpleDialogOption( child: Text(tr("app.yes")), onPressed: () { Navigator.of(context).pop("true"); }, ), SimpleDialogOption( child: Text(tr("app.no")), onPressed: () { Navigator.of(context).pop("false"); }, ), ], ); }, ); if (choose != null) { await method.setUseApiClientLoadImage(choose); _currentUseApiLoadImage = choose == "true"; } } ================================================ FILE: lib/basic/config/UsingRightClickPop.dart ================================================ /// 自动全屏 import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; const _propertyName = "usingRightClickPop"; late bool _usingRightClickPop; Future initUsingRightClickPop() async { _usingRightClickPop = (await method.loadProperty(_propertyName, "false")) == "true"; } bool currentUsingRightClickPop() { return _usingRightClickPop; } Widget usingRightClickPopSetting() { if (!(Platform.isWindows || Platform.isMacOS || Platform.isLinux)) { return Container(); } return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("settings.using_right_click_pop.title")), onChanged: (value) async { await method.saveProperty(_propertyName, "${value ? tr("app.yes") : tr("app.no")}"); _usingRightClickPop = value; setState(() {}); }, value: _usingRightClickPop, ); }, ); } ================================================ FILE: lib/basic/config/Version.dart ================================================ import 'dart:async' show Future; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Cross.dart'; import '../Method.dart'; import 'IgnoreUpgradeConfirm.dart'; const _versionAssets = 'lib/assets/version.txt'; late String _version; String? _latestVersion; String? _latestVersionInfo; String? _downloadUrl; Future initVersion() async { // 当前版本 try { _version = (await rootBundle.loadString(_versionAssets)).trim(); } catch (e) { _version = "dirty"; } } var versionEvent = Event(); String currentVersion() { return _version; } String? latestVersion() { return _latestVersion; } String? latestVersionInfo() { return _latestVersionInfo; } String? downloadUrl() { return _downloadUrl; } Future autoCheckNewVersion() { // if (!isPro) { // return Future.value(); // } return _versionCheck(); } Future manualCheckNewVersion(BuildContext context) async { try { defaultToast(context, "检查更新中"); await _versionCheck(); defaultToast(context, "检查更新成功"); } catch (e) { defaultToast(context, "检查更新失败 : $e"); } } bool dirtyVersion() { return "dirty" == _version; } // maybe exception Future _versionCheck() async { if (!dirtyVersion()) { var config = await method.appConfig(); if (config["latestVersion"] != null) { String latestVersion = config["latestVersion"]; if (latestVersion != _version && _isServerNewer(_version, latestVersion)) { _latestVersion = latestVersion; _latestVersionInfo = config["changeLog"] ?? ""; _downloadUrl = config["downloadUrl"]; } } } // else dirtyVersion versionEvent.broadcast(); } bool _isServerNewer(String current, String latest) { final c = _SemVer.parse(current); final l = _SemVer.parse(latest); if (c == null || l == null) return false; if (l.major != c.major) return l.major > c.major; if (l.minor != c.minor) return l.minor > c.minor; return l.patch > c.patch; } var _display = true; void versionPop(BuildContext context) { final latest = latestVersion(); if (latest == null || !_display) { return; } final force = _isForceUpgrade(currentVersion(), latest); if (!force || ignoreUpgradeConfirm) { return; } _display = false; TopConfirm.topConfirm( context, "发现新版本", force ? "发现新版本 $latest,请立即更新后继续使用" : "发现新版本 $latest,建议更新", force: force, primaryText: "去下载", onPrimary: () async { _openRelease(context); }, ); } class _SemVer { final int major; final int minor; final int patch; const _SemVer(this.major, this.minor, this.patch); static _SemVer? parse(String input) { // todo remove first v if (input.startsWith('v')) { input = input.substring(1); } final regExp = RegExp(r'^(\d+)\.(\d+)\.(\d+)$'); final m = regExp.firstMatch(input); if (m == null) return null; return _SemVer( int.parse(m.group(1)!), int.parse(m.group(2)!), int.parse(m.group(3)!), ); } @override String toString() { return '$major.$minor.$patch'; } } bool _isForceUpgrade(String current, String latest) { print("checking force upgrade..."); print("current version string: $current, latest version string: $latest"); final c = _SemVer.parse(current); final l = _SemVer.parse(latest); print("current version: $c, latest version: $l"); if (c == null || l == null) return false; if (l.major != c.major) return true; if (l.minor != c.minor) return true; return false; } Future _openRelease(BuildContext context) async { try { if (_downloadUrl != null && _downloadUrl!.isNotEmpty) { await openUrl(_downloadUrl!); } } catch (_) { defaultToast(context, "下载失败"); } } class TopConfirm { static topConfirm(BuildContext context, String title, String message, {bool force = false, String primaryText = "朕知道了", Future Function()? onPrimary, Function()? afterIKnown}) { late OverlayEntry overlayEntry; overlayEntry = OverlayEntry(builder: (BuildContext context) { return LayoutBuilder( builder: ( BuildContext context, BoxConstraints constraints, ) { var mq = MediaQuery.of(context).size.width - 30; return Material( color: Colors.transparent, child: Container( width: constraints.maxWidth, height: constraints.maxHeight, decoration: BoxDecoration( color: Colors.black.withOpacity(.35), ), child: Column( children: [ Expanded(child: Container()), Container( width: mq, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), ), child: Column( children: [ Container(height: 30), Text( title, style: const TextStyle( color: Colors.black, fontSize: 28, ), ), Container(height: 15), Text( message, style: const TextStyle( color: Colors.black, fontSize: 16, ), ), Container(height: 25), MaterialButton( elevation: 0, color: Colors.black.withOpacity(.1), onPressed: () { if (onPrimary != null) { onPrimary(); } if (!force) { overlayEntry.remove(); } afterIKnown?.call(); }, child: Text(primaryText), ), Container(height: 30), ], ), ), Expanded(child: Container()), ], ), ), ); }, ); }); final overlay = Overlay.of(context); overlay?.insert(overlayEntry); } } ================================================ FILE: lib/basic/config/VolumeController.dart ================================================ /// 音量键翻页 import 'dart:io'; import 'package:flutter/material.dart'; import '../Method.dart'; import 'package:pikapika/i18.dart'; const _propertyName = "volumeController"; late bool volumeController; Future initVolumeController() async { volumeController = (await method.loadProperty(_propertyName, "false")) == "true"; } Widget volumeControllerSetting() { if (Platform.isAndroid) { return StatefulBuilder(builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: volumeController, title: Text(tr('settings.volume_controller.title')), onChanged: (target) async { await method.saveProperty(_propertyName, "$target"); volumeController = target; setState(() {}); }); }); } return Container(); } ================================================ FILE: lib/basic/config/VolumeNextChapter.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _propertyName = "volumeNextChapter"; late bool _volumeNextChapter; Future initVolumeNextChapter() async { _volumeNextChapter = (await method.loadProperty(_propertyName, "true")) == "true"; } bool volumeNextChapter() { return _volumeNextChapter; } Widget volumeNextChapterSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr("settings.volume_next_chapter.title")), value: _volumeNextChapter, onChanged: (value) async { await method.saveProperty(_propertyName, "$value"); _volumeNextChapter = value; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/WebDav.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import '../Common.dart'; import '../Method.dart'; import 'IsPro.dart'; const _webdavRootPropertyName = "webdavRoot"; const _webdavUsernamePropertyName = "webdavUsername"; const _webdavPasswordPropertyName = "webdavPassword"; const _autoSyncHistoryToWebdavPropertyName = "autoSyncHistoryToWebdav"; const _useLocalFavoritePropertyName = "useLocalFavorite"; const _autoSyncLocalFavoriteToWebdavPropertyName = "autoSyncLocalFavoriteToWebdav"; late String _webdavRoot; late String _webdavUsername; late String _webdavPassword; late bool _autoSyncHistoryToWebdav; late bool _useLocalFavorite; late bool _autoSyncLocalFavoriteToWebdav; String get webdavRoot => _webdavRoot; String get webdavUsername => _webdavUsername; String get webdavPassword => _webdavPassword; bool get useLocalFavorite => _useLocalFavorite; final useLocalFavoriteEvent = Event(); Future initWebDav() async { _webdavRoot = await method.loadProperty( _webdavRootPropertyName, "", ); if (_webdavRoot == "https://your.dav.host/folder") { _webdavRoot = ""; } _webdavUsername = await method.loadProperty( _webdavUsernamePropertyName, "", ); _webdavPassword = await method.loadProperty( _webdavPasswordPropertyName, "", ); _useLocalFavorite = await method.loadProperty( _useLocalFavoritePropertyName, "false", ) == "true"; if (!isPro) { _autoSyncHistoryToWebdav = false; _autoSyncLocalFavoriteToWebdav = false; return; } _autoSyncHistoryToWebdav = await method.loadProperty( _autoSyncHistoryToWebdavPropertyName, "false", ) == "true"; _autoSyncLocalFavoriteToWebdav = await method.loadProperty( _autoSyncLocalFavoriteToWebdavPropertyName, "false", ) == "true"; if (_autoSyncLocalFavoriteToWebdav && _webdavRoot.isNotEmpty) { try { await method.mergeLocalFavoritesFromWebDav( _webdavRoot, _webdavUsername, _webdavPassword, ); } catch (e, s) { print("$e\n$s"); } } } Future setUseLocalFavorite(bool value) async { await method.saveProperty( _useLocalFavoritePropertyName, value ? "true" : "false", ); _useLocalFavorite = value; useLocalFavoriteEvent.broadcast(); } Widget useLocalFavoriteSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _useLocalFavorite, onChanged: (bool value) async { await setUseLocalFavorite(value); setState(() {}); }, title: Text(tr("settings.use_local_favorite")), subtitle: Text(tr("settings.use_local_favorite_desc")), ); }, ); } Future syncLocalFavoriteToWebdav(BuildContext context) async { if (_webdavRoot.isEmpty) { defaultToast(context, tr("settings.webdav.not_set")); return; } try { await method.mergeLocalFavoritesFromWebDav( _webdavRoot, _webdavUsername, _webdavPassword, ); defaultToast(context, tr("settings.local_favorite_sync.sync_success")); } catch (e, s) { print("$e\n$s"); defaultToast(context, tr("settings.local_favorite_sync.sync_failed")); } } Widget localFavoriteSyncAutoTile() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( value: _autoSyncLocalFavoriteToWebdav && isPro, onChanged: isPro ? (bool value) async { await method.saveProperty( _autoSyncLocalFavoriteToWebdavPropertyName, value ? "true" : "false", ); setState(() { _autoSyncLocalFavoriteToWebdav = value; }); if (value) { syncLocalFavoriteToWebdav(context); } } : null, title: Text( tr("settings.local_favorite_sync.auto_sync") + (isPro ? "" : " (${tr('app.pro')})"), style: TextStyle( color: isPro ? null : Colors.grey, ), ), subtitle: Text(tr("settings.local_favorite_sync.auto_sync_desc")), ); }, ); } Widget localFavoriteSyncManualTile() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( onTap: () async { await syncLocalFavoriteToWebdav(context); }, title: Text(tr("settings.local_favorite_sync.manual_sync")), ); }, ); } Future syncWebDavIfAuto(BuildContext context) async { if (_autoSyncHistoryToWebdav) { try { await method.mergeHistoriesFromWebDav( _webdavRoot, _webdavUsername, _webdavPassword, "pk.histories", "all", ); } catch (e, s) { print("$e\n$s"); defaultToast(context, tr("settings.webdav.sync_failed")); } } } Future syncHistoryToWebdav(BuildContext context) async { try { await method.mergeHistoriesFromWebDav( _webdavRoot, _webdavUsername, _webdavPassword, "pk.histories", "all", ); defaultToast(context, tr("settings.webdav.sync_success")); } catch (e, s) { print("$e\n$s"); defaultToast(context, tr("settings.webdav.sync_failed")); } } Future uploadHistoryToWebdav(BuildContext context) async { try { await method.mergeHistoriesFromWebDav( _webdavRoot, _webdavUsername, _webdavPassword, "pk.histories", "up", ); defaultToast(context, tr("settings.webdav.sync_success")); } catch (e, s) { print("$e\n$s"); defaultToast(context, tr("settings.webdav.sync_failed")); } } List webDavSettings(BuildContext context) { return [ // StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text( tr("settings.webdav.path"), ), subtitle: Text(_webdavRoot.isEmpty ? tr("settings.webdav.not_set") : _webdavRoot), onTap: () async { String? input = await displayTextInputDialog( context, src: _webdavRoot, title: tr("settings.webdav.path"), hint: tr("settings.webdav.path_hint"), ); if (input != null) { await method.saveProperty(_webdavRootPropertyName, input); setState(() { _webdavRoot = input == "https://your.dav.host/folder" ? "" : input; }); } }); }, ), // StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text( tr("settings.webdav.username"), ), subtitle: Text(_webdavUsername), onTap: () async { String? input = await displayTextInputDialog( context, src: _webdavUsername, title: tr("settings.webdav.username"), hint: tr("settings.webdav.username_hint"), ); if (input != null) { await method.saveProperty(_webdavUsernamePropertyName, input); setState(() { _webdavUsername = input; }); } }); }, ), // StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text( tr("settings.webdav.password"), ), subtitle: Text(_webdavPassword), onTap: () async { String? input = await displayTextInputDialog( context, src: _webdavPassword, title: tr("settings.webdav.password"), hint: tr("settings.webdav.password_hint"), ); if (input != null) { await method.saveProperty(_webdavPasswordPropertyName, input); setState(() { _webdavPassword = input; }); } }); }, ), // ListTile( title: Text(tr('settings.history_sync')), dense: true, ), // StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text( tr("settings.webdav.auto_sync_history_to_webdav") + (isPro ? "" : "(${tr("app.pro")})"), style: TextStyle( color: !isPro ? Colors.grey : null, ), ), subtitle: Text( _autoSyncHistoryToWebdav ? tr("app.yes") : tr("app.no"), style: TextStyle( color: !isPro ? Colors.grey : null, ), ), onTap: () async { if (!isPro) { return; } String? result = await chooseListDialog( context, tr("settings.webdav.auto_sync_history_to_webdav"), [tr("app.yes"), tr("app.no")]); if (result != null) { var target = result == tr("app.yes"); await method.saveProperty( _autoSyncHistoryToWebdavPropertyName, "$target"); _autoSyncHistoryToWebdav = target; } setState(() {}); }, ); }, ), // ListTile( title: Text(tr("settings.webdav.sync_history_to_webdav")), onTap: () async { await syncHistoryToWebdav(context); }), // ListTile( title: Text(tr("settings.webdav.upload_history_to_webdav")), subtitle: Text(tr("settings.webdav.upload_history_to_webdav_desc")), onTap: () async { await uploadHistoryToWebdav(context); }), // const Divider(), ListTile( title: Text(tr('settings.local_favorite_sync_title')), dense: true, ), localFavoriteSyncAutoTile(), localFavoriteSyncManualTile(), ]; } ================================================ FILE: lib/basic/config/WebToonScrollMode.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; enum WebToonScrollMode { IMAGE, SCREEN, // Distance } const _propertyName = "webToonScrollMode"; WebToonScrollMode _webToonScrollMode = WebToonScrollMode.IMAGE; Future initWebToonScrollMode() async { var value = await method.loadProperty(_propertyName, "0"); if (value == "1") { _webToonScrollMode = WebToonScrollMode.SCREEN; } else { _webToonScrollMode = WebToonScrollMode.IMAGE; } } WebToonScrollMode currentWebToonScrollMode() => _webToonScrollMode; String currentWebToonScrollModeName() => _webToonScrollMode == WebToonScrollMode.SCREEN ? tr("settings.web_toon_scroll_mode.screen") : tr("settings.web_toon_scroll_mode.image"); Future chooseWebToonScrollMode(BuildContext context) async { var result = await showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: Text(tr("settings.web_toon_scroll_mode.choose")), children: [ SimpleDialogOption( onPressed: () { Navigator.pop(context, WebToonScrollMode.IMAGE); }, child: Text(tr("settings.web_toon_scroll_mode.image")), ), SimpleDialogOption( onPressed: () { Navigator.pop(context, WebToonScrollMode.SCREEN); }, child: Text(tr("settings.web_toon_scroll_mode.screen")), ), ], ); }, ); if (result != null) { await method.saveProperty( _propertyName, result == WebToonScrollMode.SCREEN ? "1" : "0", ); _webToonScrollMode = result; } } Widget webToonScrollModeSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr("settings.web_toon_scroll_mode.title")), subtitle: Text(currentWebToonScrollModeName()), onTap: () async { await chooseWebToonScrollMode(context); setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/WillPopNotice.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../Method.dart'; const _propertyName = "willPopNotice"; late bool _willPopNotice; Future initWillPopNotice() async { _willPopNotice = (await method.loadProperty(_propertyName, "false")) == "true"; } bool willPopNotice() { return _willPopNotice; } Widget willPopNoticeSetting() { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return SwitchListTile( title: Text(tr('settings.will_pop_notice')), value: _willPopNotice, onChanged: (value) async { await method.saveProperty(_propertyName, "$value"); _willPopNotice = value; setState(() {}); }, ); }, ); } ================================================ FILE: lib/basic/config/i18n.dart ================================================ import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:pikapika/i18b.dart'; import 'package:flutter/material.dart'; Widget languageListTile() { if (Platform.isIOS || Platform.isAndroid) { } else { return Container(); } return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return ListTile( title: Text(tr('language.title')), subtitle: Text(tr('language.name')), onTap: () async { var choose = await showDialog( context: context, builder: (context) { return AlertDialog( title: Text(tr('language.title')), content: Column( children: [ ListTile( title: const Text("English - United States"), onTap: () { Navigator.pop(context, const Locale('en', 'US')); }, ), ListTile( title: const Text("简体中文 - 中国大陆"), onTap: () { Navigator.pop(context, const Locale('zh', 'CN')); }, ), ListTile( title: const Text("繁體中文 - 中國台灣"), onTap: () { Navigator.pop(context, const Locale('zh', 'TW')); }, ), ListTile( title: const Text("日本語 - 日本"), onTap: () { Navigator.pop(context, const Locale('ja', 'JP')); }, ), ListTile( title: const Text("한국어 - 대한민국"), onTap: () { Navigator.pop(context, const Locale('ko', 'KR')); }, ), ], ), ); }); if (choose != null) { setLocale(context, choose); } }, ); }, ); } ================================================ FILE: lib/basic/config/passed.dart ================================================ import 'package:pikapika/basic/Method.dart'; const _propertyName = "passed"; late bool _passed; Future initPassed() async { _passed = (await method.loadProperty(_propertyName, "false")) == "true"; } bool currentPassed() { return _passed; } Future firstPassed() async { await method.saveProperty(_propertyName, "true"); } ================================================ FILE: lib/basic/connect.dart ================================================ import 'dart:io'; import 'package:flutter/services.dart'; const MethodChannel _networkChannel = MethodChannel('network'); Future isMobileNetwork() async { if (!Platform.isAndroid && !Platform.isIOS) { return false; } final result = await _networkChannel.invokeMethod('getIsMobile'); return result ?? false; } Future checkConnectivity() async { final isMobile = await isMobileNetwork(); if (isMobile) { print('使用移动网络'); } else { print('非移动网络'); } } ================================================ FILE: lib/basic/define.dart ================================================ import 'package:flutter/material.dart'; const supportedLocales = [Locale('en', 'US'), Locale("zh", "CN"), Locale('zh', 'TW'), Locale('ja', 'JP'), Locale('ko', 'KR')]; const fallbackLocale = Locale('en', 'US'); const translationsPath = 'lib/assets/translations'; ================================================ FILE: lib/basic/enum/ErrorTypes.dart ================================================ const ERROR_TYPE_NETWORK = "NETWORK_ERROR"; const ERROR_TYPE_PERMISSION = "PERMISSION_ERROR"; const ERROR_TYPE_TIME = "TIME_ERROR"; const ERROR_TYPE_UNDER_REVIEW = "UNDER_VIEW_ERROR"; // 错误的类型, 方便照展示和谐的提示 String errorType(String error) { // EXCEPTION // Get "https://picaapi.picacomic.com/categories": net/http: TLS handshake timeout // Get "https://picaapi.picacomic.com/comics?c=%E9%95%B7%E7%AF%87&s=ua&page=1": proxyconnect tcp: dial tcp 192.168.123.217:1080: connect: connection refused // Get "https://picaapi.picacomic.com/comics?c=%E5%85%A8%E5%BD%A9&s=ua&page=1": context deadline exceeded (Client.Timeout exceeded while awaiting headers) if (error.contains("timeout") || error.contains("connection refused") || error.contains("deadline") || error.contains("connection abort") || error.contains("certificate") || error.contains("x509") || error.contains("ssl")) { return ERROR_TYPE_NETWORK; } if (error.contains("permission denied")) { return ERROR_TYPE_PERMISSION; } if (error.contains("time is not synchronize")) { return ERROR_TYPE_TIME; } if (error.contains("under review")) { return ERROR_TYPE_UNDER_REVIEW; } return ""; } ================================================ FILE: lib/basic/enum/Sort.dart ================================================ /// 官方提供的排序方式 import 'package:flutter/material.dart'; const SORT_DEFAULT = "ua"; const SORT_TIME_NEWEST = "dd"; const SORT_TIME_OLDEST = "da"; const SORT_LIKE_MOST = "ld"; const SORT_GIVE_MOST = "vd"; const LABEL_DEFAULT = '默认排序'; const LABEL_TIME_NEWEST = "时间最新"; const LABEL_TIME_OLDEST = "时间最久"; const LABEL_LIKE_MOST = "点赞最多"; const LABEL_GIVE_MOST = "查看最多"; class _Sort { final String code; final String label; _Sort.of({ required this.code, required this.label, }); } final sortList = [ _Sort.of(code: SORT_DEFAULT, label: LABEL_DEFAULT), _Sort.of(code: SORT_TIME_NEWEST, label: LABEL_TIME_NEWEST), _Sort.of(code: SORT_TIME_OLDEST, label: LABEL_TIME_OLDEST), _Sort.of(code: SORT_LIKE_MOST, label: LABEL_LIKE_MOST), _Sort.of(code: SORT_GIVE_MOST, label: LABEL_GIVE_MOST), ]; List> items = sortList .map((e) => DropdownMenuItem( value: e.code, child: Text(e.label), )) .toList(); final collSortList = [ _Sort.of(code: SORT_TIME_NEWEST, label: LABEL_TIME_NEWEST), _Sort.of(code: SORT_TIME_OLDEST, label: LABEL_TIME_OLDEST), ]; List> collItems = collSortList .map((e) => DropdownMenuItem( value: e.code, child: Text(e.label), )) .toList(); ================================================ FILE: lib/basic/store/Categories.dart ================================================ /// 全局配置文件, 项目启动时加载 // 数据缓存 var storedCategories = []; ================================================ FILE: lib/i18.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart' as el; import 'package:flutter/services.dart'; Map translations = {}; Future loadTranslations() async { String data = await rootBundle.loadString('lib/assets/translations/zh-CN.json'); Map jsonData = json.decode(data); putMap("", jsonData); } void putMap(String prefix, Map map) { for (String key in map.keys) { if (map[key] is Map) { putMap("$prefix$key.", map[key] as Map); } else if (map[key] is String) { translations["$prefix$key"] = map[key] as String; } else { throw Exception("Unsupported type for key: $prefix$key"); } } } String tr(String key) { if (Platform.isIOS || Platform.isAndroid) { return el.tr(key); } return translations[key] ?? key; } ================================================ FILE: lib/i18b.dart ================================================ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; void setLocale(BuildContext context, Locale locale) { context.setLocale(locale); } ================================================ FILE: lib/main.dart ================================================ import 'dart:io'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/screens/InitScreen.dart'; import 'package:pikapika/basic/Navigator.dart'; import 'package:pikapika/screens/components/MouseAndTouchScrollBehavior.dart'; import 'basic/config/Themes.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:easy_localization/easy_localization.dart'; import 'i18.dart' as i18; import 'basic/define.dart'; main() async { WidgetsFlutterBinding.ensureInitialized(); if (Platform.isAndroid || Platform.isIOS) { await EasyLocalization.ensureInitialized(); runApp( EasyLocalization( supportedLocales: supportedLocales, path: translationsPath, fallbackLocale: fallbackLocale, child: const PikapikaApp()), ); } else { await i18.loadTranslations(); runApp(const PikapikaApp()); } } class PikapikaApp extends StatefulWidget { const PikapikaApp({Key? key}) : super(key: key); @override State createState() => _PikapikaAppState(); } class _PikapikaAppState extends State { @override void initState() { themeEvent.subscribe(_onChangeTheme); super.initState(); } @override void dispose() { themeEvent.unsubscribe(_onChangeTheme); super.dispose(); } void _onChangeTheme(EventArgs? args) { setState(() {}); } @override Widget build(BuildContext context) { if ((Platform.isAndroid || Platform.isIOS)) { return MaterialApp( scrollBehavior: mouseAndTouchScrollBehavior, theme: currentLightThemeData(), darkTheme: currentDarkThemeData(), navigatorObservers: [navigatorObserver, routeObserver], home: const InitScreen(), localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, locale: context.locale, ); } return MaterialApp( scrollBehavior: mouseAndTouchScrollBehavior, theme: currentLightThemeData(), darkTheme: currentDarkThemeData(), navigatorObservers: [navigatorObserver, routeObserver], home: const InitScreen(), ); } } ================================================ FILE: lib/main_desktop.dart ================================================ import 'main.dart' as original_main; // This file is the default main entry-point for go-flutter application. void main() { original_main.main(); } ================================================ FILE: lib/screens/AboutScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:pikapika/basic/Cross.dart'; import 'package:pikapika/basic/config/Version.dart'; import 'package:pikapika/screens/components/Badge.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; // 关于 class AboutScreen extends StatefulWidget { const AboutScreen({Key? key}) : super(key: key); @override State createState() => _AboutScreenState(); } class _AboutScreenState extends State { @override void initState() { versionEvent.subscribe(_onVersion); super.initState(); } @override void dispose() { versionEvent.unsubscribe(_onVersion); super.dispose(); } void _onVersion(dynamic a) { setState(() {}); } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { var size = MediaQuery.of(context).size; var min = size.width < size.height ? size.width : size.height; var _currentVersion = currentVersion(); var _latestVersion = latestVersion(); var _latestVersionInfo = latestVersionInfo(); var _dirty = dirtyVersion(); return Scaffold( appBar: AppBar( title: Text(tr("screen.about.title")), ), body: PikaListView( children: [ Container(height: 20), SizedBox( width: min / 2, height: min / 2, child: Center( child: // isPro ? SvgPicture.asset( 'lib/assets/github.svg', width: min / 3, height: min / 3, color: Colors.grey.shade500, ) // : SizedBox( // width: min / 3, // height: min / 3, // ) , ), ), Container(height: 20), const Divider(), Container( padding: const EdgeInsets.only(left: 20, right: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( tr("screen.about.version") + " : $_currentVersion", style: const TextStyle( height: 1.3, ), ), Text.rich( TextSpan( children: [ TextSpan(text: tr("screen.about.check_update") + " : "), ...(_dirty ? _buildDirty() : _buildNewVersion(_latestVersion)), ], ), ), _buildNewVersionInfo(_latestVersionInfo), ], ), ), const Divider(), Container( padding: const EdgeInsets.all(20), child: SelectableText( tr("screen.about.tips"), style: const TextStyle( height: 1.3, ), ), ), const Divider(), ], ), ); } List _buildNewVersion(String? latestVersion) { // if (!isPro) { // return [ // TextSpan( // text: tr("screen.about.download_new_version"), // ) // ]; // } if (latestVersion != null) { return [ WidgetSpan( child: Badged( child: Container( padding: const EdgeInsets.only(right: 12), child: Text( latestVersion, style: const TextStyle(height: 1.3), ), ), badge: "1", ), ), const TextSpan(text: " "), TextSpan( text: "去下载", style: TextStyle( height: 1.3, color: Theme.of(context).colorScheme.primary, ), recognizer: TapGestureRecognizer() ..onTap = _openRelease, ), ]; } return [ TextSpan( text: tr("screen.about.no_new_version"), style: const TextStyle(height: 1.3)), WidgetSpan( alignment: PlaceholderAlignment.middle, child: Container( padding: const EdgeInsets.all(4), margin: const EdgeInsets.only(left: 3, right: 3), decoration: const BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(20)), ), ), ), TextSpan( text: tr("screen.about.check_update"), style: TextStyle( height: 1.3, color: Theme.of(context).colorScheme.primary, ), recognizer: TapGestureRecognizer() ..onTap = () => manualCheckNewVersion(context), ), ]; } List _buildDirty() { return [ TextSpan( text: tr("screen.about.download_release_version"), style: TextStyle( height: 1.3, color: Theme.of(context).colorScheme.primary, ), recognizer: TapGestureRecognizer()..onTap = _openRelease, ) ]; } Future _openRelease() async { var url = downloadUrl(); if (url != null && url.isNotEmpty) { await openUrl(url); } } Widget _buildNewVersionInfo(String? latestVersionInfo) { // if (!isPro) { // return const Text(""); // } if (latestVersionInfo != null) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Divider(), Text(tr("screen.about.update_content") + ":"), Container( padding: const EdgeInsets.all(15), child: Text( latestVersionInfo, style: const TextStyle(), ), ), ], ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Divider(), Container( padding: const EdgeInsets.all(15), child: Text.rich( TextSpan( text: tr("screen.about.go_to_release_repository"), style: TextStyle( height: 1.3, color: Theme.of(context).colorScheme.primary, ), recognizer: TapGestureRecognizer() ..onTap = _openRelease, ), ), ), ], ); } } ================================================ FILE: lib/screens/AccessKeyReplaceScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/ContentLoading.dart'; import '../basic/config/IsPro.dart'; class AccessKeyReplaceScreen extends StatefulWidget { final String accessKey; const AccessKeyReplaceScreen({Key? key, required this.accessKey}) : super(key: key); @override State createState() => _AccessKeyReplaceScreenState(); } class _AccessKeyReplaceScreenState extends State { var _loading = false; var _message = ""; var _success = false; _set() async { setState(() { _loading = true; }); try { await method.setPatAccessKey(widget.accessKey); await reloadIsPro(); _success = true; } catch (e) { _message = tr("app.error") + " : $e"; } finally { setState(() { _loading = false; }); } } Widget _content() { if (_loading) { return ContentLoading(label: tr('app.loading')); } if (_success) { return Text(tr('app.pat.success')); } return Column( children: [ Expanded(child: Container()), Text(widget.accessKey), Text(_message), Container( height: 10, ), MaterialButton( color: Colors.grey, onPressed: _set, child: Text(tr("app.confirm")), ), Expanded(child: Container()), ], ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr("screen.access_key_replace.title")), ), body: Center( child: _content(), ), ); } } ================================================ FILE: lib/screens/AccountScreen.dart ================================================ import 'dart:async'; import 'package:pikapika/i18.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/config/IsPro.dart'; import 'package:pikapika/basic/enum/ErrorTypes.dart'; import 'package:pikapika/screens/RegisterScreen.dart'; import 'package:pikapika/screens/components/NetworkSetting.dart'; import 'package:pikapika/screens/components/RecommendLinksPanel.dart'; import '../basic/config/IconLoading.dart'; import '../basic/config/Version.dart'; import 'AppScreen.dart'; import 'DownloadListScreen.dart'; import 'ForgotPasswordScreen.dart'; import 'components/ContentLoading.dart'; import 'components/ListView.dart'; // 账户设置 class AccountScreen extends StatefulWidget { const AccountScreen({Key? key}) : super(key: key); @override _AccountScreenState createState() => _AccountScreenState(); } class _AccountScreenState extends State { late bool _logging = false; late String _username = ""; late String _password = ""; late StreamSubscription _linkSubscription; late int _versionClick = 0; @override void initState() { _linkSubscription = linkSubscript(context); _loadProperties(); super.initState(); Future.delayed(Duration.zero, () async { versionPop(context); versionEvent.subscribe(_versionSub); }); } @override void dispose() { _linkSubscription.cancel(); versionEvent.unsubscribe(_versionSub); super.dispose(); } _versionSub(_) { versionPop(context); } Future _loadProperties() async { var username = await method.getUsername(); var password = await method.getPassword(); setState(() { _username = username; _password = password; }); } @override Widget build(BuildContext context) { if (_logging) { return _buildLogging(); } return _buildGui(); } Widget _buildLogging() { return Scaffold( body: ContentLoading(label: tr('app.loading')), ); } Widget _buildGui() { return Scaffold( appBar: AppBar( title: Text(tr('screen.account.title')), actions: [ SizedBox( width: 80, child: IconButton( onPressed: () { setState(() { _versionClick++; }); }, icon: Text(currentVersion()), ), ), IconButton( onPressed: _toDownloadList, icon: const Icon(Icons.download_rounded), ), IconButton( onPressed: _logIn, icon: const Icon(Icons.save), ), ], ), body: PikaListView( children: [ ListTile( title: Text(tr("screen.account.username")), subtitle: Text( _username == "" ? tr("screen.account.not_set") : _username), onTap: () async { String? input = await displayTextInputDialog( context, src: _username, title: tr("screen.account.username"), hint: tr("screen.account.username_hint"), ); if (input != null) { await method.setUsername(input); setState(() { _username = input; }); } }, ), ListTile( title: Text(tr("screen.account.password")), subtitle: Text( _password == "" ? tr("screen.account.not_set") : '\u2022' * 10), onTap: () async { String? input = await displayTextInputDialog( context, src: _password, title: tr("screen.account.password"), hint: tr("screen.account.password_hint"), isPasswd: true, ); if (input != null) { await method.setPassword(input); setState(() { _password = input; }); } }, ), const NetworkSetting(), ..._versionClick >= 7 ? [ Container( padding: const EdgeInsets.all(15), child: Text.rich(TextSpan( text: tr("screen.account.no_account_register"), style: TextStyle( color: Theme.of(context).colorScheme.secondary, decoration: TextDecoration.underline, ), recognizer: TapGestureRecognizer() ..onTap = () => Navigator.push( context, mixRoute( builder: (BuildContext context) => const RegisterScreen()), ).then((value) => _loadProperties()), )), ), ] : [], Container( padding: const EdgeInsets.all(15), child: Text.rich(TextSpan( text: tr("screen.account.password_reset"), style: TextStyle( color: Theme.of(context).colorScheme.secondary, decoration: TextDecoration.underline, ), recognizer: TapGestureRecognizer() ..onTap = () => Navigator.push( context, mixRoute( builder: (BuildContext context) => const ForgotPasswordScreen()), ).then((value) => _loadProperties()), )), ), const RecommendLinksPanel( padding: EdgeInsets.fromLTRB(16, 0, 16, 16), ), ], ), ); } _logIn() async { setState(() { _logging = true; }); try { await method.login(); await reloadIsPro(); Navigator.pushReplacement( context, mixRoute(builder: (context) => const AppScreen()), ); } catch (e, s) { print("$e\n$s"); setState(() { _logging = false; }); var message = tr("screen.account.check_username_password_or_network"); switch (errorType("$e")) { case ERROR_TYPE_NETWORK: message = tr("screen.account.network_不通"); break; case ERROR_TYPE_TIME: message = tr("screen.account.check_device_time"); break; } if ("$e".contains("email") && "$e".contains("password")) { message = tr("screen.account.username_or_password_error"); } alertDialog( context, tr("screen.account.login_failed"), "$message\n$e", ); } } _toDownloadList() { Navigator.push( context, mixRoute(builder: (context) => const DownloadListScreen()), ); } } ================================================ FILE: lib/screens/AppScreen.dart ================================================ import 'dart:async'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; import 'package:pikapika/basic/config/Version.dart'; import 'package:pikapika/basic/config/WillPopNotice.dart'; import 'package:pikapika/screens/components/Badge.dart'; import 'package:pikapika/screens/components/TimeoutLock.dart'; import '../basic/Common.dart'; import 'CategoriesScreen.dart'; import 'SpaceScreen.dart'; // MAIN UI 底部导航栏 class AppScreen extends StatefulWidget { const AppScreen({Key? key}) : super(key: key); @override State createState() => _AppScreenState(); } class _AppScreenState extends State { late StreamSubscription _linkSubscription; @override void initState() { versionEvent.subscribe(_onVersion); _linkSubscription = linkSubscript(context); super.initState(); Future.delayed(Duration.zero, () async { versionPop(context); versionEvent.subscribe(_versionSub); }); } @override void dispose() { versionEvent.unsubscribe(_onVersion); _linkSubscription.cancel(); versionEvent.unsubscribe(_versionSub); super.dispose(); } _versionSub(_) { versionPop(context); } void _onVersion(dynamic a) { setState(() {}); } static const List _widgetOptions = [ CategoriesScreen(), SpaceScreen(), ]; late int _selectedIndex = 0; void _onItemTapped(int index) { setState(() { _selectedIndex = index; }); } @override Widget build(BuildContext context) { final body = Scaffold( body: IndexedStack( index: _selectedIndex, children: _widgetOptions, ), bottomNavigationBar: BottomNavigationBar( items: [ BottomNavigationBarItem( icon: const Icon(Icons.public), label: tr('app.categories'), ), BottomNavigationBarItem( icon: Badged( child: const Icon(Icons.face), badge: latestVersion() == null ? null : "1", ), label: tr('app.my'), ), ], currentIndex: _selectedIndex, iconSize: 20, selectedFontSize: 12, unselectedFontSize: 12, onTap: _onItemTapped, ), ); return TimeoutLock(child: willPop(body)); } int _noticeTime = 0; Widget willPop(Scaffold body) { return WillPopScope( child: body, onWillPop: () async { if (willPopNotice()) { final now = DateTime.now().millisecondsSinceEpoch; if (_noticeTime + 3000 > now) { return true; } else { _noticeTime = now; showToast( tr("screen.app.will_pop_notice"), context: context, position: StyledToastPosition.center, animation: StyledToastAnimation.scale, reverseAnimation: StyledToastAnimation.fade, duration: const Duration(seconds: 3), animDuration: const Duration(milliseconds: 300), curve: Curves.elasticOut, reverseCurve: Curves.linear, ); return false; } } return true; }, ); } } ================================================ FILE: lib/screens/CategoriesScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import 'SearchAuthorScreen.dart'; import 'components/flutter_search_bar.dart' as fsb; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/config/HiddenSubIcon.dart'; import 'package:pikapika/basic/config/ShadowCategoriesEvent.dart'; import 'package:pikapika/basic/config/ShadowCategoriesMode.dart'; import 'package:pikapika/basic/store/Categories.dart'; import 'package:pikapika/basic/config/ShadowCategories.dart'; import 'package:pikapika/screens/ComicCollectionsScreen.dart'; import 'package:pikapika/screens/RankingsScreen.dart'; import 'package:pikapika/screens/SearchScreen.dart'; import 'package:pikapika/screens/components/ContentError.dart'; import 'package:pikapika/basic/Method.dart'; import '../basic/config/Address.dart'; import '../basic/config/CategoriesColumnCount.dart'; import '../basic/config/CategoriesSort.dart'; import '../basic/config/IconLoading.dart'; import 'ComicSubscribesScreen.dart'; import 'ComicsScreen.dart'; import 'GamesScreen.dart'; import 'RandomComicsScreen.dart'; import 'components/Common.dart'; import 'components/ContentLoading.dart'; import 'components/Images.dart'; import 'components/ListView.dart'; // 分类 class CategoriesScreen extends StatefulWidget { const CategoriesScreen({Key? key}) : super(key: key); @override State createState() => _CategoriesScreenState(); } class _CategoriesScreenState extends State { late final fsb.SearchBar _searchBar = fsb.SearchBar( hintText: tr('screen.categories.search_hint'), inBar: false, setState: setState, onSubmitted: (value) { if (value.isNotEmpty) { Navigator.push( context, mixRoute( builder: (context) => SearchScreen(keyword: value), ), ); } }, buildDefaultAppBar: (BuildContext context) { return AppBar( title: Text(tr('app.categories')), actions: [ if (!hiddenSubIcon) const IntoComicSubscribesScreenButton(), commonPopMenu(context), addressPopMenu(context), if (!hiddenSubIcon) const ComicSearchAuthorScreenButton(), _searchBar.getSearchAction(context), ], ); }, ); late Future> _categoriesFuture = _fetch(); Future> _fetch() async { List categories = await method.categories(); storedCategories = []; for (var element in categories) { if (!element.isWeb) { storedCategories.add(element.title); } } return categories; } void _reloadCategories() { setState(() { this._categoriesFuture = _fetch(); }); } @override void initState() { shadowCategoriesEvent.subscribe(_onShadowChange); categoriesColumnCountEvent.subscribe(_setState); categoriesSortEvent.subscribe(_onShadowChange); hiddenSubIconEvent.subscribe(_setState); super.initState(); } @override void dispose() { shadowCategoriesEvent.unsubscribe(_onShadowChange); categoriesColumnCountEvent.unsubscribe(_setState); categoriesSortEvent.unsubscribe(_onShadowChange); hiddenSubIconEvent.unsubscribe(_setState); super.dispose(); } void _onShadowChange(EventArgs? args) { _reloadCategories(); } _setState(_) { setState(() {}); } @override Widget build(BuildContext context) { var theme = Theme.of(context); var themeBackground = theme.scaffoldBackgroundColor; var shadeBackground = Color.fromARGB( 0x11, 255 - themeBackground.red, 255 - themeBackground.green, 255 - themeBackground.blue, ); return Scaffold( appBar: _searchBar.build(context), body: Container( color: shadeBackground, child: FutureBuilder( future: _categoriesFuture, builder: ((BuildContext context, AsyncSnapshot> snapshot) { if (snapshot.hasError) { return ContentError( error: snapshot.error, stackTrace: snapshot.stackTrace, onRefresh: () async { _reloadCategories(); }, ); } if (snapshot.connectionState != ConnectionState.done) { return ContentLoading(label: tr('app.loading')); } // late double blockSize; late double imageSize; late double imageRs; if (categoriesColumnCount == 0) { var size = MediaQuery.of(context).size; var min = size.width < size.height ? size.width : size.height; blockSize = (min ~/ 3).floorToDouble(); } else { var size = MediaQuery.of(context).size; var min = size.width; blockSize = (min ~/ categoriesColumnCount).floorToDouble(); } imageSize = blockSize - 15; imageRs = imageSize / 10; List items = []; // items.addAll(_buildChannels(imageSize)); items.addAll(_buildCategories(snapshot.data!, imageSize)); var names = items.map((e) => e.title).toList(); var sort = getCategoriesSort(); items.sort((a, b) { var aIndex = sort.indexOf(a.title); var bIndex = sort.indexOf(b.title); if (aIndex == bIndex) { aIndex = names.indexOf(a.title); bIndex = names.indexOf(b.title); } if (aIndex == -1) { return 1; } else if (bIndex == -1) { return -1; } else { return aIndex - bIndex; } }); List wrapItems = _wrapItems(items, blockSize, imageRs); return PikaListView( children: [ Container(height: 20), Wrap( runSpacing: 20, alignment: WrapAlignment.spaceAround, children: wrapItems, ), Container(height: 20), ], ); }), ), ), ); } List _wrapItems( List items, double blockSize, double imageRs, ) { List list = []; append(Widget widget, String title, Function() onTap) { list.add( GestureDetector( onTap: onTap, child: SizedBox( width: blockSize, child: Column( children: [ Card( elevation: .5, child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(imageRs)), child: widget, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(imageRs)), ), ), Container(height: 5), Center( child: Text(title), ), ], ), ), ), ); } for (var value in items) { append(value.icon, value.title, value.onTap); } return list; } List _buildCategories( List cList, double imageSize, ) { List items = []; items.add(CategoriesItem( buildSvg('lib/assets/books.svg', imageSize, imageSize, margin: 20), tr('categories.all'), () => _navigateToCategory(null), )); items.add(CategoriesItem( Icon( Icons.recommend_outlined, size: imageSize, color: Colors.grey, ), tr('categories.recommend'), () { Navigator.push( context, mixRoute( builder: (context) => const ComicCollectionsScreen(), ), ); }, )); for (var i = 0; i < cList.length; i++) { var c = cList[i]; if (c.isWeb) continue; switch (currentShadowCategoriesMode()) { case ShadowCategoriesMode.BLACK_LIST: if (shadowCategories.contains(c.title)) continue; break; case ShadowCategoriesMode.WHITE_LIST: if (!shadowCategories.contains(c.title)) continue; break; } items.add(CategoriesItem( RemoteImage( fileServer: c.thumb.fileServer, path: c.thumb.path, width: imageSize, height: imageSize, ), c.title, () => _navigateToCategory(c.title), )); } return items; } List _buildChannels(double imageSize) { List items = []; items.add(CategoriesItem( buildSvg('lib/assets/rankings.svg', imageSize, imageSize, margin: 20, color: Colors.red.shade700), tr('categories.rankings'), () { Navigator.push( context, mixRoute(builder: (context) => const RankingsScreen()), ); }, )); items.add(CategoriesItem( buildSvg('lib/assets/random.svg', imageSize, imageSize, margin: 20, color: Colors.orangeAccent.shade700), tr('categories.random'), () { Navigator.push( context, mixRoute(builder: (context) => const RandomComicsScreen()), ); }, )); items.add(CategoriesItem( buildSvg('lib/assets/gamepad.svg', imageSize, imageSize, margin: 20, color: Colors.blue.shade500), tr('categories.game'), () { Navigator.push( context, mixRoute(builder: (context) => const GamesScreen()), ); }, )); return items; } void _navigateToCategory(String? categoryTitle) { Navigator.push( context, mixRoute( builder: (context) => ComicsScreen(category: categoryTitle), ), ); } } class CategoriesItem { final Widget icon; final String title; final Function() onTap; const CategoriesItem( this.icon, this.title, this.onTap, ); } ================================================ FILE: lib/screens/CategoriesSortScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/ContentError.dart'; import 'package:pikapika/screens/components/ListView.dart'; import '../basic/Entities.dart'; import '../basic/config/CategoriesColumnCount.dart'; import '../basic/config/CategoriesSort.dart'; import 'CategoriesScreen.dart'; import 'components/Images.dart'; class CategoriesSortScreen extends StatefulWidget { const CategoriesSortScreen({Key? key}) : super(key: key); @override _CategoriesSortScreenState createState() => _CategoriesSortScreenState(); } class _CategoriesSortScreenState extends State { late Key _key = UniqueKey(); late Future> _future = method.categories(); _reload() { setState(() { _key = UniqueKey(); _future = method.categories(); }); } @override Widget build(BuildContext context) { return FutureBuilder( key: _key, future: _future, builder: (BuildContext context, AsyncSnapshot> snapshot) { if (snapshot.hasError) { return Scaffold( appBar: AppBar( title: Text(tr('screen.categories_sort.title')), ), body: ContentError( error: snapshot.error, stackTrace: snapshot.stackTrace, onRefresh: () async { _reload(); }, ), ); } if (snapshot.connectionState != ConnectionState.done) { return Scaffold( appBar: AppBar( title: Text(tr('screen.categories_sort.title')), ), body: const Center( child: CircularProgressIndicator(), ), ); } return CategoriesSortPanel(snapshot.requireData); }, ); } } class CategoriesSortPanel extends StatefulWidget { final List requireData; const CategoriesSortPanel(this.requireData, {Key? key}) : super(key: key); @override _CategoriesSortPanelState createState() => _CategoriesSortPanelState(); } class _CategoriesSortPanelState extends State { final List _categoriesSort = []; _switch(String value) { setState(() { if (_categoriesSort.contains(value)) { _categoriesSort.remove(value); } else { _categoriesSort.add(value); } }); } @override Widget build(BuildContext context) { // late double blockSize; late double imageSize; late double imageRs; if (categoriesColumnCount == 0) { var size = MediaQuery.of(context).size; var min = size.width < size.height ? size.width : size.height; blockSize = (min ~/ 3).floorToDouble(); } else { var size = MediaQuery.of(context).size; var min = size.width; blockSize = (min ~/ categoriesColumnCount).floorToDouble(); } imageSize = blockSize - 15; imageRs = imageSize / 10; List items = []; // items.addAll(_buildChannels(imageSize)); items.addAll(_buildCategories(widget.requireData, imageSize)); var names = items.map((e) => e.title).toList(); var sort = getCategoriesSort(); items.sort((a, b) { var aIndex = sort.indexOf(a.title); var bIndex = sort.indexOf(b.title); if (aIndex == bIndex) { aIndex = names.indexOf(a.title); bIndex = names.indexOf(b.title); } if (aIndex == -1) { return 1; } else if (bIndex == -1) { return -1; } else { return aIndex - bIndex; } }); List wrapItems = _wrapItems(items, blockSize, imageRs, imageSize); // return Scaffold( appBar: AppBar( title: Text(tr('screen.categories_sort.title')), actions: [ _saveIcon(), ], ), body: PikaListView( children: [ Container(height: 20), Wrap( runSpacing: 20, alignment: WrapAlignment.spaceAround, children: wrapItems, ), Container(height: 20), ], ), ); } List _wrapItems( List items, double blockSize, double imageRs, double imageSize, ) { List list = []; append(Widget widget, String title, Function() onTap) { list.add( GestureDetector( onTap: onTap, child: SizedBox( width: blockSize, child: Column( children: [ Stack( children: [ Card( elevation: .5, child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(imageRs)), child: widget, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(imageRs)), ), ), if (!_categoriesSort.contains(title)) Container( width: imageSize, height: imageSize, color: Colors.black.withOpacity(.6), margin: const EdgeInsets.all(4.0), ), if (_categoriesSort.contains(title)) Container( width: imageSize, height: imageSize, color: Colors.black.withOpacity(.2), margin: const EdgeInsets.all(4.0), ), if (_categoriesSort.contains(title)) Container( color: Colors.black.withOpacity(.2), padding: const EdgeInsets.all(10), child: Text( "${_categoriesSort.indexOf(title) + 1}", style: const TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, ), ), ), ], ), Container(height: 5), Center( child: Text(title), ), ], ), ), ), ); } for (var value in items) { append(value.icon, value.title, value.onTap); } return list; } List _buildCategories( List cList, double imageSize, ) { List items = []; items.add(CategoriesItem( buildSvg('lib/assets/books.svg', imageSize, imageSize, margin: 20), tr('categories.all'), () => _switch(tr('categories.all')), )); items.add(CategoriesItem( Icon( Icons.recommend_outlined, size: imageSize, color: Colors.grey, ), tr('screen.categories.recommend'), () => _switch(tr('screen.categories.recommend')), )); for (var i = 0; i < cList.length; i++) { var c = cList[i]; if (c.isWeb) continue; items.add(CategoriesItem( RemoteImage( fileServer: c.thumb.fileServer, path: c.thumb.path, width: imageSize, height: imageSize, ), c.title, () => _switch(c.title), )); } return items; } List _buildChannels(double imageSize) { List items = []; items.add(CategoriesItem( buildSvg('lib/assets/rankings.svg', imageSize, imageSize, margin: 20, color: Colors.red.shade700), tr('categories.rankings'), () => _switch(tr('categories.rankings')), )); items.add(CategoriesItem( buildSvg('lib/assets/random.svg', imageSize, imageSize, margin: 20, color: Colors.orangeAccent.shade700), tr('categories.random'), () => _switch(tr('categories.random')), )); items.add(CategoriesItem( buildSvg('lib/assets/gamepad.svg', imageSize, imageSize, margin: 20, color: Colors.blue.shade500), tr('categories.game'), () => _switch(tr('categories.game')), )); return items; } Widget _saveIcon() { return IconButton( onPressed: () async { await saveCategoriesSort(_categoriesSort); Navigator.of(context).pop(); }, icon: const Icon(Icons.save), ); } } ================================================ FILE: lib/screens/CleanScreen.dart ================================================ import 'dart:async'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Channels.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/FitButton.dart'; import 'components/ContentLoading.dart'; import 'components/RightClickPop.dart'; // 清理 class CleanScreen extends StatefulWidget { const CleanScreen({Key? key}) : super(key: key); @override State createState() => _CleanScreenState(); } class _CleanScreenState extends State { late bool _cleaning = false; late String _cleaningMessage = tr('screen.clean.cleaning'); late String _cleanResult = ""; @override void initState() { registerEvent(_onMessageChange, "EXPORT"); super.initState(); } @override void dispose() { unregisterEvent(_onMessageChange); super.dispose(); } void _onMessageChange(String event) { setState(() { _cleaningMessage = event; }); } @override Widget build(BuildContext context){ return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { if (_cleaning) { return Scaffold( body: ContentLoading(label: _cleaningMessage), ); } return Scaffold( appBar: AppBar( title: Text(tr('screen.clean.title')), ), body: ListView( children: [ Container( padding: const EdgeInsets.all(8), child: _cleanResult != "" ? Text(_cleanResult) : Container(), ), SizedBox( height: 50, child: FitButton( text: tr('screen.clean.clean_network_cache'), onPressed: () { processCleanAction(method.cleanNetworkCache); }, ), ), SizedBox( height: 50, child: FitButton( text: tr('screen.clean.clean_image_cache'), onPressed: () { processCleanAction(method.cleanImageCache); }, ), ), SizedBox( height: 50, child: FitButton( text: tr('screen.clean.clean_all_cache'), onPressed: () { processCleanAction(method.clean); }, ), ), ], ), ); } Future processCleanAction(Future Function() action) async { try { setState(() { _cleaning = true; }); await action(); setState(() { _cleanResult = tr('screen.clean.clean_success'); }); } catch (e) { setState(() { _cleanResult = tr('screen.clean.clean_failed') + " $e"; }); } finally { setState(() { _cleaning = false; }); } } } ================================================ FILE: lib/screens/CloseAppScreen.dart ================================================ import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; class CloseAppScreen extends StatelessWidget { const CloseAppScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr('screen.close_app.title')), ), body: Center( child: ElevatedButton( onPressed: () { // 关闭应用 exit(0); }, child: Text(tr('screen.close_app.close_app')), ), ), ); } } ================================================ FILE: lib/screens/ComicCollectionsScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/ComicList.dart'; import 'package:pikapika/screens/components/ContentBuilder.dart'; import 'package:pikapika/screens/components/ContentMessage.dart'; import 'components/RightClickPop.dart'; class ComicCollectionsScreen extends StatefulWidget { const ComicCollectionsScreen({Key? key}) : super(key: key); @override State createState() => _ComicCollectionsScreenState(); } class _ComicCollectionsScreenState extends State { late Future> _future; late Key _key = UniqueKey(); @override void initState() { _future = method.collections(); super.initState(); } @override void dispose() { // TODO: implement dispose super.dispose(); } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(tr('categories.recommend'))), body: ContentBuilder( key: _key, future: _future, onRefresh: () async { setState(() { _future = method.collections(); _key = UniqueKey(); }); }, successBuilder: ( BuildContext context, AsyncSnapshot> snapshot, ) { final collection = snapshot.requireData; if (collection.isEmpty) { return ContentMessage( message: tr('screen.comic_collections.no_resource'), icon: Icons.no_sim_outlined, onRefresh: () async { setState(() { _future = method.collections(); }); }, ); } final ThemeData theme = Theme.of(context); final AppBarTheme appBarTheme = AppBarTheme.of(context); return DefaultTabController( length: collection.length, child: Scaffold( appBar: PreferredSizeContainer( color: appBarTheme.backgroundColor, child: TabBar( indicatorColor: theme.dividerColor, tabs: collection .map((e) => Tab( text: e.title.indexOf("推薦") > 0 ? e.title.substring(0, e.title.indexOf("推薦")) : e.title)) .toList(), ), ), body: TabBarView( children: collection.map((e) => ComicList(e.comics)).toList(), ), ), ); }, ), ); } } class PreferredSizeContainer extends StatelessWidget implements PreferredSizeWidget { final PreferredSizeWidget child; final Color? color; const PreferredSizeContainer({ required this.child, this.color, Key? key, }) : super(key: key); @override Size get preferredSize => child.preferredSize; @override Widget build(BuildContext context) { return Container( color: color, child: child, ); } } ================================================ FILE: lib/screens/ComicInfoScreen.dart ================================================ import 'dart:async'; import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Cross.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/Navigator.dart'; import 'package:pikapika/basic/config/IgnoreInfoHistory.dart'; import 'package:pikapika/screens/ComicsScreen.dart'; import 'package:pikapika/screens/components/CommentMainType.dart'; import 'package:pikapika/screens/components/ItemBuilder.dart'; import 'package:pikapika/screens/components/Recommendation.dart'; import '../basic/config/HiddenSubIcon.dart'; import '../basic/config/IconLoading.dart'; import 'ComicReaderScreen.dart'; import 'DownloadConfirmScreen.dart'; import 'components/ComicDescriptionCard.dart'; import 'components/ComicInfoCard.dart'; import 'components/ComicTagsCard.dart'; import 'components/CommentList.dart'; import 'components/CommonData.dart'; import 'components/ContentError.dart'; import 'components/ContentLoading.dart'; import 'components/ContinueReadButton.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; // 漫画详情 class ComicInfoScreen extends StatefulWidget { final String comicId; final bool holdPkz; const ComicInfoScreen({Key? key, required this.comicId, this.holdPkz = false}) : super(key: key); @override State createState() => _ComicInfoScreenState(); } class _ComicInfoScreenState extends State with RouteAware { late var _tabIndex = 0; late Future _comicFuture = _loadComic(); late Key _comicFutureKey = UniqueKey(); late Future _viewFuture = _loadViewLog(); late Future _subscribedFuture = _loadSubscribed(); late Future> _epListFuture = _loadEps(); StreamSubscription? _linkSubscription; Future _loadComic() async { return await method .comicInfo(widget.comicId, currentIgnoreInfoHistory()) .then((value) async { subscribedViewed(widget.comicId); return value; }); } Future> _loadEps() async { List eps = []; var page = 0; late EpPage rsp; do { rsp = await method.comicEpPage(widget.comicId, ++page); eps.addAll(rsp.docs); } while (rsp.page < rsp.pages); return eps; } Future _loadViewLog() { return method.loadView(widget.comicId); } Future _loadSubscribed() { return method.loadSubscribed(widget.comicId); } @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe(this, ModalRoute.of(context)!); } @override void didPopNext() { setState(() { _viewFuture = _loadViewLog(); }); } @override void initState() { if (widget.holdPkz) { _linkSubscription = linkSubscript(context); } hiddenSubIconEvent.subscribe(_setState); super.initState(); } @override void dispose() { _linkSubscription?.cancel(); routeObserver.unsubscribe(this); hiddenSubIconEvent.unsubscribe(_setState); super.dispose(); } void _setState(dynamic args) { setState(() {}); } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return FutureBuilder( key: _comicFutureKey, future: _comicFuture, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return Scaffold( appBar: AppBar(), body: ContentError( error: snapshot.error, stackTrace: snapshot.stackTrace, onRefresh: () async { setState(() { _comicFuture = _loadComic(); _comicFutureKey = UniqueKey(); }); }, ), ); } if (snapshot.connectionState != ConnectionState.done) { return Scaffold( appBar: AppBar(), body: ContentLoading(label: tr('app.loading')), ); } var _comicInfo = snapshot.data!; var theme = Theme.of(context); var _tabs = [ Tab(text: tr('screen.comic_info.chapter') + ' (${_comicInfo.epsCount})'), Tab(text: tr('screen.comic_info.comment') + ' (${_comicInfo.commentsCount})'), Tab(text: tr('screen.comic_info.recommend')), ]; var _views = [ _buildEpWrap(_epListFuture, _comicInfo), CommentList(CommentMainType.COMIC, _comicInfo.id), Recommendation(comicId: _comicInfo.id), ]; return DefaultTabController( length: _tabs.length, child: Scaffold( appBar: AppBar( title: Text(_comicInfo.title), actions: [ _buildSubscribeAction(_subscribedFuture, _comicInfo), _buildDownloadAction(_epListFuture, _comicInfo), ], ), body: PikaListView( children: [ ComicInfoCard(_comicInfo, linkItem: true), ComicTagsCard(_comicInfo.tags), ComicDescriptionCard(description: _comicInfo.description), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: theme.dividerColor, ), ), ), child: Wrap( alignment: WrapAlignment.spaceBetween, children: [ Text.rich(TextSpan( children: [ WidgetSpan( child: GestureDetector( onTap: () { if (_comicInfo.creator.id != "") { navPushOrReplace( context, (context) => ComicsScreen( creatorId: _comicInfo.creator.id, creatorName: _comicInfo.creator.name, ), ); } }, onLongPress: () { confirmCopy( context, _comicInfo.creator.name, ); }, child: Text( _comicInfo.creator.name, style: const TextStyle( fontSize: 14, color: Colors.grey, ), ), ), ), const TextSpan( text: " ", style: TextStyle( fontSize: 14, color: Colors.grey, ), ), TextSpan( text: "( ${formatTimeToDate(_comicInfo.updatedAt)} )", style: const TextStyle( fontSize: 13, color: Colors.grey, ), ), ], )), GestureDetector( onTap: () { if (_comicInfo.chineseTeam != "") { navPushOrReplace( context, (context) => ComicsScreen( chineseTeam: _comicInfo.chineseTeam, ), ); } }, onLongPress: () { confirmCopy(context, _comicInfo.chineseTeam); }, child: Text( _comicInfo.chineseTeam, style: const TextStyle( fontSize: 13, color: Colors.grey, ), ), ), ], ), ), Container(height: 5), Container( height: 40, color: theme.colorScheme.secondary.withOpacity(.025), child: TabBar( tabs: _tabs, indicatorColor: theme.colorScheme.secondary, labelColor: theme.colorScheme.secondary, onTap: (val) async { setState(() { _tabIndex = val; }); }, ), ), Container(height: 15), _views[_tabIndex], Container(height: 5), ], ), ), ); }, ); } Widget _buildSubscribeAction( Future _subscribedFuture, ComicInfo _comicInfo, ) { if (hiddenSubIcon) { return Container(); } return FutureBuilder( future: _subscribedFuture, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return IconButton( onPressed: () { setState(() { this._subscribedFuture = _loadSubscribed(); }); }, icon: const Icon(Icons.sync_problem), ); } if (snapshot.connectionState != ConnectionState.done) { return IconButton(onPressed: () {}, icon: const Icon(Icons.sync)); } var _subscribed = snapshot.data; return IconButton( onPressed: () async { if (_subscribed == null) { await method.addSubscribed(_comicInfo.id); } else { await method.removeSubscribed(_comicInfo.id); } setState(() { this._subscribedFuture = _loadSubscribed(); }); }, icon: Icon( _subscribed == null ? Icons.notifications_none : Icons.notifications, ), ); }, ); } Widget _buildDownloadAction( Future> _epListFuture, ComicInfo _comicInfo, ) { return FutureBuilder( future: _epListFuture, builder: (BuildContext context, AsyncSnapshot> snapshot) { if (snapshot.hasError) { return IconButton( onPressed: () { setState(() { this._epListFuture = _loadEps(); }); }, icon: const Icon(Icons.sync_problem), ); } if (snapshot.connectionState != ConnectionState.done) { return IconButton(onPressed: () {}, icon: const Icon(Icons.sync)); } var _epList = snapshot.data!; return IconButton( onPressed: () async { Navigator.push( context, mixRoute( builder: (context) => DownloadConfirmScreen( comicInfo: _comicInfo, epList: _epList.reversed.toList(), ), ), ); }, icon: const Icon(Icons.download_rounded), ); }, ); } Widget _buildEpWrap(Future> _epListFuture, ComicInfo _comicInfo) { return ItemBuilder( future: _epListFuture, successBuilder: (BuildContext context, AsyncSnapshot> snapshot) { var _epList = snapshot.data!; return Column( children: [ ContinueReadButton( viewFuture: _viewFuture, onChoose: (int? epOrder, int? pictureRank) { if (epOrder != null && pictureRank != null) { for (var i in _epList) { if (i.order == epOrder) { _push(_comicInfo, _epList, epOrder, pictureRank); return; } } } else { _push( _comicInfo, _epList, _epList.reversed.first.order, null); return; } }, ), Wrap( spacing: 10, runSpacing: 10, alignment: WrapAlignment.spaceAround, children: [ ..._epList.map((e) { return MaterialButton( onPressed: () { _push(_comicInfo, _epList, e.order, null); }, color: Colors.white, child: Text( e.title, style: const TextStyle(color: Colors.black), ), ); }), ], ), ], ); }, onRefresh: () async { setState(() { _epListFuture = _loadEps(); }); }, ); } Future _push(ComicInfo comicInfo, List epList, int order, int? rank) async { if (currentIgnoreInfoHistory()) { await method.comicInfo(widget.comicId, false); } Navigator.push( context, mixRoute( builder: (context) => ComicReaderScreen( comicInfo: comicInfo, epList: epList, currentEpOrder: order, initPicturePosition: rank, ), ), ); } } ================================================ FILE: lib/screens/ComicReaderScreen.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/config/AutoFullScreen.dart'; import 'package:pikapika/basic/config/FullScreenUI.dart'; import 'package:pikapika/basic/config/Quality.dart'; import 'package:pikapika/screens/components/ContentError.dart'; import 'package:pikapika/screens/components/ContentLoading.dart'; import '../basic/config/IconLoading.dart'; import 'DownloadConfirmScreen.dart'; import 'components/ImageReader.dart'; import 'components/RightClickPop.dart'; // 在线阅读漫画 class ComicReaderScreen extends StatefulWidget { final ComicInfo comicInfo; final List epList; final int currentEpOrder; final int? initPicturePosition; late final bool autoFullScreen; ComicReaderScreen({ Key? key, required this.comicInfo, required this.epList, required this.currentEpOrder, this.initPicturePosition, bool? autoFullScreen, }) : super(key: key) { this.autoFullScreen = autoFullScreen ?? currentAutoFullScreen(); } @override State createState() => _ComicReaderScreenState(); } class _ComicReaderScreenState extends State { late Ep _ep; late bool _fullScreen = false; late Future> _future; int? _lastChangeRank; bool _replacement = false; Future> _load() async { if (widget.initPicturePosition == null) { await method.storeViewEp(widget.comicInfo.id, _ep.order, _ep.title, 0); } List list = []; var _needLoadPage = 0; late PicturePage page; do { page = await method.comicPicturePageWithQuality( widget.comicInfo.id, widget.currentEpOrder, ++_needLoadPage, currentQualityCode(), ); list.addAll(page.docs.map((element) => element.media)); } while (page.pages > page.page); if (widget.autoFullScreen) { setState(() { SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: [], ); _fullScreen = true; }); } return list; } Future _onPositionChange(int position) async { _lastChangeRank = position; return method.storeViewEp( widget.comicInfo.id, _ep.order, _ep.title, position); } FutureOr _onChangeEp(int epOrder) { var orderMap = {}; for (var element in widget.epList) { orderMap[element.order] = element; } if (orderMap.containsKey(epOrder)) { _replacement = true; Navigator.of(context).pushReplacement( mixRoute( builder: (context) => ComicReaderScreen( comicInfo: widget.comicInfo, epList: widget.epList, currentEpOrder: epOrder, autoFullScreen: _fullScreen, ), ), ); } } FutureOr _onReloadEp() { _replacement = true; Navigator.of(context).pushReplacement(mixRoute( builder: (context) => ComicReaderScreen( comicInfo: widget.comicInfo, epList: widget.epList, currentEpOrder: widget.currentEpOrder, initPicturePosition: _lastChangeRank ?? widget.initPicturePosition, // maybe null autoFullScreen: _fullScreen, ), )); } FutureOr _onDownload() { Navigator.push( context, mixRoute( builder: (context) => DownloadConfirmScreen( comicInfo: widget.comicInfo, epList: widget.epList.reversed.toList(), ), ), ); } @override void initState() { // EP for (var element in widget.epList) { if (element.order == widget.currentEpOrder) { _ep = element; } } // INIT _future = _load(); super.initState(); } @override void dispose() { if (!_replacement) { switchFullScreenUI(); } super.dispose(); } @override Widget build(BuildContext context){ return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return readerKeyboardHolder(_build(context)); } Widget _build(BuildContext context) { return FutureBuilder( future: _future, builder: (BuildContext context, AsyncSnapshot> snapshot) { if (snapshot.hasError) { return Scaffold( appBar: _fullScreen ? null : AppBar( title: Text("${_ep.title} - ${widget.comicInfo.title}"), ), body: ContentError( error: snapshot.error, stackTrace: snapshot.stackTrace, onRefresh: () async { setState(() { _future = _load(); }); }, ), ); } if (snapshot.connectionState != ConnectionState.done) { return Scaffold( appBar: _fullScreen ? null : AppBar( title: Text("${_ep.title} - ${widget.comicInfo.title}"), ), body: ContentLoading(label: tr('app.loading')), ); } var epNameMap = {}; for (var element in widget.epList) { epNameMap[element.order] = element.title; } return Scaffold( body: ImageReader( ImageReaderStruct( images: snapshot.data! .map((e) => ReaderImageInfo( e.fileServer, e.path, null, null, null, null, null, )) .toList(), fullScreen: _fullScreen, onFullScreenChange: _onFullScreenChange, onPositionChange: _onPositionChange, initPosition: widget.initPicturePosition, epNameMap: epNameMap, epOrder: _ep.order, comicTitle: widget.comicInfo.title, onChangeEp: _onChangeEp, onReloadEp: _onReloadEp, onDownload: _onDownload, ), ), ); }, ); } Future _onFullScreenChange(bool fullScreen) async { setState(() { if (fullScreen) { if (Platform.isAndroid || Platform.isIOS) { SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: [], ); } } else { switchFullScreenUI(); } _fullScreen = fullScreen; }); } } ================================================ FILE: lib/screens/ComicSubscribesScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/ComicList.dart'; import 'package:pikapika/screens/components/CommonData.dart'; import 'package:pikapika/screens/components/ContentBuilder.dart'; import '../basic/config/Address.dart'; import 'components/Badge.dart'; import 'components/Common.dart'; class IntoComicSubscribesScreenButton extends StatefulWidget { const IntoComicSubscribesScreenButton({Key? key}) : super(key: key); @override State createState() => _IntoComicSubscribesScreenButtonState(); } class _IntoComicSubscribesScreenButtonState extends State { @override void initState() { super.initState(); subscribedEvent.subscribe(_setState); _sync(); } @override void dispose() { subscribedEvent.unsubscribe(_setState); super.dispose(); } _setState(_) { setState(() {}); } void _sync() async { await updateSubscribed(); } @override Widget build(BuildContext context) { final count = allSubscribed.values.isEmpty ? 0 : allSubscribed.values .map((e) => e.newEpCount) .reduce((value, element) => value + element); return Badged( badge: count == 0 ? null : count.toString(), child: IconButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => const ComicSubscribesScreen(), ), ); }, icon: const Icon(Icons.alarm), ), ); } } class ComicSubscribesScreen extends StatefulWidget { const ComicSubscribesScreen({Key? key}) : super(key: key); @override State createState() => _ComicSubscribesScreenState(); } class _ComicSubscribesScreenState extends State { @override void initState() { super.initState(); subscribedEvent.subscribe(_setState); } @override void dispose() { subscribedEvent.unsubscribe(_setState); super.dispose(); } void _setState(_) { setState(() {}); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr('screen.comic_subscribes.update_reminder')), actions: [ commonPopMenu(context), addressPopMenu(context), _popMenu(context), ], ), body: _body(context), ); } Widget _body(BuildContext context) { final subs = allSubscribed.values.toList(); List comicList = []; for (var comicSubscribe in subs) { comicList.add(ComicSimple.fromJson(comicSubscribe.toSimpleJson())); } return ComicList(comicList); } } Widget _popMenu(BuildContext context) { return PopupMenuButton( icon: const Icon(Icons.more_vert), itemBuilder: (BuildContext context) => >[ PopupMenuItem( value: 0, child: ListTile( leading: const Icon(Icons.share), title: Text(tr('screen.comic_subscribes.check_update')), ), ), PopupMenuItem( value: 1, child: ListTile( leading: const Icon(Icons.image_search), title: Text(tr('screen.comic_subscribes.cancel_all_update_reminder')), ), ), ], onSelected: (int value) { switch (value) { case 0: updateSubscribedForce(); break; case 1: removeAllSubscribed(); break; } }, ); } ================================================ FILE: lib/screens/ComicsScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'components/flutter_search_bar.dart' as fsb; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/config/PagerAction.dart'; import 'package:pikapika/basic/config/ShadowCategories.dart'; import 'package:pikapika/basic/config/ShadowCategoriesMode.dart'; import 'package:pikapika/basic/store/Categories.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/ComicList.dart'; import '../basic/Entities.dart'; import '../basic/config/Address.dart'; import '../basic/config/IconLoading.dart'; import 'SearchScreen.dart'; import 'components/ComicPager.dart'; import 'components/Common.dart'; import 'components/GoDownloadSelect.dart'; import 'components/RightClickPop.dart'; // 漫画列表 class ComicsScreen extends StatefulWidget { final String? category; // 指定分类 final String? tag; // 指定标签 final String? creatorId; // 指定上传者 final String? creatorName; // 上传者名称 (仅显示) final String? chineseTeam; const ComicsScreen({ Key? key, this.category, this.tag, this.creatorId, this.creatorName, this.chineseTeam, }) : super(key: key); @override State createState() => _ComicsScreenState(); } class _ComicsScreenState extends State { late final _comicListController = ComicListController(); late final fsb.SearchBar _categorySearchBar = fsb.SearchBar( hintText: tr('screen.comics.search_hint') + ' - ${categoryTitle(widget.category)}', inBar: false, setState: setState, onSubmitted: (value) { if (value.isNotEmpty) { Navigator.push( context, mixRoute( builder: (context) => SearchScreen(keyword: value, category: widget.category), ), ); } }, buildDefaultAppBar: (BuildContext context) { return AppBar( title: Text(categoryTitle(widget.category)), actions: [ commonPopMenu( context, setState: setState, comicListController: _comicListController, ), addressPopMenu(context), _chooseCategoryAction(), _categorySearchBar.getSearchAction(context), ], ); }, ); Widget _chooseCategoryAction() => IconButton( onPressed: () async { String? category = await chooseListDialog(context, tr('screen.comics.choose_category'), [ categoryTitle(null), ...filteredList( storedCategories, (c) { switch (currentShadowCategoriesMode()) { case ShadowCategoriesMode.BLACK_LIST: if (shadowCategories.contains(c)) return false; break; case ShadowCategoriesMode.WHITE_LIST: if (!shadowCategories.contains(c)) return false; break; } return true; }, ), ]); if (category != null) { if (category == categoryTitle(null)) { category = null; } Navigator.of(context).pushReplacement(mixRoute( builder: (context) { return ComicsScreen( category: category, tag: widget.tag, creatorId: widget.creatorId, creatorName: widget.creatorName, chineseTeam: widget.chineseTeam, ); }, )); } }, icon: const Icon(Icons.category), ); Future _load(String _currentSort, int _currentPage) { if (currentPagerAction() == PagerAction.CONTROLLER && _comicListController.selecting) { setState(() { _comicListController.selecting = false; }); } return method.comics( _currentSort, _currentPage, category: widget.category ?? "", tag: widget.tag ?? "", creatorId: widget.creatorId ?? "", chineseTeam: widget.chineseTeam ?? "", ); } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { PreferredSizeWidget? appBar; if (widget.tag == null && widget.creatorId == null && widget.chineseTeam == null) { // 只有只传分类或不传参数时时才开放搜索 appBar = _categorySearchBar.build(context); } else { var title = ""; if (widget.category != null) { title += "${widget.category} "; } if (widget.tag != null) { title += "${widget.tag} "; } if (widget.creatorName != null) { title += "${widget.creatorName} "; } if (widget.chineseTeam != null) { title += "${widget.chineseTeam} "; } appBar = AppBar( title: Text(title), actions: [ commonPopMenu( context, setState: setState, comicListController: _comicListController, ), addressPopMenu(context), _chooseCategoryAction(), ], ); } if (_comicListController.selecting) { appBar = downAppBar(context, _comicListController, setState); } var a = Scaffold( appBar: appBar, body: ComicPager( fetchPage: _load, comicListController: _comicListController, ), ); return WillPopScope( onWillPop: () async { if (_comicListController.selecting) { setState(() { _comicListController.selecting = false; }); return false; } return true; }, child: a, ); } } ================================================ FILE: lib/screens/CommentScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Entities.dart' as e; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/CommentItem.dart'; import 'package:pikapika/screens/components/CommentMainType.dart'; import 'package:pikapika/screens/components/ContentBuilder.dart'; import 'components/BottomSheetInput.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; class _CommentChildPage extends e.Page { late List docs; _CommentChildPage.ofComic(CommentChildrenPage commentPage) : super.of(commentPage.total, commentPage.limit, commentPage.page, commentPage.pages) { this.docs = commentPage.docs; } _CommentChildPage.ofGame(GameCommentChildrenPage commentPage) : super.of(commentPage.total, commentPage.limit, commentPage.page, commentPage.pages) { this.docs = commentPage.docs; } } class CommentScreen extends StatefulWidget { final CommentMainType mainType; final String mainId; final CommentBase comment; const CommentScreen(this.mainType, this.mainId, this.comment, {Key? key}) : super(key: key); @override State createState() => _CommentScreenState(); } class _CommentScreenState extends State { late int _currentPage = 1; late Future<_CommentChildPage> _future = _loadPage(); late Key _key = UniqueKey(); Future<_CommentChildPage> _loadPage() async { switch (widget.mainType) { case CommentMainType.COMIC: return _CommentChildPage.ofComic(await method.commentChildren( widget.mainId, widget.comment.id, _currentPage, )); case CommentMainType.GAME: return _CommentChildPage.ofGame(await method.gameCommentChildren( widget.mainId, widget.comment.id, _currentPage, )); } } Widget _buildChildrenPager() { return ContentBuilder( key: _key, future: _future, onRefresh: _loadPage, successBuilder: (BuildContext context, AsyncSnapshot<_CommentChildPage> snapshot) { var page = snapshot.data!; return PikaListView( children: [ _buildPrePage(page), ...page.docs.map((e) => _buildComment(e)), _buildNextPage(page), _buildPostComment(), ], ); }, ); } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr('screen.comment.title')), actions: [ IconButton( icon: const Icon(Icons.add_comment), onPressed: _onReply, ), ], ), body: Column( children: [ ComicCommentItem(widget.mainType, widget.mainId, widget.comment), Container( height: 3, color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.05), ), Expanded(child: _buildChildrenPager()) ], ), ); } Widget _buildComment(CommentBase e) { return ComicCommentItem(widget.mainType, widget.mainId, e); } Future _onReply() async { showInputModalBottomSheet( context: context, onSubmitted: (text) async { switch (widget.mainType) { case CommentMainType.COMIC: await method.postChildComment(widget.comment.id, text); break; case CommentMainType.GAME: await method.postGameChildComment(widget.comment.id, text); break; } setState(() { _future = _loadPage(); _key = UniqueKey(); widget.comment.commentsCount++; }); defaultToast(context, tr('screen.comment.success')); }, hintText: tr('screen.comment.hint_text'), ); } Widget _buildPostComment() { return InkWell( onTap: _onReply, child: Container( decoration: BoxDecoration( border: Border( top: BorderSide( width: .25, style: BorderStyle.solid, color: Colors.grey.shade500.withOpacity(.5), ), bottom: BorderSide( width: .25, style: BorderStyle.solid, color: Colors.grey.shade500.withOpacity(.5), ), ), ), padding: const EdgeInsets.all(30), child: Center( child: Text(tr('screen.comment.i_have_something_to_say')), ), ), ); } Widget _buildPrePage(_CommentChildPage page) { if (page.page > 1) { return InkWell( onTap: () { setState(() { _currentPage = page.page - 1; _future = _loadPage(); _key = UniqueKey(); }); }, child: Container( padding: const EdgeInsets.all(30), child: Center( child: Text(tr('app.previous_page')), ), ), ); } return Container(); } Widget _buildNextPage(_CommentChildPage page) { if (page.page < page.pages) { return InkWell( onTap: () { setState(() { _currentPage = page.page + 1; _future = _loadPage(); _key = UniqueKey(); }); }, child: Container( padding: const EdgeInsets.all(30), child: Center( child: Text(tr('app.next_page')), ), ), ); } return Container(); } } ================================================ FILE: lib/screens/DesktopAuthenticationScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Method.dart'; const _key = "desktopAuthPassword"; Future needDesktopAuthentication() async { return await method.loadProperty(_key, "") != ""; } class VerifyPassword extends StatefulWidget { const VerifyPassword({Key? key}) : super(key: key); @override State createState() => _VerifyPasswordState(); } class _VerifyPasswordState extends State { String _password = ""; @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Padding( padding: const EdgeInsets.all(30), child: Column( children: [ Expanded(child: Container()), TextField( decoration: InputDecoration(labelText: tr('screen.desktop_authentication.current_password')), onChanged: (value) { _password = value; }, ), Container(height: 10), ElevatedButton( onPressed: () async { String savedPassword = await method.loadProperty(_key, ""); if (_password == savedPassword) { Navigator.of(context).pop(true); } else { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(tr('screen.desktop_authentication.password_error')))); } }, child: Text(tr('app.confirm')), ), Expanded(child: Container()), ], ), ), ), ); } } class SetPassword extends StatefulWidget { const SetPassword({Key? key}) : super(key: key); @override State createState() => _SetPasswordState(); } class _SetPasswordState extends State { String _password = ""; String _password2 = ""; @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Padding( padding: const EdgeInsets.all(30), child: Column( children: [ Text( tr('screen.desktop_authentication.password_initialization'), style: TextStyle( height: 18, ), ), Container( height: 10, ), TextField( decoration: InputDecoration(labelText: tr('screen.desktop_authentication.password')), onChanged: (value) { _password = value; }, ), Container( height: 10, ), TextField( decoration: InputDecoration(labelText: tr('screen.desktop_authentication.re_enter_password')), onChanged: (value) { _password2 = value; }, ), Container( height: 10, ), Row( children: [ ElevatedButton( onPressed: () async { Navigator.of(context).pop(false); }, child: Text(tr('app.cancel')), ), Container(width: 10), Expanded( child: ElevatedButton( onPressed: () async { if (_password != _password2) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('screen.desktop_authentication.password_mismatch')))); return; } await method.saveProperty(_key, _password); Navigator.of(context).pop(true); }, child: Text(tr('screen.desktop_authentication.set_password')), ), ), ], ), ], ), ), ), ); } } ================================================ FILE: lib/screens/DownloadConfirmScreen.dart ================================================ import 'dart:convert'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/screens/components/ContentLoading.dart'; import 'package:pikapika/basic/Method.dart'; import 'components/ComicInfoCard.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; // 确认下载 class DownloadConfirmScreen extends StatefulWidget { final ComicInfo comicInfo; final List epList; const DownloadConfirmScreen({ Key? key, required this.comicInfo, required this.epList, }) : super(key: key); @override State createState() => _DownloadConfirmScreenState(); } class _DownloadConfirmScreenState extends State { DownloadComic? _task; // 之前的下载任务 final List _taskedEps = []; // 已经下载的EP final List _selectedEps = []; // 选中的EP late Future f = _load(); Future _load() async { _taskedEps.clear(); _task = await method.loadDownloadComic(widget.comicInfo.id); if (_task != null) { var epList = await method.downloadEpList(widget.comicInfo.id); _taskedEps.addAll(epList.map((e) => e.epOrder)); } } void _selectAll() { setState(() { _selectedEps.clear(); for (var element in widget.epList) { if (!_taskedEps.contains(element.order)) { _selectedEps.add(element.order); } } }); } Future _download() async { // 必须选中才能下载 if (_selectedEps.isEmpty) { defaultToast(context, tr('screen.download_confirm.please_select_ep')); return; } // 下载对象 Map create = { "id": widget.comicInfo.id, "createdAt": widget.comicInfo.createdAt, "updatedAt": widget.comicInfo.updatedAt, "title": widget.comicInfo.title, "author": widget.comicInfo.author, "pagesCount": widget.comicInfo.pagesCount, "epsCount": widget.comicInfo.epsCount, "finished": widget.comicInfo.finished, "categories": json.encode(widget.comicInfo.categories), "thumbOriginalName": widget.comicInfo.thumb.originalName, "thumbFileServer": widget.comicInfo.thumb.fileServer, "thumbPath": widget.comicInfo.thumb.path, "description": widget.comicInfo.description, "chineseTeam": widget.comicInfo.chineseTeam, "tags": json.encode(widget.comicInfo.tags), }; // 下载EP列表 List> list = []; for (var element in widget.epList) { if (_selectedEps.contains(element.order)) { list.add({ "comicId": widget.comicInfo.id, "id": element.id, "updatedAt": element.updatedAt, "epOrder": element.order, "title": element.title, }); } } try { // 如果之前下载过就将EP加入下载 // 如果之前没有下载过就创建下载 if (_task != null) { await method.addDownload(create, list); } else { await method.createDownload(create, list); } // 退出 defaultToast(context, tr('screen.download_confirm.already_added_to_download_list')); Navigator.pop(context); } catch (e, s) { defaultToast(context, e.toString()); } } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("下载 - ${widget.comicInfo.title}"), ), body: FutureBuilder( future: f, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { print(snapshot.error); print(snapshot.stackTrace); return const Text('error'); } if (snapshot.connectionState != ConnectionState.done) { return ContentLoading(label: tr('app.loading')); } return PikaListView( children: [ ComicInfoCard(widget.comicInfo), _buildButtons(), Wrap( alignment: WrapAlignment.spaceAround, runSpacing: 10, spacing: 10, children: [ ...widget.epList.map((e) { return Container( padding: const EdgeInsets.all(5), child: MaterialButton( onPressed: () { _clickOfEp(e); }, color: _colorOfEp(e), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ _iconOfEp(e), Container( width: 10, ), Text(e.title, style: const TextStyle(color: Colors.black)), ], ), ), ); }), ], ), ], ); }, ), ); } Widget _buildButtons() { var theme = Theme.of(context); return Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: Colors.grey.shade200, ), ), ), child: Wrap( spacing: 10, runSpacing: 10, alignment: WrapAlignment.spaceAround, children: [ MaterialButton( color: theme.colorScheme.secondary, textColor: Colors.white, onPressed: _selectAll, child: Text(tr('app.select_all')), ), MaterialButton( color: theme.colorScheme.secondary, textColor: Colors.white, onPressed: _download, child: Text(tr('app.confirm_download')), ), ], ), ); } Color _colorOfEp(Ep e) { if (_taskedEps.contains(e.order)) { return Colors.grey.shade300; } if (_selectedEps.contains(e.order)) { return Colors.blueGrey.shade300; } return Colors.grey.shade200; } Icon _iconOfEp(Ep e) { if (_taskedEps.contains(e.order)) { return const Icon(Icons.download_rounded, color: Colors.black); } if (_selectedEps.contains(e.order)) { return const Icon(Icons.check_box, color: Colors.black); } return const Icon(Icons.check_box_outline_blank, color: Colors.black); } void _clickOfEp(Ep e) { if (_taskedEps.contains(e.order)) { return; } if (_selectedEps.contains(e.order)) { setState(() { _selectedEps.remove(e.order); }); } else { setState(() { _selectedEps.add(e.order); }); } } } ================================================ FILE: lib/screens/DownloadExportGroupScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import '../basic/Entities.dart'; import '../basic/Method.dart'; import '../basic/config/IconLoading.dart'; import 'DownloadExportingGroupScreen.dart'; import 'components/ContentLoading.dart'; import 'components/DownloadInfoCard.dart'; import 'components/ListView.dart'; class DownloadExportGroupScreen extends StatefulWidget { const DownloadExportGroupScreen({Key? key}) : super(key: key); @override State createState() => _DownloadExportGroupScreenState(); } class _DownloadExportGroupScreenState extends State { late Future> _f = method.allDownloads(""); @override Widget build(BuildContext context) { return FutureBuilder( future: _f, builder: (BuildContext context, AsyncSnapshot> snapshot) { if (snapshot.connectionState != ConnectionState.done) { return Scaffold( appBar: AppBar( title: Text(tr("screen.download_export_group.title")), ), body: ContentLoading(label: tr("app.loading")), ); } if (snapshot.hasError) { print("${snapshot.error}"); print("${snapshot.stackTrace}"); return Scaffold( appBar: AppBar( title: Text(tr("screen.download_export_group.title")), ), body: Center(child: Text(tr("app.load_failed"))), ); } var data = snapshot.data!; List ws = []; List exportable = []; List exportableIds = []; for (var value in data) { if (!value.deleting && value.downloadFinished) { ws.add(downloadWidget(value)); exportable.add(value); exportableIds.add(value.id); } } return Scaffold( appBar: AppBar( title: Text(tr("screen.download_export_group.title")), actions: [ _selectAllButton(exportableIds), _goToExport(), ], ), body: RefreshIndicator( onRefresh: () async { setState(() { selected.clear(); _f = method.allDownloads(""); }); }, child: PikaListView( children: ws, ), ), ); }, ); } List selected = []; Widget downloadWidget(DownloadComic e) { return InkWell( onTap: () { if (selected.contains(e.id)) { selected.remove(e.id); } else { selected.add(e.id); } setState(() {}); }, child: Stack(children: [ DownloadInfoCard( task: e, ), Row(children: [ Expanded(child: Container()), Padding( padding: const EdgeInsets.all(5), child: Icon( selected.contains(e.id) ? Icons.check_circle_sharp : Icons.circle_outlined, color: Theme .of(context) .colorScheme .secondary, ), ), ]), ]), ); } Widget _selectAllButton(List exportableIds) { return MaterialButton( minWidth: 0, onPressed: () async { setState(() { if (selected.length >= exportableIds.length) { selected.clear(); } else { selected.clear(); selected.addAll(exportableIds); } }); }, child: Column( children: [ Expanded(child: Container()), const Icon( Icons.select_all, size: 18, color: Colors.white, ), Text( tr("app.select_all"), style: TextStyle(fontSize: 14, color: Colors.white), ), Expanded(child: Container()), ], )); } Widget _goToExport() { return MaterialButton( minWidth: 0, onPressed: () async { if (selected.isEmpty) { defaultToast(context, tr("screen.download_export_group.please_select_content")); return; } final exported = await Navigator.of(context).push( mixRoute( builder: (context) => DownloadExportingGroupScreen( idList: selected, ), ), ); }, child: Column( children: [ Expanded(child: Container()), const Icon( Icons.check, size: 18, color: Colors.white, ), Text( tr("app.confirm"), style: TextStyle(fontSize: 14, color: Colors.white), ), Expanded(child: Container()), ], )); } } ================================================ FILE: lib/screens/DownloadExportToFileScreen.dart ================================================ import 'dart:async'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Channels.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/config/ExportRename.dart'; import 'package:pikapika/screens/DownloadExportToSocketScreen.dart'; import '../basic/config/ExportPath.dart'; import '../basic/config/IconLoading.dart'; import '../basic/config/IsPro.dart'; import 'components/ContentError.dart'; import 'components/ContentLoading.dart'; import 'components/DownloadInfoCard.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; // 导出 class DownloadExportToFileScreen extends StatefulWidget { final String comicId; final String comicTitle; const DownloadExportToFileScreen({ required this.comicId, required this.comicTitle, Key? key, }) : super(key: key); @override State createState() => _DownloadExportToFileScreenState(); } class _DownloadExportToFileScreenState extends State { late DownloadComic _task; late Future _future = _load(); late bool exporting = false; late String exportMessage = tr("screen.download_export_group.exporting"); late String exportResult = ""; Future _load() async { _task = (await method.loadDownloadComic(widget.comicId))!; } @override void initState() { registerEvent(_onMessageChange, "EXPORT"); super.initState(); } @override void dispose() { unregisterEvent(_onMessageChange); super.dispose(); } void _onMessageChange(event) { setState(() { exportMessage = event; }); } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: !exporting, ); } Widget buildScreen(BuildContext context) { if (exporting) { return Scaffold( body: ContentLoading(label: exportMessage), ); } return Scaffold( appBar: AppBar( title: Text(tr("screen.download_export_to_file.title") + " - " + widget.comicTitle), ), body: FutureBuilder( future: _future, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return ContentError( error: snapshot.error, stackTrace: snapshot.stackTrace, onRefresh: () async { setState(() { _future = _load(); }); }); } if (snapshot.connectionState != ConnectionState.done) { return ContentLoading(label: tr("app.loading")); } return PikaListView( children: [ DownloadInfoCard(task: _task), Container( padding: const EdgeInsets.all(8), child: exportResult != "" ? Text(exportResult) : Container(), ), displayExportPathInfo(), Container(height: 15), _exportPkzButton(), Container(height: 10), _exportPkiButton(), Container(height: 10), _exportHtmlZipButton(), Container(height: 10), _exportToHtmlJPEGButton(), Container(height: 10), _exportToHtmlPdfButton(), Container(height: 10), _exportToHtmlPdfFolderButton(), Container(height: 10), _exportToHtmlEpubButton(), Container(height: 10), _exportToJPEGSZIPButton(), Container(height: 10), _exportToHtmlJPEGNotDownOverButton(), Container(height: 10), _exportComicDownloadToCbzsZipButton(), Container(height: 10), MaterialButton( onPressed: () async { Navigator.of(context).push( mixRoute( builder: (context) => DownloadExportToSocketScreen( task: _task, comicId: widget.comicId, comicTitle: widget.comicTitle, ), ), ); }, child: _buildButtonInner(tr("screen.download_export_to_file.transfer_to_other_device")), ), Container(height: 40), ], ); }, ), ); } Widget _exportPkzButton() { return MaterialButton( onPressed: () async { var name = ""; if (currentExportRename()) { var rename = await inputString( context, tr("screen.download_export_to_file.input_save_name"), defaultValue: _task.title, ); if (rename != null && rename.isNotEmpty) { name = rename; } else { return; } } else { if (!await confirmDialog( context, tr('screen.download_export_to_file.export_confirm'), tr('screen.download_export_to_file.export_to_pkz_title') + showExportPath(), )) { return; } } try { setState(() { exporting = true; }); await method.exportComicDownloadToPkz( [widget.comicId], await attachExportPath(), name, ); setState(() { exportResult = tr("screen.download_export_group.export_success"); }); } catch (e) { setState(() { exportResult = tr("screen.download_export_group.export_failed") + " $e"; }); } finally { setState(() { exporting = false; }); } }, child: _buildButtonInner( tr("screen.download_export_group.export_to_pkz_title") + showExportPath(), ), ); } Widget _exportPkiButton() { return MaterialButton( onPressed: () async { var name = ""; if (currentExportRename()) { var rename = await inputString( context, tr("screen.download_export_to_file.input_save_name"), defaultValue: _task.title, ); if (rename != null && rename.isNotEmpty) { name = rename; } else { return; } } else { if (!await confirmDialog( context, tr('screen.download_export_to_file.export_confirm'), tr('screen.download_export_to_file.export_to_pki_title') + showExportPath(), )) { return; } } try { setState(() { exporting = true; }); await method.exportComicDownloadToPki( widget.comicId, await attachExportPath(), name, ); setState(() { exportResult = tr("screen.download_export_group.export_success"); }); } catch (e) { setState(() { exportResult = tr("screen.download_export_group.export_failed") + " $e"; }); } finally { setState(() { exporting = false; }); } }, child: _buildButtonInner( tr("screen.download_export_group.export_to_pki_title") + showExportPath(), ), ); } Widget _exportHtmlZipButton() { return MaterialButton( onPressed: () async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } var name = ""; if (currentExportRename()) { var rename = await inputString( context, tr("screen.download_export_to_file.input_save_name"), defaultValue: _task.title, ); if (rename != null && rename.isNotEmpty) { name = rename; } else { return; } } else { if (!await confirmDialog( context, tr('screen.download_export_to_file.export_confirm'), tr('screen.download_export_to_file.export_to_zip_title') + showExportPath(), )) { return; } } try { setState(() { exporting = true; }); await method.exportComicDownload( widget.comicId, await attachExportPath(), name, ); setState(() { exportResult = tr("screen.download_export_group.export_success"); }); } catch (e) { setState(() { exportResult = tr("screen.download_export_group.export_failed") + " $e"; }); } finally { setState(() { exporting = false; }); } }, child: _buildButtonInner( tr("screen.download_export_to_file.export_to_zip_desc") + showExportPath(), ), ); } Widget _exportToHtmlJPEGButton() { return MaterialButton( onPressed: () async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } var name = ""; if (currentExportRename()) { var rename = await inputString( context, tr("screen.download_export_to_file.input_save_name"), defaultValue: _task.title, ); if (rename != null && rename.isNotEmpty) { name = rename; } else { return; } } else { if (!await confirmDialog( context, tr('screen.download_export_to_file.export_confirm'), tr('screen.download_export_to_file.export_to_jpeg_zip_title') + showExportPath(), )) { return; } } try { setState(() { exporting = true; }); await method.exportComicDownloadToJPG( widget.comicId, await attachExportPath(), name, ); setState(() { exportResult = tr("screen.download_export_group.export_success"); }); } catch (e) { setState(() { exportResult = tr("screen.download_export_group.export_failed") + " $e"; }); } finally { setState(() { exporting = false; }); } }, child: _buildButtonInner(tr("screen.download_export_group.export_to_jpeg_zip_title") + showExportPath()), ); } Widget _exportToHtmlPdfButton() { return MaterialButton( onPressed: () async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } var name = ""; if (currentExportRename()) { var rename = await inputString( context, tr("screen.download_export_to_file.input_save_name"), defaultValue: _task.title, ); if (rename != null && rename.isNotEmpty) { name = rename; } else { return; } } else { if (!await confirmDialog( context, tr('screen.download_export_to_file.export_confirm'), tr('screen.download_export_to_file.export_to_pdf_title') + showExportPath(), )) { return; } } try { setState(() { exporting = true; }); await method.exportComicDownloadToPDF( widget.comicId, await attachExportPath(), name, ); setState(() { exportResult = tr("screen.download_export_group.export_success"); }); } catch (e) { setState(() { exportResult = tr("screen.download_export_group.export_failed") + " $e"; }); } finally { setState(() { exporting = false; }); } }, child: _buildButtonInner(tr("screen.download_export_group.export_to_pdf_title") + showExportPath()), ); } Widget _exportToHtmlEpubButton() { return MaterialButton( onPressed: () async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } var name = ""; if (currentExportRename()) { var rename = await inputString( context, tr("screen.download_export_to_file.input_save_name"), defaultValue: _task.title, ); if (rename != null && rename.isNotEmpty) { name = rename; } else { return; } } else { if (!await confirmDialog( context, tr('screen.download_export_to_file.export_confirm'), tr('screen.download_export_to_file.export_to_epub_title') + showExportPath(), )) { return; } } try { setState(() { exporting = true; }); await method.exportComicDownloadToEpub( widget.comicId, await attachExportPath(), name, ); setState(() { exportResult = tr("screen.download_export_group.export_success"); }); } catch (e) { setState(() { exportResult = tr("screen.download_export_group.export_failed") + " $e"; }); } finally { setState(() { exporting = false; }); } }, child: _buildButtonInner(tr("screen.download_export_group.export_to_epub_title") + showExportPath()), ); } Widget _exportToHtmlPdfFolderButton() { return MaterialButton( onPressed: () async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } var name = ""; if (currentExportRename()) { var rename = await inputString( context, tr("screen.download_export_to_file.input_save_name"), defaultValue: _task.title, ); if (rename != null && rename.isNotEmpty) { name = rename; } else { return; } } else { if (!await confirmDialog( context, tr('screen.download_export_to_file.export_confirm'), tr('screen.download_export_to_file.export_to_pdf_folder_title') + showExportPath(), )) { return; } } try { setState(() { exporting = true; }); await method.exportComicDownloadToPDFFolder( widget.comicId, await attachExportPath(), name, ); setState(() { exportResult = tr("screen.download_export_group.export_success"); }); } catch (e) { setState(() { exportResult = tr("screen.download_export_group.export_failed") + " $e"; }); } finally { setState(() { exporting = false; }); } }, child: _buildButtonInner(tr("screen.download_export_group.export_to_pdf_folder_title") + showExportPath()), ); } Widget _exportToJPEGSZIPButton() { return MaterialButton( onPressed: () async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } var name = ""; if (currentExportRename()) { var rename = await inputString( context, tr("screen.download_export_to_file.input_save_name"), defaultValue: _task.title, ); if (rename != null && rename.isNotEmpty) { name = rename; } else { return; } } else { if (!await confirmDialog( context, tr('screen.download_export_to_file.export_confirm'), tr('screen.download_export_group.export_to_jpeg_zip_title') + showExportPath(), )) { return; } } try { setState(() { exporting = true; }); await method.exportComicDownloadJpegZip( widget.comicId, await attachExportPath(), name, ); setState(() { exportResult = tr("screen.download_export_group.export_success"); }); } catch (e) { setState(() { exportResult = tr("screen.download_export_group.export_failed") + " $e"; }); } finally { setState(() { exporting = false; }); } }, child: _buildButtonInner( tr("screen.download_export_group.export_to_jpeg_zip_title")+ (!isPro ? "\n(${tr('app.pro')})" : ""), ), ); } Widget _exportToHtmlJPEGNotDownOverButton() { return MaterialButton( onPressed: () async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } var name = ""; if (currentExportRename()) { var rename = await inputString( context, tr("screen.download_export_to_file.input_save_name"), defaultValue: _task.title, ); if (rename != null && rename.isNotEmpty) { name = rename; } else { return; } } else { if (!await confirmDialog( context, tr('screen.download_export_to_file.export_confirm'), tr('screen.download_export_group.export_to_jpeg_zip_title_not_down_over') + showExportPath(), )) { return; } } try { setState(() { exporting = true; }); await method.exportComicJpegsEvenNotFinish( widget.comicId, await attachExportPath(), name, ); setState(() { exportResult = tr("screen.download_export_group.export_success"); }); } catch (e) { setState(() { exportResult = tr("screen.download_export_group.export_failed") + " $e"; }); } finally { setState(() { exporting = false; }); } }, child: _buildButtonInner( tr("screen.download_export_group.export_to_jpeg_zip_title_not_down_over") + (!isPro ? "\n(${tr('app.pro')})" : ""), ), ); } Widget _exportComicDownloadToCbzsZipButton() { return MaterialButton( onPressed: () async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } var name = ""; if (currentExportRename()) { var rename = await inputString( context, tr("screen.download_export_to_file.input_save_name"), defaultValue: _task.title, ); if (rename != null && rename.isNotEmpty) { name = rename; } else { return; } } else { if (!await confirmDialog( context, tr('screen.download_export_to_file.export_confirm'), tr('screen.download_export_group.export_to_cbz_title') + showExportPath(), )) { return; } } try { setState(() { exporting = true; }); await method.exportComicDownloadToCbzsZip( widget.comicId, await attachExportPath(), name, ); setState(() { exportResult = tr("screen.download_export_group.export_success"); }); } catch (e) { setState(() { exportResult = tr("screen.download_export_group.export_failed") + " $e"; }); } finally { setState(() { exporting = false; }); } }, child: _buildButtonInner( tr("screen.download_export_to_file.export_to_cbz_desc") + (!isPro ? "\n(${tr('app.pro')})" : ""), ), ); } Widget _buildButtonInner(String text) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Container( width: constraints.maxWidth, padding: const EdgeInsets.all(15), color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.05), child: Text( text, textAlign: TextAlign.center, ), ); }, ); } } ================================================ FILE: lib/screens/DownloadExportToSocketScreen.dart ================================================ import 'dart:async'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Channels.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'components/ContentError.dart'; import 'components/ContentLoading.dart'; import 'components/DownloadInfoCard.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; // 传输到其他设备 class DownloadExportToSocketScreen extends StatefulWidget { final DownloadComic task; final String comicId; final String comicTitle; const DownloadExportToSocketScreen({ required this.task, required this.comicId, required this.comicTitle, Key? key, }) : super(key: key); @override State createState() => _DownloadExportToSocketScreenState(); } class _DownloadExportToSocketScreenState extends State { late Future _future = method.exportComicUsingSocket(widget.comicId); late final Future _ipFuture = method.clientIpSet(); late String exportMessage = ""; @override void initState() { registerEvent(_onMessageChange, "EXPORT"); super.initState(); } @override void dispose() { method.exportComicUsingSocketExit(); unregisterEvent(_onMessageChange); super.dispose(); } void _onMessageChange(event) { if (event is String) { setState(() { exportMessage = event; }); } } @override Widget build(BuildContext context){ return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr("screen.download_export_to_socket.title") + " - " + widget.comicTitle), ), body: FutureBuilder( future: _future, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return ContentError( error: snapshot.error, stackTrace: snapshot.stackTrace, onRefresh: () async { setState(() { _future = method.exportComicUsingSocket(widget.comicId); }); }); } if (snapshot.connectionState != ConnectionState.done) { return ContentLoading(label: tr("screen.download_export_to_socket.loading")); } return PikaListView( children: [ DownloadInfoCard(task: widget.task), Container( padding: const EdgeInsets.all(8), child: Column( children: [ Text( tr("screen.download_export_to_socket.tips")), FutureBuilder( future: _ipFuture, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return Text(tr("screen.download_export_to_socket.get_ip_failed")); } if (snapshot.connectionState != ConnectionState.done) { return Text(tr("screen.download_export_to_socket.getting_ip")); } return Text('${snapshot.data}'); }, ), Text(tr("screen.download_export_to_socket.port") + ':${snapshot.data}'), Text(exportMessage), ], ), ), ], ); }, ), ); } } ================================================ FILE: lib/screens/DownloadExportingGroupScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import '../basic/Channels.dart'; import '../basic/Method.dart'; import '../basic/config/ExportPath.dart'; import '../basic/config/ExportRename.dart'; import '../basic/config/IsPro.dart'; import 'components/ContentLoading.dart'; import 'components/ListView.dart'; class DownloadExportingGroupScreen extends StatefulWidget { final List idList; const DownloadExportingGroupScreen({Key? key, required this.idList}) : super(key: key); @override State createState() => _DownloadExportingGroupScreenState(); } class _DownloadExportingGroupScreenState extends State { bool exporting = false; bool exported = false; bool exportFail = false; dynamic e; String exportMessage = tr("screen.download_export_group.exporting"); @override void initState() { registerEvent(_onMessageChange, "EXPORT"); super.initState(); } @override void dispose() { unregisterEvent(_onMessageChange); super.dispose(); } void _onMessageChange(event) { setState(() { exportMessage = event; }); } Widget _body() { if (exporting) { return ContentLoading(label: exportMessage); } if (exportFail) { return Center(child: Text(tr("screen.download_export_group.export_failed") + "\n$e")); } if (exported) { return Center(child: Text(tr("screen.download_export_group.export_success"))); } return PikaListView( children: [ Container(height: 20), displayExportPathInfo(), Container(height: 20), MaterialButton( onPressed: _exportPkz, child: _buildButtonInner(tr("screen.download_export_group.export_to_pkz")), ), Container(height: 20), MaterialButton( onPressed: _exportPkis, child: _buildButtonInner(tr("screen.download_export_group.export_to_pki")), ), Container(height: 20), MaterialButton( onPressed: _exportZips, child: _buildButtonInner( tr("screen.download_export_group.export_to_zip") + (!isPro ? "\n" + tr("screen.download_export_group.after_power_use") : "")), ), Container(height: 20), MaterialButton( onPressed: _exportToJPEGSZips, child: _buildButtonInner( tr("screen.download_export_group.export_to_jpeg_zip") + (!isPro ? "\n" + tr("screen.download_export_group.after_power_use") : ""), ), ), Container(height: 20), MaterialButton( onPressed: _exportToJPEGSFolders, child: _buildButtonInner( tr("screen.download_export_group.export_to_jpeg_folder") + (!isPro ? "\n" + tr("screen.download_export_group.after_power_use") : ""), ), ), Container(height: 20), MaterialButton( onPressed: _exportToPdf, child: _buildButtonInner( tr("screen.download_export_group.export_to_pdf") + (!isPro ? "\n" + tr("screen.download_export_group.after_power_use") : ""), ), ), Container(height: 20), MaterialButton( onPressed: _exportToPdfFolder, child: _buildButtonInner( tr("screen.download_export_group.export_to_pdf_folder") + (!isPro ? "\n" + tr("screen.download_export_group.after_power_use") : ""), ), ), Container(height: 20), MaterialButton( onPressed: _exportToEpub, child: _buildButtonInner( tr("screen.download_export_group.export_to_epub") + (!isPro ? "\n" + tr("screen.download_export_group.after_power_use") : ""), ), ), Container(height: 20), MaterialButton( onPressed: _exportComicDownloadToCbzsZip, child: _buildButtonInner( tr("screen.download_export_group.export_to_cbz") + (!isPro ? "\n" + tr("screen.download_export_group.after_power_use") : ""), ), ), Container(height: 20), ], ); } _exportPkz() async { var name = ""; if (currentExportRename()) { var rename = await inputString( context, tr("screen.download_export_group.input_save_name"), defaultValue: "${DateTime.now().millisecondsSinceEpoch}", ); if (rename != null && rename.isNotEmpty) { name = rename; } else { return; } } else { if (!await confirmDialog( context, tr("screen.download_export_group.export_confirm"), tr("screen.download_export_group.export_to_pkz_title") + showExportPath())) { return; } } try { setState(() { exporting = true; }); await method.exportComicDownloadToPkz( widget.idList, await attachExportPath(), name, ); exported = true; } catch (err) { e = err; exportFail = true; } finally { setState(() { exporting = false; }); } } _exportPkis() async { if (!await confirmDialog( context, tr("screen.download_export_group.export_confirm"), tr("screen.download_export_group.export_to_pki_title") + showExportPath())) { return; } try { setState(() { exporting = true; }); await method.exportAnyComicDownloadsToPki( widget.idList, await attachExportPath(), ); exported = true; } catch (err) { e = err; exportFail = true; } finally { setState(() { exporting = false; }); } } _exportZips() async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } if (!await confirmDialog( context, tr("screen.download_export_group.export_confirm"), tr("screen.download_export_group.export_to_zip_title") + showExportPath())) { return; } try { setState(() { exporting = true; }); await method.exportAnyComicDownloadsToZip( widget.idList, await attachExportPath(), ); exported = true; } catch (err) { e = err; exportFail = true; } finally { setState(() { exporting = false; }); } } _exportToJPEGSZips() async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } if (!await confirmDialog( context, tr("screen.download_export_group.export_confirm"), tr("screen.download_export_group.export_to_jpeg_zip_title") + showExportPath())) { return; } try { setState(() { exporting = true; }); final path = await attachExportPath(); for (var id in widget.idList) { await method.exportComicDownloadJpegZip( id, path, "", ); } exported = true; } catch (err) { e = err; exportFail = true; } finally { setState(() { exporting = false; }); } } _exportToJPEGSFolders() async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } if (!await confirmDialog( context, tr("screen.download_export_group.export_confirm"), tr("screen.download_export_group.export_to_jpeg_folder_title") + showExportPath())) { return; } try { setState(() { exporting = true; }); final path = await attachExportPath(); for (var id in widget.idList) { await method.exportComicDownloadToJPG( id, path, "", ); } exported = true; } catch (err) { e = err; exportFail = true; } finally { setState(() { exporting = false; }); } } _exportToPdf() async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } if (!await confirmDialog( context, tr("screen.download_export_group.export_confirm"), tr("screen.download_export_group.export_to_pdf_title") + showExportPath())) { return; } try { setState(() { exporting = true; }); final path = await attachExportPath(); for (var id in widget.idList) { await method.exportComicDownloadToPDF( id, path, "", ); } exported = true; } catch (err) { e = err; exportFail = true; } finally { setState(() { exporting = false; }); } } _exportToEpub() async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } if (!await confirmDialog( context, tr("screen.download_export_group.export_confirm"), tr("screen.download_export_group.export_to_epub_title") + showExportPath())) { return; } try { setState(() { exporting = true; }); final path = await attachExportPath(); for (var id in widget.idList) { await method.exportComicDownloadToEpub( id, path, "", ); } exported = true; } catch (err) { e = err; exportFail = true; } finally { setState(() { exporting = false; }); } } _exportToPdfFolder() async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } if (!await confirmDialog( context, tr("screen.download_export_group.export_confirm"), tr("screen.download_export_group.export_to_pdf_folder_title") + showExportPath())) { return; } try { setState(() { exporting = true; }); final path = await attachExportPath(); for (var id in widget.idList) { await method.exportComicDownloadToPDFFolder( id, path, "", ); } exported = true; } catch (err) { e = err; exportFail = true; } finally { setState(() { exporting = false; }); } } _exportComicDownloadToCbzsZip() async { if (!isPro) { defaultToast(context, tr("screen.download_export_group.please_power_up")); return; } if (!await confirmDialog( context, tr("screen.download_export_group.export_confirm"), tr("screen.download_export_group.export_to_cbz_title") + showExportPath())) { return; } try { setState(() { exporting = true; }); final path = await attachExportPath(); for (var id in widget.idList) { await method.exportComicDownloadToCbzsZip( id, path, "", ); } exported = true; } catch (err) { e = err; exportFail = true; } finally { setState(() { exporting = false; }); } } @override Widget build(BuildContext context) { return WillPopScope( child: Scaffold( appBar: AppBar( title: Text(tr("screen.download_export_group.title")), ), body: _body(), ), onWillPop: () async { if (exporting) { defaultToast(context, tr("screen.download_export_group.exporting_please_wait")); return false; } return true; }, ); } Widget _buildButtonInner(String text) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Container( width: constraints.maxWidth, padding: const EdgeInsets.all(15), color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.05), child: Text( text, textAlign: TextAlign.center, ), ); }, ); } } ================================================ FILE: lib/screens/DownloadImportScreen.dart ================================================ import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:file_picker/file_picker.dart'; import 'package:filesystem_picker/filesystem_picker.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Channels.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/config/ChooserRoot.dart'; import '../basic/Cross.dart'; import '../basic/config/IconLoading.dart'; import '../basic/config/ImportNotice.dart'; import '../basic/config/IsPro.dart'; import 'PkzArchiveScreen.dart'; import 'components/ContentLoading.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; // 导入 class DownloadImportScreen extends StatefulWidget { const DownloadImportScreen({Key? key}) : super(key: key); @override State createState() => _DownloadImportScreenState(); } class _DownloadImportScreenState extends State { bool _importing = false; String _importMessage = ""; @override void initState() { registerEvent(_onMessageChange, "EXPORT"); super.initState(); } @override void dispose() { unregisterEvent(_onMessageChange); super.dispose(); } void _onMessageChange(event) { if (event is String) { setState(() { _importMessage = event; }); } } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: !_importing, ); } Widget buildScreen(BuildContext context) { if (_importing) { return Scaffold( body: ContentLoading(label: _importMessage), ); } return Scaffold( appBar: AppBar( title: Text(tr("screen.download_import.title")), ), body: PikaListView( children: [ Container( padding: const EdgeInsets.all(10), child: Text(_importMessage), ), Container(height: 20), importNotice(context), Container(height: 20), _fileImportButton(), Container(height: 20), _networkImportButton(), Container(height: 20), _importDirFilesZipButton(), Container(height: 40), ], ), ); } Widget _fileImportButton() { return MaterialButton( height: 80, onPressed: () async { late String chooseRoot; try { chooseRoot = await currentChooserRoot(); } catch (e) { defaultToast(context, "$e"); return; } String? path; if (Platform.isAndroid) { path = await FilesystemPicker.open( title: tr("screen.download_import.open_file"), context: context, rootDirectory: Directory(chooseRoot), fsType: FilesystemType.file, folderIconColor: Colors.teal, allowedExtensions: ['.pkz', '.zip', '.pki'], fileTileSelectMode: FileTileSelectMode.wholeTile, ); } else { var ls = await FilePicker.platform.pickFiles( dialogTitle: tr("screen.download_import.select_file"), allowMultiple: false, initialDirectory: chooseRoot, type: FileType.custom, allowedExtensions: ['pkz', 'zip', 'pki'], allowCompression: false, ); path = ls != null && ls.count > 0 ? ls.paths[0] : null; } if (path != null) { if (path.endsWith(".pkz")) { Navigator.of(context).push( mixRoute( builder: (BuildContext context) => PkzArchiveScreen(pkzPath: path!), ), ); } else if (path.endsWith(".zip") || path.endsWith(".pki")) { try { setState(() { _importing = true; }); if (path.endsWith(".zip")) { await method.importComicDownload(path); } else if (path.endsWith(".pki")) { await method.importComicDownloadPki(path); } setState(() { _importMessage = tr("screen.download_import.import_success"); }); } catch (e) { setState(() { _importMessage = tr("screen.download_import.import_failed") + " $e"; }); } finally { setState(() { _importing = false; }); } } } }, child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Container( width: constraints.maxWidth, padding: const EdgeInsets.only(top: 15, bottom: 15), color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.05), child: Text( tr("screen.download_import.select_file_desc"), textAlign: TextAlign.center, ), ); }, ), ); } Widget _networkImportButton() { return MaterialButton( height: 80, onPressed: () async { var path = await inputString(context, tr("screen.download_import.input_address")); if (path != null) { try { setState(() { _importing = true; }); await method.importComicDownloadUsingSocket(path); setState(() { _importMessage = tr("screen.download_import.import_success"); }); } catch (e) { setState(() { _importMessage = tr("screen.download_import.import_failed") + " $e"; }); } finally { setState(() { _importing = false; }); } } }, child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Container( width: constraints.maxWidth, padding: const EdgeInsets.only(top: 15, bottom: 15), color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.05), child: Text( tr("screen.download_import.import_from_other_device"), textAlign: TextAlign.center, ), ); }, ), ); } Widget _importDirFilesZipButton() { return MaterialButton( height: 80, onPressed: () async { late String? path; try { path = await chooseFolder(context); } catch (e) { defaultToast(context, "$e"); return; } if (path != null) { try { setState(() { _importing = true; }); await method.importComicDownloadDir(path); setState(() { _importMessage = tr("screen.download_import.import_success"); }); } catch (e) { setState(() { _importMessage = tr("screen.download_import.import_failed") + " $e"; }); } finally { setState(() { _importing = false; }); } } }, child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Container( width: constraints.maxWidth, padding: const EdgeInsets.only(top: 15, bottom: 15), color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.05), child: Text( tr("screen.download_import.select_folder_desc") + (!isPro ? "\n(${tr('app.pro')})" : ""), textAlign: TextAlign.center, ), ); }, ), ); } } ================================================ FILE: lib/screens/DownloadInfoScreen.dart ================================================ import 'dart:convert'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Navigator.dart'; import 'package:pikapika/basic/Method.dart'; import '../basic/config/IconLoading.dart'; import '../basic/config/ShowCommentAtDownload.dart'; import 'ComicInfoScreen.dart'; import 'DownloadExportToFileScreen.dart'; import 'DownloadReaderScreen.dart'; import 'components/ComicDescriptionCard.dart'; import 'components/ComicTagsCard.dart'; import 'components/CommentList.dart'; import 'components/CommentMainType.dart'; import 'components/ContentError.dart'; import 'components/ContentLoading.dart'; import 'components/ContinueReadButton.dart'; import 'components/DownloadInfoCard.dart'; import 'components/ListView.dart'; import 'components/Recommendation.dart'; // 下载详情 class DownloadInfoScreen extends StatefulWidget { final String comicId; final String comicTitle; const DownloadInfoScreen({ required this.comicId, required this.comicTitle, Key? key, }) : super(key: key); @override State createState() => _DownloadInfoScreenState(); } class _DownloadInfoScreenState extends State with RouteAware { late Future _viewFuture = _loadViewLog(); late DownloadComic _task; late List _epList = []; late Future _future = _load(); Future _load() async { _task = (await method.loadDownloadComic(widget.comicId))!; _epList = await method.downloadEpList(widget.comicId); } Future _loadViewLog() { return method.loadView(widget.comicId); } @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe(this, ModalRoute.of(context)!); } @override void didPopNext() { setState(() { _viewFuture = _loadViewLog(); }); } @override void dispose() { routeObserver.unsubscribe(this); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.comicTitle), actions: [ IconButton( onPressed: () async { Navigator.push( context, mixRoute( builder: (context) => DownloadExportToFileScreen( comicId: widget.comicId, comicTitle: widget.comicTitle, ), ), ); }, icon: const Icon(Icons.add_to_home_screen), ), IconButton( onPressed: () { Navigator.push( context, mixRoute( builder: (context) => ComicInfoScreen( comicId: widget.comicId, ), ), ); }, icon: const Icon(Icons.settings_ethernet_outlined), ), ], ), body: FutureBuilder( future: _future, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return ContentError( error: snapshot.error, stackTrace: snapshot.stackTrace, onRefresh: () async { setState(() { _future = _load(); }); }); } if (snapshot.connectionState != ConnectionState.done) { return ContentLoading(label: tr("screen.download_info.loading")); } List tagsDynamic = json.decode(_task.tags); List tags = tagsDynamic.map((e) => "$e").toList(); var list = PikaListView( children: [ DownloadInfoCard(task: _task, linkItem: true), ComicTagsCard(tags), ComicDescriptionCard(description: _task.description), Container(height: 5), _bottom(), ], ); // todo only pika task if (showCommentAtDownload()) { return DefaultTabController( length: 3, child: list, ); } return list; }, ), ); } var _tabIndex = 0; Widget _bottom() { // todo only pika task if (showCommentAtDownload()) { final theme = Theme.of(context); var _tabs = [ Tab(text: tr("screen.download_info.chapter") + " (${_epList.length})"), Tab(text: tr("screen.download_info.comment")), Tab(text: tr("screen.download_info.recommend")), ]; var _views = [ _chapters(), CommentList(CommentMainType.COMIC, widget.comicId), Recommendation(comicId: widget.comicId), ]; return Column(children: [ Container( height: 40, color: theme.colorScheme.secondary.withOpacity(.025), child: TabBar( tabs: _tabs, indicatorColor: theme.colorScheme.secondary, labelColor: theme.colorScheme.secondary, onTap: (val) async { setState(() { _tabIndex = val; }); }, ), ), Container(height: 15), _views[_tabIndex], Container(height: 5), ]); } return _chapters(); } Widget _chapters() { return Wrap( spacing: 10, runSpacing: 10, alignment: WrapAlignment.spaceAround, children: [ ContinueReadButton( viewFuture: _viewFuture, onChoose: (int? epOrder, int? pictureRank) { if (epOrder != null && pictureRank != null) { for (var i in _epList) { if (i.epOrder == epOrder) { _push(_task, _epList, epOrder, pictureRank); return; } } } else { _push(_task, _epList, _epList.first.epOrder, null); } }, ), ..._epList.reversed.map((e) { return MaterialButton( onPressed: () { _push(_task, _epList, e.epOrder, null); }, color: Colors.white, child: Text(e.title, style: const TextStyle(color: Colors.black)), ); }), ], ); } void _push( DownloadComic task, List epList, int epOrder, int? rank, ) { Navigator.push( context, mixRoute( builder: (context) => DownloadReaderScreen( comicInfo: _task, epList: _epList, currentEpOrder: epOrder, initPicturePosition: rank, ), ), ); } } ================================================ FILE: lib/screens/DownloadListScreen.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'components/flutter_search_bar.dart' as fsb; import 'package:pikapika/basic/Channels.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/DownloadExportGroupScreen.dart'; import '../basic/config/IconLoading.dart'; import 'DownloadImportScreen.dart'; import 'DownloadInfoScreen.dart'; import 'components/ContentLoading.dart'; import 'components/DownloadInfoCard.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; // 下载列表 class DownloadListScreen extends StatefulWidget { const DownloadListScreen({Key? key}) : super(key: key); @override State createState() => _DownloadListScreenState(); } class _DownloadListScreenState extends State { String _search = ""; bool _selecting = false; List _selectingList = []; List _selectable = []; String _filterCustomFolder = ""; List _folderList = []; late final fsb.SearchBar _searchBar = fsb.SearchBar( hintText: tr('screen.download_list.search_download'), inBar: false, setState: setState, onSubmitted: (value) { _search = value; _reloadList(); setState(() {}); _searchBar.controller.text = value; }, buildDefaultAppBar: (BuildContext context) { final theme = Theme.of(context); final appBarIconColor = theme.appBarTheme.foregroundColor ?? Colors.white; if (_selecting) { return AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back_ios), onPressed: () { if (_selecting) { setState(() { _selecting = false; _selectingList = []; }); } else { Navigator.pop(context); } }, ), title: Text(tr('screen.download_list.multi_select_operation')), actions: [ _selectingCancelButton(appBarIconColor), _selectingMoveButton(appBarIconColor), _selectingDeleteButton(appBarIconColor), _selectAllButton(appBarIconColor), ], ); } return AppBar( title: Text(_search == "" ? tr('screen.download_list.download_list') : (tr('screen.download_list.search_download') + ' - $_search')), actions: [ //_searchBar.getSearchAction(context), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { _searchBar.beginSearch(context); }, child: Column( children: [ Expanded(child: Container()), Icon( Icons.search, size: 18, color: appBarIconColor, ), Text( tr('screen.download_list.search'), style: TextStyle(fontSize: 14, color: appBarIconColor), ), Expanded(child: Container()), ], ), ), _customFolderButton(appBarIconColor), _toSelectingButton(appBarIconColor), _fileButton(appBarIconColor), Container(width: 10), pauseButton(appBarIconColor), ], ); }, ); DownloadComic? _downloading; late bool _downloadRunning = false; late Future> _f = method .allDownloads(_search, customFolder: _filterCustomFolder) .then((value) { setState(() { _selecting = false; _selectingList = []; _selectable = value.map((e) => e.id).toList(); }); return value; }); List _data = []; void _onMessageChange(String event) { print("EVENT"); print(event); try { setState(() { _downloading = DownloadComic.fromJson(json.decode(event)); }); } catch (e, s) { print(e); print(s); } } @override void initState() { registerEvent(_onMessageChange, "DOWNLOAD"); method .downloadRunning() .then((val) => setState(() => _downloadRunning = val)); method.allCustomFolders().then((value) { setState(() { _folderList = value.where((e) => e.isNotEmpty).toList(); }); }); super.initState(); } @override void dispose() { unregisterEvent(_onMessageChange); super.dispose(); } Widget _selectAllButton(Color appBarIconColor) { return IconButton( icon: Icon( Icons.select_all, size: 18, color: appBarIconColor, ), onPressed: () async { setState(() { if (_selectingList.length >= _selectable.length) { _selectingList.clear(); } else { _selectingList.clear(); _selectingList.addAll(_selectable); } }); }, ); } @override Widget build(BuildContext context) { final screen = Scaffold( appBar: _searchBar.build(context), body: FutureBuilder( future: _f, builder: (BuildContext context, AsyncSnapshot> snapshot) { if (snapshot.connectionState != ConnectionState.done) { return ContentLoading(label: tr('app.loading')); } if (snapshot.hasError) { print("${snapshot.error}"); print("${snapshot.stackTrace}"); return Center(child: Text(tr('app.loading_failed'))); } var data = snapshot.data!; _data = data; if (_downloading != null) { try { for (var i = 0; i < data.length; i++) { if (_downloading!.id == data[i].id) { data[i].copy(_downloading!); } } } catch (e, s) { print(e); print(s); } } if (_selecting) { return ListView( children: [ ...data.map(selectingWidget), ], ); } return RefreshIndicator( onRefresh: () async { _reloadList(); setState(() {}); }, child: PikaListView( children: [ ...data.map(downloadWidget), ], ), ); }, ), ); var w = rightClickPop( child: screen, context: context, canPop: true, ); return WillPopScope( onWillPop: () async { if (_selecting) { setState(() { _selecting = false; _selectingList = []; }); return false; } return true; }, child: w, ); } Widget _customFolderButton(Color appBarIconColor) { return IconButton( onPressed: () async { String? choose = await chooseListDialog( context, tr('screen.download_list.select_folder'), [ tr('app.all'), ..._folderList, ]); if (choose != null) { if (choose == tr('app.all')) { choose = ""; } _filterCustomFolder = choose; _reloadList(); setState(() {}); } }, icon: Column( children: [ Expanded(child: Container()), Icon( Icons.folder, size: 18, color: appBarIconColor, ), Text( _customFolderName(), style: TextStyle(fontSize: 14, color: appBarIconColor), maxLines: 1, overflow: TextOverflow.ellipsis, ), Expanded(child: Container()), ], )); } String _customFolderName() { if (_filterCustomFolder == "") { return tr('app.all'); } return _filterCustomFolder; } Widget downloadWidget(DownloadComic e) { return InkWell( onTap: () { if (e.deleting) { return; } Navigator.push( context, mixRoute( builder: (context) => DownloadInfoScreen( comicId: e.id, comicTitle: e.title, ), ), ); }, onLongPress: () async { String? action = await chooseListDialog(context, e.title, [tr('app.delete')]); if (action == tr('app.delete')) { await method.deleteDownloadComic(e.id); setState(() => e.deleting = true); } }, child: DownloadInfoCard( task: e, downloading: _downloading != null && _downloading!.id == e.id, ), ); } Widget selectingWidget(DownloadComic e) { return InkWell( onTap: () { if (e.deleting) { defaultToast(context, tr('screen.download_list.download_already_in_delete_queue')); return; } else { if (_selectingList.contains(e.id)) { setState(() { _selectingList.remove(e.id); }); } else { setState(() { _selectingList.add(e.id); }); } } }, child: Stack( children: [ DownloadInfoCard( task: e, downloading: _downloading != null && _downloading!.id == e.id, ), SizedBox( height: imageHeight, child: Align( alignment: Alignment.bottomRight, child: Container( margin: const EdgeInsets.only(top: 5), padding: const EdgeInsets.only(right: 10, left: 5), decoration: BoxDecoration( color: Colors.grey.shade500.withOpacity(.1), borderRadius: const BorderRadius.only( topLeft: Radius.circular(5), bottomLeft: Radius.circular(5), ), ), child: Padding( padding: const EdgeInsets.all(5), child: Icon( _selectingList.contains(e.id) ? Icons.check_circle_sharp : Icons.circle_outlined, color: Theme.of(context).colorScheme.secondary, ), ), ), ), ), ], ), ); } Widget _fileButton(Color appBarIconColor) { return PopupMenuButton( itemBuilder: (BuildContext context) => >[ PopupMenuItem( value: 0, child: ListTile( leading: const Icon(Icons.read_more), title: Text(tr('screen.download_list.import')), ), ), PopupMenuItem( value: 1, child: ListTile( leading: const Icon(Icons.save_alt), title: Text(tr('screen.download_list.export')), ), ), ], onSelected: (a) async { if (a == 0) { await Navigator.push( context, mixRoute( builder: (context) => const DownloadImportScreen(), ), ); _reloadList(); setState(() {}); } else if (a == 1) { await Navigator.push( context, mixRoute( builder: (context) => const DownloadExportGroupScreen(), ), ); } }, child: Column( children: [ Expanded(child: Container()), Icon( Icons.drive_file_move, size: 18, color: appBarIconColor, ), Text( tr('screen.download_list.file'), style: TextStyle(fontSize: 14, color: appBarIconColor), ), Expanded(child: Container()), ], ), ); } Future _onPauseChangeClick() async { await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(tr('screen.download_list.download_task')), content: Text( _downloadRunning ? tr('screen.download_list.pause_download') : tr('screen.download_list.start_download'), ), actions: [ MaterialButton( onPressed: () async { Navigator.pop(context); }, child: Text(tr('app.cancel')), ), MaterialButton( onPressed: () async { Navigator.pop(context); var to = !_downloadRunning; await method.setDownloadRunning(to); setState(() { _downloadRunning = to; }); }, child: Text(tr('app.confirm')), ), ], ); }, ); } Widget pauseButton(Color appBarIconColor) { return PopupMenuButton( itemBuilder: (BuildContext context) => >[ PopupMenuItem( value: 0, child: ListTile( leading: const Icon(Icons.compare_arrows_sharp), title: Text(_downloadRunning ? tr('screen.download_list.pause_download') : tr('screen.download_list.start_download')), ), ), PopupMenuItem( value: 1, child: ListTile( leading: const Icon(Icons.sync_problem), title: Text(tr('screen.download_list.resume_failed')), ), ), ], onSelected: (a) async { if (a == 0) { await _onPauseChangeClick(); } else if (a == 1) { await method.resetFailed(); _reloadList(); setState(() {}); defaultToast( context, tr('screen.download_list.resume_failed_desc')); } }, child: Column( children: [ Expanded(child: Container()), Icon( _downloadRunning ? Icons.compare_arrows_sharp : Icons.schedule_send, size: 18, color: appBarIconColor, ), Text( _downloadRunning ? tr('screen.download_list.downloading') : tr('screen.download_list.paused'), style: TextStyle(fontSize: 14, color: appBarIconColor), ), Expanded(child: Container()), ], )); } void _reloadList() { _f = method .allDownloads(_search, customFolder: _filterCustomFolder) .then((value) { setState(() { _selecting = false; _selectingList = []; }); return value; }); method.allCustomFolders().then((value) { setState(() { _folderList = value.where((e) => e.isNotEmpty).toList(); }); }); } Widget _selectingCancelButton(Color appBarIconColor) { return IconButton( onPressed: () { setState(() { _selecting = false; _selectingList = []; }); }, icon: Icon(Icons.cancel, color: appBarIconColor), ); } Widget _selectingMoveButton(Color appBarIconColor) { return IconButton( onPressed: () async { var tmp = _selectingList; _selecting = false; _selectingList = []; setState(() {}); if (tmp.isEmpty) { defaultToast( context, tr('screen.download_list.select_download_to_move')); } else { var moveToChoose = await chooseListDialog( context, tr('screen.download_list.move_download'), [ tr('app.all'), ..._folderList, tr('screen.download_list.input_name') ], tips: tr('screen.download_list.empty_folder_will_be_deleted'), ); if (moveToChoose == null) { return; } if (moveToChoose == tr('screen.download_list.input_name')) { String? name = await displayTextInputDialog(context, title: tr('screen.download_list.folder_name'), hint: tr('screen.download_list.please_input_folder_name')); if (name != null) { if (tr('app.all') != name && tr('screen.download_list.input_name') != name) { await method.moveDownloadComic(tmp, name); _reloadList(); setState(() {}); } } } else if (moveToChoose == tr('app.all')) { await method.moveDownloadComic(tmp, ""); _reloadList(); setState(() {}); } else { await method.moveDownloadComic(tmp, moveToChoose); _reloadList(); setState(() {}); } } }, icon: Icon(Icons.move_down, color: appBarIconColor), ); } Widget _selectingDeleteButton(Color appBarIconColor) { return IconButton( onPressed: () async { var tmp = _selectingList; _selecting = false; _selectingList = []; setState(() {}); if (tmp.isEmpty) { defaultToast( context, tr('screen.download_list.select_download_to_delete')); } else { if (await confirmDialog( context, tr('screen.download_list.delete_download'), tr('screen.download_list.delete_selected_download'))) { for (var id in tmp) { await method.deleteDownloadComic(id); } for (var i = 0; i < _data.length; i++) { if (tmp.contains(_data[i].id)) { _data[i].deleting = true; } } _selecting = false; _selectingList = []; //_reloadList(); setState(() {}); } } }, icon: Icon(Icons.delete, color: appBarIconColor), ); } Widget _toSelectingButton(Color appBarIconColor) { return IconButton( onPressed: () { setState(() { _selecting = true; _selectingList = []; }); }, icon: Column( children: [ Expanded(child: Container()), Icon( Icons.rule, size: 18, color: appBarIconColor, ), Text( tr('screen.download_list.multi_select'), style: TextStyle(fontSize: 14, color: appBarIconColor), ), Expanded(child: Container()), ], ), ); } } ================================================ FILE: lib/screens/DownloadOnlyImportScreen.dart ================================================ import 'dart:async'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import 'package:pikapika/basic/Common.dart'; import '../basic/Channels.dart'; import '../basic/Method.dart'; import 'components/ContentLoading.dart'; class DownloadOnlyImportScreen extends StatefulWidget { final bool holdPkz; final String path; const DownloadOnlyImportScreen({ Key? key, required this.path, this.holdPkz = false, }) : super(key: key); @override State createState() => _DownloadOnlyImportScreenState(); } class _DownloadOnlyImportScreenState extends State { bool importing = false; bool imported = false; bool importFail = false; dynamic e; String importMessage = tr('screen.download_import.importing'); StreamSubscription? _linkSubscription; @override void initState() { if (widget.holdPkz) { _linkSubscription = linkSubscript(context); } registerEvent(_onMessageChange, "EXPORT"); super.initState(); } @override void dispose() { _linkSubscription?.cancel(); unregisterEvent(_onMessageChange); super.dispose(); } void _onMessageChange(event) { setState(() { importMessage = event; }); } Widget _body() { if (importing) { return ContentLoading(label: importMessage); } if (importFail) { return Center(child: Text(tr('screen.download_import.import_failed') + "\n$e")); } if (imported) { return Center(child: Text(tr('screen.download_import.import_success'))); } return Center( child: MaterialButton( onPressed: _import, child: Text(tr('screen.download_import.click_import_file') + "\n${p.basename(widget.path)}"), ), ); } _import() async { try { setState(() { importing = true; }); if (widget.path.endsWith(".zip")) { await method.importComicDownload(widget.path); } else if (widget.path.endsWith(".pki")) { await method.importComicDownloadPki(widget.path); } imported = true; } catch (err) { e = err; importFail = true; } finally { setState(() { importing = false; }); } } @override Widget build(BuildContext context) { return WillPopScope( child: Scaffold( appBar: AppBar( title: Text(tr('screen.download_import.import')), ), body: _body(), ), onWillPop: () async { if (importing) { defaultToast(context, tr('screen.download_import.importing_please_wait')); return false; } return true; }, ); } } ================================================ FILE: lib/screens/DownloadReaderScreen.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/config/AutoFullScreen.dart'; import 'package:pikapika/basic/config/FullScreenUI.dart'; import 'package:pikapika/basic/config/ReaderDirection.dart'; import 'package:pikapika/basic/config/ReaderType.dart'; import 'package:pikapika/basic/Method.dart'; import '../basic/config/IconLoading.dart'; import 'components/ContentError.dart'; import 'components/ContentLoading.dart'; import 'components/ImageReader.dart'; import 'components/RightClickPop.dart'; // 阅读下载的内容 class DownloadReaderScreen extends StatefulWidget { final DownloadComic comicInfo; final List epList; final int currentEpOrder; final int? initPicturePosition; final ReaderType pagerType = currentReaderType(); final ReaderDirection pagerDirection = gReaderDirection; late final bool autoFullScreen; DownloadReaderScreen({ Key? key, required this.comicInfo, required this.epList, required this.currentEpOrder, this.initPicturePosition, bool? autoFullScreen, }) : super(key: key) { this.autoFullScreen = autoFullScreen ?? currentAutoFullScreen(); } @override State createState() => _DownloadReaderScreenState(); } class _DownloadReaderScreenState extends State { late DownloadEp _ep; late bool _fullScreen = false; late List pictures = []; late Future _future = _load(); int? _lastChangeRank; bool _replacement = false; Future _load() async { if (widget.initPicturePosition == null) { await method.storeViewEp(widget.comicInfo.id, _ep.epOrder, _ep.title, 0); } pictures.clear(); for (var ep in widget.epList) { if (ep.epOrder == widget.currentEpOrder) { pictures.addAll((await method.downloadPicturesByEpId(ep.id))); } } if (widget.autoFullScreen) { setState(() { SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: [], ); _fullScreen = true; }); } } Future _onPositionChange(int position) async { _lastChangeRank = position; return method.storeViewEp( widget.comicInfo.id, _ep.epOrder, _ep.title, position); } FutureOr _onDownload() async { defaultToast(context, "您阅读的是下载漫画"); } FutureOr _onChangeEp(int epOrder) { var orderMap = {}; for (var element in widget.epList) { orderMap[element.epOrder] = element; } if (orderMap.containsKey(epOrder)) { _replacement = true; Navigator.of(context).pushReplacement( mixRoute( builder: (context) => DownloadReaderScreen( comicInfo: widget.comicInfo, epList: widget.epList, currentEpOrder: epOrder, autoFullScreen: _fullScreen, ), ), ); } } FutureOr _onReloadEp() { _replacement = true; Navigator.of(context).pushReplacement( mixRoute( builder: (context) => DownloadReaderScreen( comicInfo: widget.comicInfo, epList: widget.epList, currentEpOrder: widget.currentEpOrder, initPicturePosition: _lastChangeRank ?? widget.initPicturePosition, // maybe null autoFullScreen: _fullScreen, ), ), ); } @override void initState() { // EP for (var element in widget.epList) { if (element.epOrder == widget.currentEpOrder) { _ep = element; } } // INIT _future = _load(); super.initState(); } @override void dispose() { if (!_replacement) { switchFullScreenUI(); } super.dispose(); } @override Widget build(BuildContext context){ return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return readerKeyboardHolder(_build(context)); } Widget _build(BuildContext context) { return FutureBuilder( future: _future, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return Scaffold( appBar: _fullScreen ? null : AppBar( title: Text("${_ep.title} - ${widget.comicInfo.title}"), ), body: ContentError( error: snapshot.error, stackTrace: snapshot.stackTrace, onRefresh: () async { setState(() { _future = _load(); }); }, ), ); } if (snapshot.connectionState != ConnectionState.done) { return Scaffold( appBar: _fullScreen ? null : AppBar( title: Text("${_ep.title} - ${widget.comicInfo.title}"), ), body: const ContentLoading(label: '加载中'), ); } var epNameMap = {}; for (var element in widget.epList) { epNameMap[element.epOrder] = element.title; } return Scaffold( body: ImageReader( ImageReaderStruct( images: pictures .map((e) => ReaderImageInfo(e.fileServer, e.path, e.localPath, e.width, e.height, e.format, e.fileSize)) .toList(), fullScreen: _fullScreen, onFullScreenChange: _onFullScreenChange, onPositionChange: _onPositionChange, initPosition: widget.initPicturePosition, epOrder: _ep.epOrder, epNameMap: epNameMap, comicTitle: widget.comicInfo.title, onReloadEp: _onReloadEp, onChangeEp: _onChangeEp, onDownload: _onDownload, ), ), ); }, ); } Future _onFullScreenChange(bool fullScreen) async { setState(() { if (fullScreen) { if (Platform.isAndroid || Platform.isIOS) { SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: [], ); } } else { switchFullScreenUI(); } _fullScreen = fullScreen; }); } } ================================================ FILE: lib/screens/FavouritePaperScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Method.dart'; import '../basic/Entities.dart'; import 'components/ComicPager.dart'; import 'components/RightClickPop.dart'; // 收藏的漫画 class FavouritePaperScreen extends StatefulWidget { const FavouritePaperScreen({Key? key}) : super(key: key); @override State createState() => _FavouritePaperScreen(); } class _FavouritePaperScreen extends State { Future _fetch(String _currentSort, int _currentPage) { return method.favouriteComics(_currentSort, _currentPage); } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr('screen.favourite_paper.favourite')), ), body: ComicPager( fetchPage: _fetch, ), ); } } ================================================ FILE: lib/screens/FilePhotoViewScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Cross.dart'; import 'package:pikapika/screens/components/Images.dart'; import 'components/RightClickPop.dart'; // 预览图片 class FilePhotoViewScreen extends StatelessWidget { final String filePath; const FilePhotoViewScreen(this.filePath, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) => Scaffold( body: Stack( children: [ GestureDetector( onLongPress: () async { String? choose = await chooseListDialog( context, tr('app.please_select'), [tr('app.save_image')]); if (choose == null) { return; } if (choose == tr('app.save_image')) { saveImage(filePath, context); } }, child: PhotoView( imageProvider: ResourceFileImageProvider(filePath), ), ), InkWell( onTap: () => Navigator.of(context).pop(), child: Container( margin: const EdgeInsets.only(top: 30), padding: const EdgeInsets.only(left: 4, right: 4), decoration: BoxDecoration( color: Colors.black.withOpacity(.75), borderRadius: const BorderRadius.only( topRight: Radius.circular(8), bottomRight: Radius.circular(8), ), ), child: const Icon(Icons.keyboard_backspace, color: Colors.white), ), ), ], ), ); } ================================================ FILE: lib/screens/ForgotPasswordScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../basic/Common.dart'; import '../basic/Cross.dart'; import '../basic/Method.dart'; import 'components/ContentLoading.dart'; import 'components/ListView.dart'; class ForgotPasswordScreen extends StatefulWidget { const ForgotPasswordScreen({Key? key}) : super(key: key); @override State createState() => _ForgotPasswordScreenState(); } class _ForgotPasswordScreenState extends State { bool _loading = false; int _state = 0; // 0 输入账号,1 回答问题,2 密码已经找回 String _email = ""; String _question1 = ""; String _question2 = ""; String _question3 = ""; String _answer1 = ""; String _answer2 = ""; String _answer3 = ""; String _password = ""; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr("screen.forgot_password.title")), ), body: _stateScreen(), ); } Widget _stateScreen() { if (_loading) { return ContentLoading(label: tr('app.loading')); } switch (_state) { case 0: return _inputEmailScreen(); case 1: return _inputAnswerScreen(); case 2: return _showNewPasswordScreen(); } throw ''; } Widget _inputEmailScreen() { return PikaListView(children: [ ListTile( title: Text(tr("screen.forgot_password.username")), subtitle: Text(_email == "" ? tr("screen.forgot_password.not_set") : _email), onTap: () async { String? input = await displayTextInputDialog( context, src: _email, title: tr('screen.forgot_password.username'), hint: tr('screen.forgot_password.please_enter_username'), ); if (input != null) { setState(() { _email = input; }); } }, ), Container( margin: const EdgeInsets.all(10), color: Colors.grey.shade500.withAlpha(18), child: MaterialButton( onPressed: _confirmEmail, child: Text(tr("screen.forgot_password.confirm")), ), ), ]); } void _confirmEmail() async { if (_email.isEmpty) { defaultToast(context, tr("screen.forgot_password.please_enter_username")); return; } try { setState(() { _loading = true; }); var result = await method.forgotPassword(_email); _question1 = result.question1; _question2 = result.question2; _question3 = result.question3; _state = 1; } catch (e, s) { print("$e\n$s"); defaultToast(context, '$e'); } finally { setState(() { _loading = false; }); } } Widget _inputAnswerScreen() { return ListView(children: [ Container(height: 10), ListTile( title: Text(tr("screen.forgot_password.username")), subtitle: Text(_email.isEmpty ? tr("screen.forgot_password.not_set") : _email), ), Container(height: 10), const Divider(), Container(height: 10), ListTile( title: Text(tr("screen.forgot_password.question_1")), subtitle: Text(_question1), ), ListTile( title: Text(tr("screen.forgot_password.answer_1")), subtitle: Text(_answer1.isEmpty ? tr("screen.forgot_password.not_set") : _answer1), onTap: () async { String? input = await displayTextInputDialog( context, src: _answer1, title: tr('screen.forgot_password.answer_1'), hint: tr('screen.forgot_password.please_enter_answer_1'), ); if (input != null) { setState(() { _answer1 = input; }); } }, ), Container( margin: const EdgeInsets.all(10), color: Colors.grey.shade500.withAlpha(18), child: MaterialButton( onPressed: () { _confirmAnswer(1, _answer1); }, child: Text(tr("screen.forgot_password.use_answer_1_recover")), ), ), Container(height: 10), const Divider(), Container(height: 10), ListTile( title: Text(tr("screen.forgot_password.question_2")), subtitle: Text(_question2), ), ListTile( title: Text(tr("screen.forgot_password.answer_2")), subtitle: Text(_answer2.isEmpty ? tr("screen.forgot_password.not_set") : _answer2), onTap: () async { String? input = await displayTextInputDialog( context, src: _answer2, title: tr('screen.forgot_password.answer_2'), hint: tr('screen.forgot_password.please_enter_answer_2'), ); if (input != null) { setState(() { _answer2 = input; }); } }, ), Container( margin: const EdgeInsets.all(10), color: Colors.grey.shade500.withAlpha(18), child: MaterialButton( onPressed: () { _confirmAnswer(2, _answer2); }, child: Text(tr("screen.forgot_password.use_answer_2_recover")), ), ), Container(height: 10), const Divider(), Container(height: 10), ListTile( title: Text(tr("screen.forgot_password.question_3")), subtitle: Text(_question3), ), ListTile( title: Text(tr("screen.forgot_password.answer_3")), subtitle: Text(_answer3.isEmpty ? tr("screen.forgot_password.not_set") : _answer3), onTap: () async { String? input = await displayTextInputDialog( context, src: _answer3, title: tr('screen.forgot_password.answer_3'), hint: tr('screen.forgot_password.please_enter_answer_3'), ); if (input != null) { setState(() { _answer3 = input; }); } }, ), Container( margin: const EdgeInsets.all(10), color: Colors.grey.shade500.withAlpha(18), child: MaterialButton( onPressed: () { _confirmAnswer(3, _answer3); }, child: Text(tr("screen.forgot_password.use_answer_3_recover")), ), ), ///////// Container(height: 20), ]); } _confirmAnswer(int answerNo, String answer) async { if (answer.isEmpty) { defaultToast(context, tr("screen.forgot_password.please_enter_answer")); return; } try { setState(() { _loading = true; }); var result = await method.resetPassword(_email, answerNo, answer); _password = result.password; _state = 2; defaultToast(context, tr("screen.forgot_password.new_password_copied")); copyToClipBoard(context, _password); } catch (e, s) { print("$e\n$s"); if ("$e".contains("invalid request")) { defaultToast(context, tr('screen.forgot_password.answer_incorrect')); } else { defaultToast(context, '$e'); } } finally { setState(() { _loading = false; }); } } Widget _showNewPasswordScreen() { return ListView(children: [ ListTile( title: Text(tr("screen.forgot_password.username")), subtitle: Text(_email.isEmpty ? tr("screen.forgot_password.not_set") : _email), ), ListTile( title: Text(tr("screen.forgot_password.password")), subtitle: Text(_password.isEmpty ? tr("screen.forgot_password.not_set") : _password), onTap: () { defaultToast(context, tr("screen.forgot_password.new_password_copied")); copyToClipBoard(context, _password); }, ), ]); } } ================================================ FILE: lib/screens/GameDownloadScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Cross.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/ItemBuilder.dart'; import 'components/GameTitleCard.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; // 游戏下载地址列表页 class GameDownloadScreen extends StatefulWidget { final GameInfo info; const GameDownloadScreen(this.info, {Key? key}) : super(key: key); @override State createState() => _GameDownloadScreenState(); } class _GameDownloadScreenState extends State { late Future> _future = method.downloadGame(widget.info.androidLinks[0]); @override Widget build(BuildContext context){ return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("${tr('screen.game_download.title')} - ${widget.info.title}"), ), body: PikaListView( children: [ GameTitleCard(widget.info), ItemBuilder( future: _future, onRefresh: () async { setState(() { _future = method.downloadGame(widget.info.androidLinks[0]); }); }, successBuilder: (BuildContext context, AsyncSnapshot> snapshot) { return Column( children: [ Container( padding: const EdgeInsets.all(30), child: Text(tr('screen.game_download.download_links_obtained')), ), ...snapshot.data!.map((e) => _copyCard(e)), ], ); }, ), ], ), ); } Widget _copyCard(String string) { return InkWell( onTap: () { copyToClipBoard(context, string); }, child: Row( children: [ Expanded( child: Container( margin: const EdgeInsets.all(10), padding: const EdgeInsets.all(10), decoration: BoxDecoration( border: Border.all( color: Colors.grey.shade500, width: .5, style: BorderStyle.solid, ), ), child: Text(string), ), ), ], ), ); } } ================================================ FILE: lib/screens/GameInfoScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/CommentMainType.dart'; import 'package:pikapika/screens/components/ContentError.dart'; import 'package:pikapika/screens/components/ContentLoading.dart'; import 'package:pikapika/screens/components/Images.dart'; import '../basic/config/IconLoading.dart'; import 'GameDownloadScreen.dart'; import 'components/CommentList.dart'; import 'components/GameTitleCard.dart'; import 'components/RightClickPop.dart'; // 游戏详情 class GameInfoScreen extends StatefulWidget { final String gameId; const GameInfoScreen(this.gameId, {Key? key}) : super(key: key); @override State createState() => _GameInfoScreenState(); } class _GameInfoScreenState extends State { late var _future = method.game(widget.gameId); late var _key = UniqueKey(); @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return FutureBuilder( key: _key, future: _future, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return Scaffold( appBar: AppBar( title: Text(tr('app.error')), ), body: ContentError( error: snapshot.error, stackTrace: snapshot.stackTrace, onRefresh: () async { setState(() { _future = method.game(widget.gameId); _key = UniqueKey(); }); }), ); } if (snapshot.connectionState != ConnectionState.done) { return Scaffold( appBar: AppBar( title: Text(tr('app.loading')), ), body: ContentLoading(label: tr('app.loading')), ); } BorderRadius iconRadius = const BorderRadius.all(Radius.circular(6)); double screenShootMargin = 10; double screenShootHeight = 200; TextStyle descriptionStyle = const TextStyle(); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var info = snapshot.data!; return DefaultTabController( length: 2, child: Scaffold( appBar: AppBar( title: Text(info.title), ), body: ListView( children: [ GameTitleCard(info), Container( padding: const EdgeInsets.only( left: 20, right: 20, top: 5, bottom: 10, ), child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(5)), child: MaterialButton( color: Theme.of(context).colorScheme.secondary, textColor: Colors.white, onPressed: () { Navigator.push( context, mixRoute( builder: (context) => GameDownloadScreen(info), ), ); }, child: Container( padding: const EdgeInsets.all(5), child: Text(tr('screen.game_info.download')), ), ), ), ), Container( margin: EdgeInsets.only( top: screenShootMargin, bottom: screenShootMargin, ), height: screenShootHeight, child: ListView( padding: EdgeInsets.only( left: screenShootMargin, right: screenShootMargin, ), scrollDirection: Axis.horizontal, children: info.screenshots .map((e) => Container( margin: EdgeInsets.only( left: screenShootMargin, right: screenShootMargin, ), child: ClipRRect( borderRadius: iconRadius, child: RemoteImage( height: screenShootHeight, fileServer: e.fileServer, path: e.path, ), ), )) .toList(), ), ), Container(height: 20), Column( children: [ Container( height: 40, color: Theme.of(context) .colorScheme .secondary .withOpacity(.025), child: TabBar( tabs: [ Tab(text: tr('screen.game_info.details')), Tab(text: '${tr('screen.game_info.comments')} (${info.commentsCount})'), ], indicatorColor: Theme.of(context).colorScheme.secondary, labelColor: Theme.of(context).colorScheme.secondary, onTap: (val) async { setState(() { _tabIndex = val; }); }, ), ), ], ), _tabIndex == 0 ? Container( padding: const EdgeInsets.all(20), child: Text(info.description, style: descriptionStyle), ) : CommentList(CommentMainType.GAME, info.id), ], ), ), ); }, ); }, ); } var _tabIndex = 0; } ================================================ FILE: lib/screens/GamesScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/ContentBuilder.dart'; import '../basic/config/IconLoading.dart'; import 'GameInfoScreen.dart'; import 'components/Images.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; // 游戏列表 class GamesScreen extends StatefulWidget { const GamesScreen({Key? key}) : super(key: key); @override State createState() => _GamesScreenState(); } class _GamesScreenState extends State { int _currentPage = 1; late Future _future = _loadPage(); late Key _key = UniqueKey(); Future _loadPage() { return method.games(_currentPage); } void _onPageChange(int number) { setState(() { _currentPage = number; _future = _loadPage(); _key = UniqueKey(); }); } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr('screen.games.title')), ), body: ContentBuilder( key: _key, future: _future, onRefresh: _loadPage, successBuilder: (BuildContext context, AsyncSnapshot snapshot) { var page = snapshot.data!; List wraps = []; GameCard? gameCard; for (var element in page.docs) { if (gameCard == null) { gameCard = GameCard(element); } else { wraps.add(Wrap( children: [GameCard(element), gameCard], alignment: WrapAlignment.center, )); gameCard = null; } } if (gameCard != null) { wraps.add(Wrap( children: [gameCard], alignment: WrapAlignment.center, )); } return Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(40), child: Container( padding: const EdgeInsets.only(left: 10, right: 10), decoration: BoxDecoration( border: Border( bottom: BorderSide( width: .5, style: BorderStyle.solid, color: Colors.grey[200]!, ), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ InkWell( onTap: () { _textEditController.clear(); showDialog( context: context, builder: (context) { return AlertDialog( content: Card( child: TextField( controller: _textEditController, decoration: InputDecoration( labelText: tr("app.please_enter_page_number"), ), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.allow( RegExp(r'\d+')), ], ), ), actions: [ MaterialButton( onPressed: () { Navigator.pop(context); }, child: Text(tr('app.cancel')), ), MaterialButton( onPressed: () { Navigator.pop(context); var text = _textEditController.text; if (text.isEmpty || text.length > 5) { return; } var num = int.parse(text); if (num == 0 || num > page.pages) { return; } _onPageChange(num); }, child: Text(tr('app.confirm')), ), ], ); }, ); }, child: Row( children: [ Text("${tr('app.page')} ${page.page} / ${page.pages}"), ], ), ), Row( children: [ MaterialButton( minWidth: 0, onPressed: () { if (page.page > 1) { _onPageChange(page.page - 1); } }, child: Text(tr('app.previous_page')), ), MaterialButton( minWidth: 0, onPressed: () { if (page.page < page.pages) { _onPageChange(page.page + 1); } }, child: Text(tr('app.next_page')), ) ], ), ], ), ), ), body: PikaListView( children: [ ...wraps, ...page.page < page.pages ? [ MaterialButton( onPressed: () { _onPageChange(page.page + 1); }, child: Container( padding: const EdgeInsets.only(top: 30, bottom: 30), child: Text(tr('screen.games.next_page')), ), ), ] : [], ], ), ); }, ), ); } } class GameCard extends StatelessWidget { final GameSimple info; const GameCard(this.info, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { var theme = Theme.of(context); var textColor = theme.textTheme.bodyText1!.color!; var categoriesStyle = TextStyle( fontSize: 13, color: textColor.withAlpha(0xCC), ); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { // data.width/data.height = width/ ? // data.width * ? = width * data.height // ? = width * data.height / data.width var size = MediaQuery.of(context).size; var min = size.width < size.height ? size.width : size.height; var imageWidth = (min - 45 - 40) / 2; var imageHeight = imageWidth * 280 / 500; return Card( child: InkWell( onTap: () { Navigator.push( context, mixRoute( builder: (context) => GameInfoScreen(info.id)), ); }, child: Container( padding: const EdgeInsets.all(10), child: SizedBox( width: imageWidth, child: Column( children: [ RemoteImage( width: imageWidth, height: imageHeight, fileServer: info.icon.fileServer, path: info.icon.path, ), Text( info.title + '\n', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(height: 1.4), strutStyle: const StrutStyle(height: 1.4), ), Text( info.publisher, style: categoriesStyle, maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), ), ), ); }, ); } } final TextEditingController _textEditController = TextEditingController(text: ''); ================================================ FILE: lib/screens/HiddenWordsScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../basic/config/HiddenWords.dart'; import 'components/RightClickPop.dart'; import 'components/ListView.dart'; import 'components/ContentBuilder.dart'; class HiddenWordsScreen extends StatefulWidget { const HiddenWordsScreen({Key? key}) : super(key: key); @override State createState() => _HiddenWordsScreenState(); } class _HiddenWordsScreenState extends State { late Future _future = initHiddenWords(); late Key _key = UniqueKey(); final TextEditingController _textController = TextEditingController(); Future _addWord() async { if (_textController.text.trim().isEmpty) return; await addHiddenWord(_textController.text.trim()); _textController.clear(); setState(() { _future = initHiddenWords(); _key = UniqueKey(); }); } Future _removeWord(String word) async { await removeHiddenWord(word); setState(() { _future = initHiddenWords(); _key = UniqueKey(); }); } Future _clearAll() async { await clearHiddenWords(); setState(() { _future = initHiddenWords(); _key = UniqueKey(); }); } @override void dispose() { _textController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr("settings.hidden_words.title")), actions: [ IconButton( icon: const Icon(Icons.delete_sweep), onPressed: () async { bool? confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: Text(tr("settings.hidden_words.clear_all")), content: Text(tr("settings.hidden_words.clear_all_desc")), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: Text(tr("settings.hidden_words.cancel")), ), TextButton( onPressed: () => Navigator.of(context).pop(true), child: Text(tr("settings.hidden_words.confirm")), ), ], ), ); if (confirm == true) { await _clearAll(); } }, ), ], ), body: Column( children: [ Padding( padding: const EdgeInsets.all(16.0), child: Row( children: [ Expanded( child: TextField( controller: _textController, decoration: InputDecoration( hintText: tr("settings.hidden_words.input_hint"), border: const OutlineInputBorder(), ), onSubmitted: (_) => _addWord(), ), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.add), onPressed: _addWord, ), ], ), ), Expanded( child: ContentBuilder( key: _key, future: _future, onRefresh: () async { setState(() { _future = initHiddenWords(); _key = UniqueKey(); }); }, successBuilder: (BuildContext context, AsyncSnapshot snapshot) { if (hiddenWords.isEmpty) { return Center( child: Text(tr("settings.hidden_words.no_words")), ); } return PikaListView( children: hiddenWords.map((word) => ListTile( title: Text(word), trailing: IconButton( icon: const Icon(Icons.delete), onPressed: () => _removeWord(word), ), )).toList(), ); }, ), ), ], ), ); } } ================================================ FILE: lib/screens/ImportFromOffScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Method.dart'; import '../basic/Channels.dart'; import 'components/ContentLoading.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; class ImportFromOffScreen extends StatefulWidget { final String dbPath; const ImportFromOffScreen({Key? key, required this.dbPath}) : super(key: key); @override State createState() => _ImportFromOffScreenState(); } class _ImportFromOffScreenState extends State { bool _importing = false; String _importMessage = ""; @override void initState() { registerEvent(_onMessageChange, "EXPORT"); _process(); super.initState(); } @override void dispose() { unregisterEvent(_onMessageChange); super.dispose(); } void _onMessageChange(event) { if (event is String) { setState(() { _importMessage = event; }); } } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: !_importing, ); } Widget buildScreen(BuildContext context) { if (_importing) { return Scaffold( body: ContentLoading(label: _importMessage), ); } return Scaffold( appBar: AppBar( title: Text(tr('screen.import_from_off.title')), ), body: PikaListView( children: [ Container( padding: const EdgeInsets.all(10), child: Text(_importMessage), ), ], ), ); } _process() async { try { setState(() { _importing = true; }); await method.importComicViewFormOff(widget.dbPath); setState(() { _importMessage = tr("screen.import_from_off.import_success"); }); } catch (e) { setState(() { _importMessage = "${tr('screen.import_from_off.import_failed')} $e"; }); } finally { setState(() { _importing = false; }); } } } ================================================ FILE: lib/screens/InitScreen.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import 'package:pikapika/basic/config/Address.dart'; import 'package:pikapika/basic/config/AndroidDisplayMode.dart'; import 'package:pikapika/basic/config/AndroidSecureFlag.dart'; import 'package:pikapika/basic/config/AppOrientation.dart'; import 'package:pikapika/basic/config/Authentication.dart'; import 'package:pikapika/basic/config/AutoClean.dart'; import 'package:pikapika/basic/config/AutoDeleteDownloadOnUnfavorite.dart'; import 'package:pikapika/basic/config/AutoDownloadOnFavorite.dart'; import 'package:pikapika/basic/config/DisableAutoDownloadOnMobile.dart'; import 'package:pikapika/basic/config/AutoFullScreen.dart'; import 'package:pikapika/basic/config/AutoFullScreenOnForward.dart'; import 'package:pikapika/basic/config/CategoriesColumnCount.dart'; import 'package:pikapika/basic/config/ChooserRoot.dart'; import 'package:pikapika/basic/config/ContentFailedReloadAction.dart'; import 'package:pikapika/basic/config/CopySkipConfirm.dart'; import 'package:pikapika/basic/config/DragRegionLock.dart'; import 'package:pikapika/basic/config/GestureSpeed.dart'; import 'package:pikapika/basic/config/DownloadAndExportPath.dart'; import 'package:pikapika/basic/config/DownloadThreadCount.dart'; import 'package:pikapika/basic/config/EBookScrolling.dart'; import 'package:pikapika/basic/config/EBookScrollingRange.dart'; import 'package:pikapika/basic/config/EBookScrollingTrigger.dart'; import 'package:pikapika/basic/config/FullScreenAction.dart'; import 'package:pikapika/basic/config/FullScreenUI.dart'; import 'package:pikapika/basic/config/HiddenSearchPersion.dart'; import 'package:pikapika/basic/config/HiddenSubIcon.dart'; import 'package:pikapika/basic/config/IgnoreInfoHistory.dart'; import 'package:pikapika/basic/config/ImageAddress.dart'; import 'package:pikapika/basic/config/ImageFilter.dart'; import 'package:pikapika/basic/config/KeyboardController.dart'; import 'package:pikapika/basic/config/LocalHistorySync.dart'; import 'package:pikapika/basic/config/NoAnimation.dart'; import 'package:pikapika/basic/config/PagerAction.dart'; import 'package:pikapika/basic/config/Platform.dart'; import 'package:pikapika/basic/config/Proxy.dart'; import 'package:pikapika/basic/config/Quality.dart'; import 'package:pikapika/basic/config/ReaderDirection.dart'; import 'package:pikapika/basic/config/ReaderSliderPosition.dart'; import 'package:pikapika/basic/config/ReaderType.dart'; import 'package:pikapika/basic/config/RecommendLinks.dart'; import 'package:pikapika/basic/config/ShadowCategories.dart'; import 'package:pikapika/basic/config/ShowCommentAtDownload.dart'; import 'package:pikapika/basic/config/Themes.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/config/ListLayout.dart'; import 'package:pikapika/basic/config/TimeOffsetHour.dart'; import 'package:pikapika/basic/config/TimeoutLock.dart'; import 'package:pikapika/basic/config/UseApiLoadImage.dart'; import 'package:pikapika/basic/config/UsingRightClickPop.dart'; import 'package:pikapika/basic/config/Version.dart'; import 'package:pikapika/basic/config/VolumeController.dart'; import 'package:pikapika/basic/config/ShadowCategoriesMode.dart'; import 'package:pikapika/basic/config/WillPopNotice.dart'; import 'package:pikapika/basic/config/passed.dart'; import 'package:pikapika/screens/AccessKeyReplaceScreen.dart'; import 'package:pikapika/screens/ComicInfoScreen.dart'; import 'package:pikapika/screens/PkzArchiveScreen.dart'; import 'package:pikapika/screens/calculator_screen.dart'; import 'package:pikapika/screens/components/ContentLoading.dart'; import 'package:uni_links/uni_links.dart'; import 'package:uri_to_file/uri_to_file.dart'; import '../basic/config/CategoriesSort.dart'; import '../basic/config/CopyFullName.dart'; import '../basic/config/CopyFullNameTemplate.dart'; import '../basic/config/DownloadCachePath.dart'; import '../basic/config/ExportPath.dart'; import '../basic/config/ExportRename.dart'; import '../basic/config/HiddenFdIcon.dart'; import '../basic/config/HiddenViewed.dart'; import '../basic/config/HiddenWords.dart'; import '../basic/config/HideOnlineFavorite.dart'; import '../basic/config/IconLoading.dart'; import '../basic/config/IgnoreUpgradeConfirm.dart'; import '../basic/config/IsPro.dart'; import '../basic/config/ReaderBackgroundColor.dart'; import '../basic/config/ReaderScrollByScreenPercentage.dart'; import '../basic/config/WebToonScrollMode.dart'; import '../basic/config/ReaderZoomScale.dart'; import '../basic/config/ReaderTwoPageDirection.dart'; import '../basic/config/ThreeKeepRight.dart'; import '../basic/config/VolumeNextChapter.dart'; import '../basic/config/WebDav.dart'; import 'AccountScreen.dart'; import 'AppScreen.dart'; import 'DownloadOnlyImportScreen.dart'; // 初始化界面 class InitScreen extends StatefulWidget { const InitScreen({Key? key}) : super(key: key); @override State createState() => _InitScreenState(); } class _InitScreenState extends State { var _authenticating = false; Widget? _loadPic; Widget _defaultLoadingPic() { return const ContentLoading(label: "加载中"); } @override initState() { _init(); super.initState(); } Future _init() async { var dataLocal = await method.dataLocal(); print("dataLocal: $dataLocal"); if (await File(p.join(dataLocal, "startup_pic")).exists()) { _loadPic = Image.file( File(p.join(dataLocal, "startup_pic")), fit: BoxFit.contain, ); } else { _loadPic = _defaultLoadingPic(); } setState(() {}); // 初始化配置文件 await initPlatform(); // 必须第一个初始化, 加载设备信息 await initAutoClean(); await initAppOrientation(); await initAddress(); await initImageAddress(); await initProxy(); await initQuality(); await initFont(); await initTheme(); await initFullScreenUI(); await initListLayout(); await initReaderType(); await initReaderDirection(); await initReaderSliderPosition(); await initAutoFullScreen(); await initAutoFullScreenOnForward(); await initFullScreenAction(); await initPagerAction(); await initShadowCategoriesMode(); await initShadowCategories(); await initIconLoading(); await initCategoriesColumnCount(); await initContentFailedReloadAction(); await initVolumeController(); await initKeyboardController(); await initAndroidDisplayMode(); await initChooserRoot(); await initExportPath(); await initTimeZone(); await initDownloadAndExportPath(); await initAndroidSecureFlag(); await initDownloadThreadCount(); await initNoAnimation(); await initDragRegionLock(); await initGestureSpeed(); await initExportRename(); await initVersion(); await initUsingRightClickPop(); await initAuthentication(); await reloadIsPro(); await initIgnoreUpgradeConfirm(); await initWillPopNotice(); await initHiddenFdIcon(); await initShowCommentAtDownload(); await initDownloadCachePath(); await initUseApiLoadImage(); await initWebDav(); await initImageFilter(); await initReaderBackgroundColor(); await initEBookScrolling(); await initEBookScrollingRange(); await initEBookScrollingTrigger(); await initVolumeNextChapter(); await initCopySkipConfirm(); await initCopyFullName(); await initCategoriesSort(); await initLocalHistorySync(); await initHideOnlineFavorite(); await initHiddenViewed(); await initHiddenSubIcon(); await initHiddenSearchPersion(); await initLockTimeOut(); await initReaderTwoPageDirection(); await initHiddenWords(); await initReaderScrollByScreenPercentage(); await initWebToonScrollMode(); await initReaderZoomScale(); await initIgnoreInfoHistory(); await initThreeKeepRight(); await initCopyFullNameTemplate(); await initAutoDownloadOnFavorite(); await initDisableAutoDownloadOnMobile(); await initAutoDeleteDownloadOnUnfavorite(); await initPassed(); await initRecommendLinks(); if (!currentPassed()) { Navigator.pushReplacement(context, MaterialPageRoute( builder: (BuildContext context) { return const CalculatorScreen(); }, )); return; } autoCheckNewVersion(); String? initUrl; if (Platform.isAndroid || Platform.isIOS) { try { initUrl = (await getInitialUri())?.toString(); // Use the uri and warn the user, if it is not correct, // but keep in mind it could be `null`. } on FormatException { // Handle exception by warning the user their action did not succeed // return? } } if (initUrl != null) { var parsed = Uri.parse(initUrl!); if (RegExp(r"^pika://access_key/([0-9A-z:\-]+)/$") .allMatches(initUrl!) .isNotEmpty) { String accessKey = RegExp(r"^pika://access_key/([0-9A-z:\-]+)/$") .allMatches(initUrl!) .first .group(1)!; Navigator.of(context).pushReplacement(mixRoute( builder: (BuildContext context) => AccessKeyReplaceScreen(accessKey: accessKey), )); return; } else if (RegExp(r"^pika://comic/([0-9A-z]+)/$") .allMatches(initUrl!) .isNotEmpty) { String comicId = RegExp(r"^pika://comic/([0-9A-z]+)/$") .allMatches(initUrl!) .first .group(1)!; Navigator.of(context).pushReplacement(mixRoute( builder: (BuildContext context) => ComicInfoScreen(comicId: comicId, holdPkz: true), )); return; } else if (RegExp(r"^https?://pika/comic/([0-9A-z]+)/$") .allMatches(initUrl!) .isNotEmpty) { String comicId = RegExp(r"^https?://pika/comic/([0-9A-z]+)/$") .allMatches(initUrl!) .first .group(1)!; Navigator.of(context).pushReplacement(mixRoute( builder: (BuildContext context) => ComicInfoScreen(comicId: comicId, holdPkz: true), )); return; } else if (RegExp(r"^.*\.pkz$").allMatches(parsed.path).isNotEmpty) { File file = await toFile(initUrl!); Navigator.of(context).pushReplacement(mixRoute( builder: (BuildContext context) => PkzArchiveScreen(pkzPath: file.path, holdPkz: true), )); return; } else if (RegExp(r"^.*\.((pki)|(zip))$") .allMatches(parsed.path) .isNotEmpty) { File file = await toFile(initUrl!); Navigator.of(context).pushReplacement( mixRoute( builder: (BuildContext context) => DownloadOnlyImportScreen(path: file.path, holdPkz: true), ), ); return; } } setState(() { _authenticating = currentAuthentication(); }); if (_authenticating) { _goAuthentication(); } else { syncWebDavIfAuto(context); _goApplication(); } } @override Widget build(BuildContext context) { if (_authenticating) { return Scaffold( appBar: AppBar( title: const Text("身份验证"), ), body: Center( child: Container( padding: const EdgeInsets.all(20), child: MaterialButton( onPressed: () { _goAuthentication(); }, child: const Text('您在之前使用APP时开启了身份验证, 请点这段文字进行身份核查, 核查通过后将会进入APP'), ), ), ), ); } return Scaffold( body: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Center( child: ConstrainedBox( constraints: BoxConstraints( maxWidth: constraints.maxWidth * 4 / 5, maxHeight: constraints.maxHeight * 4 / 5, ), child: _loadPic ?? Container(), ), ); }, ), ); } Future _goApplication() async { // 登录, 如果token失效重新登录, 网络不好的时候可能需要1分钟 if (await method.preLogin()) { // 如果token或username+password有效则直接进入登录好的界面 Navigator.pushReplacement( context, mixRoute(builder: (context) => const AppScreen()), ); } else { // 否则跳转到登录页 Navigator.pushReplacement( context, mixRoute(builder: (context) => const AccountScreen()), ); } } Future _goAuthentication() async { if (await verifyAuthentication(context)) { _goApplication(); } } } ================================================ FILE: lib/screens/LocalFavoriteScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/config/IsPro.dart'; import 'package:pikapika/screens/ComicInfoScreen.dart'; import 'package:pikapika/screens/components/ComicInfoCard.dart'; import 'package:pikapika/screens/components/ContentLoading.dart'; import 'package:pikapika/screens/components/ListView.dart'; import 'package:pikapika/screens/components/RightClickPop.dart'; class LocalFavoriteScreen extends StatefulWidget { const LocalFavoriteScreen({Key? key}) : super(key: key); @override State createState() => _LocalFavoriteScreenState(); } // 将ViewLog转换为ComicSimple ComicSimple _viewLogToComicSimple(ViewLog view) { List categories = []; try { categories = jsonDecode(view.categories); } catch (_) { categories = []; } return ComicSimple.fromJson({ "_id": view.id, "title": view.title, "author": view.author, "pagesCount": view.pagesCount, "epsCount": view.epsCount, "finished": view.finished, "categories": categories, "likesCount": 0, "thumb": { "originalName": view.thumbOriginalName, "fileServer": view.thumbFileServer, "path": view.thumbPath, }, }); } class _LocalFavoriteScreenState extends State { List _folders = []; List _comics = []; String _currentFolderId = 'all'; bool _loading = true; bool _selecting = false; final Set _selectedComicIds = {}; @override void initState() { super.initState(); _loadFolders(); } Future _loadFolders() async { setState(() { _loading = true; }); try { final folders = await method.listLocalFavoriteFolders(); if (!mounted) { return; } bool currentExists = _currentFolderId == 'all'; if (_currentFolderId != 'all') { currentExists = folders.any((f) => f.id == _currentFolderId); } setState(() { _folders = folders; if (!currentExists) { _currentFolderId = 'all'; } }); await _loadComics(); } catch (e) { print("Load folders error: $e"); } finally { if (mounted) { setState(() { _loading = false; }); } } } String _currentFolderTitle() { if (_currentFolderId == 'all') { return tr('local_favorite.all_folders'); } for (final f in _folders) { if (f.id == _currentFolderId) { return f.name; } } return tr('local_favorite.all_folders'); } Future _loadComics() async { setState(() { _loading = true; }); try { List localFavorites; if (_currentFolderId == 'all') { localFavorites = await method.listAllLocalFavoriteComics(); } else { localFavorites = await method.listLocalFavoriteComics(_currentFolderId); } // 获取漫画详情 List comics = []; for (var fav in localFavorites) { if (fav.info != null && fav.info!.isNotEmpty) { try { comics.add(ComicSimple.fromJson( Map.from(jsonDecode(fav.info!)), )); continue; } catch (e) { // Fallback to view log } } try { var view = await method.loadView(fav.comicId); if (view != null) { comics.add(_viewLogToComicSimple(view)); } } catch (e) { print("Load comic ${fav.comicId} error: $e"); } } if (mounted) { setState(() { _comics = comics; }); } } catch (e) { print("Load comics error: $e"); } finally { if (mounted) { setState(() { _loading = false; }); } } } @override Widget build(BuildContext context) { return rightClickPop( child: Scaffold( appBar: AppBar( title: Text(_selecting ? "${tr('local_favorite.title')} · ${tr('local_favorite.select_mode')} (${_selectedComicIds.length})" : "${tr('local_favorite.title')} · ${_currentFolderTitle()}"), actions: [ IconButton( onPressed: _openFolderPicker, icon: const Icon(Icons.folder_open), tooltip: tr('local_favorite.select_folder'), ), _buildMenuButton(), ], ), body: _buildBody(), ), context: context, canPop: true, ); } Future _openFolderPicker() async { String? selected = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(tr('local_favorite.select_folder')), content: SizedBox( width: double.maxFinite, child: ListView( shrinkWrap: true, children: [ ListTile( leading: const Icon(Icons.folder_special), title: Text(tr('local_favorite.all_folders')), onTap: () => Navigator.of(context).pop('all'), ), const Divider(), ..._folders.map( (f) => ListTile( leading: const Icon(Icons.folder), title: Text(f.name), onTap: () => Navigator.of(context).pop(f.id), ), ), const Divider(), ListTile( leading: const Icon(Icons.create_new_folder), title: Text(tr('local_favorite.new_folder')), onTap: () => Navigator.of(context).pop('__CREATE__'), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(tr('app.cancel')), ), ], ); }, ); if (!mounted || selected == null) return; if (selected == '__CREATE__') { await _createFolder(); return; } if (selected != _currentFolderId) { setState(() { _currentFolderId = selected; _selecting = false; _selectedComicIds.clear(); }); await _loadComics(); } } Widget _buildBody() { if (_loading) { return const ContentLoading(label: '加载中'); } if (_folders.isEmpty && _comics.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.folder_open, size: 64, color: Colors.grey), const SizedBox(height: 16), Text( tr('local_favorite.no_folders'), style: const TextStyle(color: Colors.grey), ), const SizedBox(height: 24), ElevatedButton.icon( onPressed: _createFolder, icon: const Icon(Icons.create_new_folder), label: Text(tr('local_favorite.new_folder')), ), ], ), ); } if (_comics.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.book, size: 64, color: Colors.grey), const SizedBox(height: 16), Text( tr('local_favorite.empty_folder'), style: const TextStyle(color: Colors.grey), ), ], ), ); } return ComicList( _comics, ); } Widget ComicList(List comics) { final entries = comics.map((e) { Widget card = GestureDetector( onTap: () { if (_selecting) { setState(() { if (_selectedComicIds.contains(e.id)) { _selectedComicIds.remove(e.id); } else { _selectedComicIds.add(e.id); } }); } else { Navigator.push( context, MaterialPageRoute( builder: (context) => ComicInfoScreen(comicId: e.id), ), ); } }, child: ComicInfoCard(e, linkItem: true), ); if (_selecting) { card = Stack( children: [ card, Positioned( top: 10, right: 10, child: Icon( _selectedComicIds.contains(e.id) ? Icons.check_box : Icons.check_box_outline_blank, color: _selectedComicIds.contains(e.id) ? Theme.of(context).colorScheme.primary : null, ), ), ], ); } return card; }); return PikaListView( children: entries.toList(), ); } Widget _buildMenuButton() { return PopupMenuButton( itemBuilder: (BuildContext context) => >[ PopupMenuItem( value: 10, child: ListTile( leading: const Icon(Icons.create_new_folder), title: Text(tr('local_favorite.new_folder')), ), ), if (_currentFolderId != 'all') PopupMenuItem( value: 11, child: ListTile( leading: const Icon(Icons.delete), title: Text(tr('local_favorite.delete_folder')), ), ), if (!_selecting && _comics.isNotEmpty) PopupMenuItem( value: 20, child: ListTile( leading: Icon( Icons.checklist, ), title: Text( tr('local_favorite.select_mode'), ), ), ), if (_selecting) PopupMenuItem( value: 21, child: ListTile( leading: const Icon(Icons.close), title: Text(tr('local_favorite.cancel_select_mode')), ), ), if (_selecting && _comics.isNotEmpty) PopupMenuItem( value: 22, child: ListTile( leading: const Icon(Icons.select_all), title: Text(tr('local_favorite.select_all')), ), ), if (_selecting && _selectedComicIds.isNotEmpty) PopupMenuItem( value: 1, child: ListTile( leading: const Icon(Icons.drive_file_move), title: Text(tr('local_favorite.move_to_folder')), ), ), if (_selecting && _selectedComicIds.isNotEmpty) PopupMenuItem( value: 0, child: ListTile( leading: Icon( Icons.download, color: isPro ? null : Colors.grey, ), title: Text( tr('local_favorite.batch_download') + (isPro ? "" : " (${tr('app.pro')})"), style: TextStyle( color: isPro ? null : Colors.grey, ), ), ), ), if (_selecting && _selectedComicIds.isNotEmpty) PopupMenuItem( value: 23, child: ListTile( leading: const Icon(Icons.delete_outline), title: Text(tr('local_favorite.remove_selected')), ), ), PopupMenuItem( value: 3, child: ListTile( leading: const Icon(Icons.refresh), title: Text(tr('app.refresh')), ), ), ], onSelected: (int value) { switch (value) { case 10: _createFolder(); break; case 11: _deleteFolder(); break; case 20: setState(() { _selecting = true; _selectedComicIds.clear(); }); break; case 21: setState(() { _selecting = false; _selectedComicIds.clear(); }); break; case 22: setState(() { _selectedComicIds.clear(); for (final c in _comics) { _selectedComicIds.add(c.id); } }); break; case 0: _batchDownload(); break; case 1: _moveToFolder(); break; case 23: _removeSelected(); break; case 3: _loadComics(); break; } }, ); } Future _createFolder() async { int folderCount = await method.countLocalFavoriteFolders(); if (!isPro && folderCount >= 3) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(tr('local_favorite.folder_limit_reached')), ), ); } return; } String? folderName = await showDialog( context: context, builder: (BuildContext context) { final controller = TextEditingController(); return AlertDialog( title: Text(tr('local_favorite.new_folder')), content: TextField( controller: controller, decoration: InputDecoration( hintText: tr('local_favorite.folder_name'), ), autofocus: true, ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text(tr('app.cancel')), ), TextButton( onPressed: () { Navigator.of(context).pop(controller.text.trim()); }, child: Text(tr('app.confirm')), ), ], ); }, ); if (folderName != null && folderName.isNotEmpty) { try { await method.createLocalFavoriteFolder(folderName); await _loadFolders(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.create_success'))), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.create_folder_failed'))), ); } } } } Future _deleteFolder() async { if (_currentFolderId == 'all') return; bool? confirm = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(tr('local_favorite.delete_folder')), content: Text(tr('local_favorite.delete_confirm')), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(false); }, child: Text(tr('app.cancel')), ), TextButton( onPressed: () { Navigator.of(context).pop(true); }, child: Text(tr('app.confirm')), ), ], ); }, ); if (confirm == true) { try { await method.deleteLocalFavoriteFolder(_currentFolderId); _currentFolderId = 'all'; await _loadFolders(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.delete_success'))), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.delete_failed'))), ); } } } } Future _batchDownload() async { if (!_selecting || _selectedComicIds.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.select_comics'))), ); } return; } if (!isPro) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('app.pro_required'))), ); return; } try { await method.downloadAll(_selectedComicIds.toList()); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.download_started'))), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.download_failed'))), ); } } } Future _moveToFolder() async { if (_selectedComicIds.isEmpty) return; List targetFolders = _folders.where((f) => f.id != _currentFolderId).toList(); String? targetFolderId = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(tr('local_favorite.move_to_folder')), content: SizedBox( width: double.maxFinite, child: ListView.builder( shrinkWrap: true, itemCount: targetFolders.length + 1, itemBuilder: (context, index) { if (index == 0) { return ListTile( leading: const Icon(Icons.folder_special), title: Text(tr('local_favorite.all_folders')), onTap: () { Navigator.of(context).pop(''); }, ); } final folder = targetFolders[index - 1]; return ListTile( leading: const Icon(Icons.folder), title: Text(folder.name), onTap: () { Navigator.of(context).pop(folder.id); }, ); }, ), ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text(tr('app.cancel')), ), ], ); }, ); if (targetFolderId != null) { try { await method.moveLocalFavoriteComics( _selectedComicIds.toList(), targetFolderId, ); setState(() { _selecting = false; _selectedComicIds.clear(); }); await _loadComics(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.move_success'))), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.move_failed'))), ); } } } } Future _removeSelected() async { if (_selectedComicIds.isEmpty) return; bool? confirm = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(tr('local_favorite.remove_selected')), content: Text(tr('local_favorite.remove_selected_confirm')), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: Text(tr('app.cancel')), ), TextButton( onPressed: () => Navigator.of(context).pop(true), child: Text(tr('app.confirm')), ), ], ); }, ); if (confirm != true) return; try { final ids = _selectedComicIds.toList(); for (final id in ids) { await method.removeLocalFavoriteComic(id); } if (!mounted) return; setState(() { _selectedComicIds.clear(); _selecting = false; }); await _loadComics(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.remove_selected_success'))), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.remove_selected_failed'))), ); } } } } ================================================ FILE: lib/screens/MigrateScreen.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/ContentBuilder.dart'; import 'package:pikapika/screens/components/ContentLoading.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; // 数据迁移页面 class MigrateScreen extends StatefulWidget { const MigrateScreen({Key? key}) : super(key: key); @override State createState() => _MigrateScreenState(); } class _MigrateScreenState extends State { late final Key _key = UniqueKey(); late final Future _future = _load(); late String _current; late List paths; String _message = ""; int _migrate = 0; // 0 没有开始迁移,1 正在迁移,2 迁移成功,3 迁移失败 Future _load() async { await method.setDownloadRunning(false); _current = await method.dataLocal(); if (Platform.isAndroid) { paths = await method.androidGetExtendDirs(); } } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('数据迁移'), ), body: ContentBuilder( key: _key, future: _future, onRefresh: () async {}, successBuilder: (BuildContext context, AsyncSnapshot snapshot) { switch (_migrate) { case 0: return PikaListView( children: [ Container( padding: const EdgeInsets.all(10), child: const Text( "1. 为了手机数据存储空间不足, 且具有内存卡的安卓手机设计, 可将数据迁移到内存卡上。\n\n" "2. 您在迁移之前, 请确保您的下载处于暂停状态, 或下载均已完成, 以保证您的数据完整性。\n\n" "3. 如果迁移中断, 迁移失败, 或其他原因导致程序无法启动, 图片失效等问题, 您可在程序管理中清除本应用程序的数据, 以回复正常使用。\n\n" "4. 如果您将数据迁移后将内存卡取出, 将会使用默认本地存储, 再次插入同一张内存卡会继续使用该储存卡, 不支持更换内存卡, 途中您若再次迁移会发生数据覆盖, 这必然会丢失一部分数据.\n\n" "5. 您不能更改, 删除, 移动这些数据, 否则程序可能不能正常执行\n\n" "6. 迁移成功之前一定不要退出应用程序, 也不要按返回键\n\n" "7. 如果您已经了解此功能, 悉知文件迁移的风险, 可以在下面的按钮中选择一项执行\n\n", ), ), Container( padding: const EdgeInsets.all(10), child: Text("当前文件储存路径 : $_current"), ), ...paths.map((e) => Container( padding: const EdgeInsets.all(10), child: MaterialButton( color: Theme.of(context).colorScheme.secondary, textColor: Theme.of(context).textTheme.bodyText1?.color, padding: const EdgeInsets.all(10), onPressed: () async { if (!await confirmDialog(context, "文件迁移", "您将要迁移到$e, 迁移过程中一定《 不 要 关 闭 程 序 》")) { return; } setState(() { _migrate = 1; }); try { await method.migrate(e); setState(() { _migrate = 2; }); } catch (ex, tr) { _message = "$ex\n$tr\n"; setState(() { _migrate = 3; }); } }, child: Text("迁移到 $e"), ), )), ], ); case 1: return const ContentLoading(label: "迁移中"); case 2: return const Center(child: Text("迁移成功 您需要关闭应用程序重新启动")); case 3: return Center(child: Text("迁移失败\n$_message")); default: throw ""; } }, ), ); } } ================================================ FILE: lib/screens/ModifyPasswordScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/ContentLoading.dart'; import '../basic/Common.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; class ModifyPasswordScreen extends StatefulWidget { const ModifyPasswordScreen({Key? key}) : super(key: key); @override State createState() => _ModifyPasswordScreenState(); } class _ModifyPasswordScreenState extends State { late bool _loading = false; late String _oldPassword = ""; late String _newPassword = ""; late String _newPasswordRep = ""; @override Widget build(BuildContext context){ return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr("screen.modify_password.title")), ), body: _loading ? Stack( children: [ ContentLoading(label: tr("screen.modify_password.please_wait")), WillPopScope( child: Container(), onWillPop: () async { return false; }, ), ], ) : _buildForm(), ); } Widget _buildForm() { return PikaListView( children: [ const Divider(), ListTile( title: Text(tr("screen.modify_password.old_password")), subtitle: Text(_oldPassword == "" ? tr("screen.modify_password.not_filled") : '\u2022' * 10), onTap: () async { String? input = await displayTextInputDialog( context, src: _oldPassword, title: tr('screen.modify_password.old_password'), hint: tr('screen.modify_password.please_enter_old_password'), isPasswd: true, ); if (input != null) { setState(() { _oldPassword = input; }); } }, ), const Divider(), ListTile( title: Text(tr("screen.modify_password.new_password")), subtitle: Text(_newPassword == "" ? tr("screen.modify_password.not_filled") : '\u2022' * 10), onTap: () async { String? input = await displayTextInputDialog( context, src: _newPassword, title: tr('screen.modify_password.new_password'), hint: tr('screen.modify_password.please_enter_new_password'), isPasswd: true, ); if (input != null) { setState(() { _newPassword = input; }); } }, ), const Divider(), ListTile( title: Text(tr("screen.modify_password.repeat_new_password")), subtitle: Text(_newPasswordRep == "" ? tr("screen.modify_password.not_filled") : '\u2022' * 10), onTap: () async { String? input = await displayTextInputDialog( context, src: _newPasswordRep, title: tr('screen.modify_password.repeat_new_password'), hint: tr('screen.modify_password.please_repeat_new_password'), isPasswd: true, ); if (input != null) { setState(() { _newPasswordRep = input; }); } }, ), const Divider(), Container( margin: const EdgeInsets.all(10), child: MaterialButton( textColor: Colors.white, color: Theme.of(context).appBarTheme.backgroundColor, onPressed: () async { if (_newPasswordRep != _newPassword) { defaultToast(context, tr("screen.modify_password.new_password_mismatch")); return; } setState(() { _loading = true; }); try { await method.updatePassword(_oldPassword, _newPassword); defaultToast(context, tr("screen.modify_password.modify_success")); Navigator.of(context).pop(); } catch (e) { defaultToast(context, "${tr('screen.modify_password.failed')} : $e"); setState(() { _loading = false; }); } }, child: Text(tr("screen.modify_password.confirm")), ), ), ], ); } } ================================================ FILE: lib/screens/NetworkSettingsScreen.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/screens/components/NetworkSetting.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; class NetworkSettingsScreen extends StatelessWidget { const NetworkSettingsScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context){ return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) => Scaffold( appBar: AppBar(title: Text(tr('screen.network_settings.title'))), body: PikaListView( children: const [ NetworkSetting(), ], ), ); } ================================================ FILE: lib/screens/PkzArchiveScreen.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import 'package:permission_handler/permission_handler.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/ContentBuilder.dart'; import 'package:pikapika/screens/components/PkzComicInfoCard.dart'; import 'package:uni_links/uni_links.dart'; import 'package:uri_to_file/uri_to_file.dart'; import '../basic/Common.dart'; import '../basic/Navigator.dart'; import '../basic/config/IconLoading.dart'; import '../basic/config/Platform.dart'; import 'PkzComicInfoScreen.dart'; import 'components/ListView.dart'; class PkzArchiveScreen extends StatefulWidget { final bool holdPkz; final String pkzPath; const PkzArchiveScreen({ Key? key, required this.pkzPath, this.holdPkz = false, }) : super(key: key); @override State createState() => _PkzArchiveScreenState(); } class _PkzArchiveScreenState extends State with RouteAware { Map _logMap = {}; late String _fileName; late Future _future; late Key _key; late PkzArchive _info; StreamSubscription? _linkSubscription; @override void initState() { if (widget.holdPkz) { _linkSubscription = linkSubscript(context); } _fileName = p.basename(widget.pkzPath); _future = _load(); _key = UniqueKey(); super.initState(); } @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe(this, ModalRoute.of(context)!); } @override void dispose() { _linkSubscription?.cancel(); routeObserver.unsubscribe(this); super.dispose(); } @override void didPopNext() { () async { var a = await method.pkzComicViewLogs(_fileName, widget.pkzPath); for (var value in a) { _logMap[value.lastViewComicId] = value; } setState(() {}); }(); } Future _load() async { await method.viewPkz(_fileName, widget.pkzPath); if (Platform.isAndroid) { late bool g; if (androidVersion < 30) { g = await Permission.storage.request().isGranted; }else{ g = await Permission.manageExternalStorage.request().isGranted; } if (!g) { throw 'error permission'; } } _info = await method.pkzInfo(widget.pkzPath); if (_info.comics.length == 1) { Navigator.of(context).pushReplacement(mixRoute( builder: (BuildContext context) => PkzComicInfoScreen( pkzPath: widget.pkzPath, pkzComic: _info.comics.first, holdPkz: widget.holdPkz, ), )); } var a = await method.pkzComicViewLogs(_fileName, widget.pkzPath); for (var value in a) { _logMap[value.lastViewComicId] = value; } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(_fileName), ), body: ContentBuilder( key: _key, future: _future, onRefresh: () async { setState(() { _future = _load(); _key = UniqueKey(); }); }, successBuilder: ( BuildContext context, AsyncSnapshot snapshot, ) { return PikaListView(children: [ ..._info.comics .map((e) => GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { Navigator.of(context).push(mixRoute( builder: (BuildContext context) { return PkzComicInfoScreen( pkzComic: e, pkzPath: widget.pkzPath, ); }, )); }, child: PkzComicInfoCard( info: e, pkzPath: widget.pkzPath, displayViewLog: _logMap[e.id], ), )) .toList(), ]); }, ), ); } } ================================================ FILE: lib/screens/PkzComicInfoScreen.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/PkzReaderScreen.dart'; import '../basic/Common.dart'; import '../basic/Navigator.dart'; import '../basic/config/IconLoading.dart'; import 'components/ListView.dart'; import 'components/PkzComicInfoCard.dart'; class PkzComicInfoScreen extends StatefulWidget { final bool holdPkz; final String pkzPath; final PkzComic pkzComic; const PkzComicInfoScreen({ Key? key, required this.pkzPath, required this.pkzComic, this.holdPkz = false, }) : super(key: key); @override State createState() => _PkzComicInfoScreenState(); } class _PkzComicInfoScreenState extends State with RouteAware { PkzComicViewLog? _log; StreamSubscription? _linkSubscription; @override void initState() { if (widget.holdPkz) { _linkSubscription = linkSubscript(context); } _load(); super.initState(); } @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe(this, ModalRoute.of(context)!); } @override void dispose() { _linkSubscription?.cancel(); routeObserver.unsubscribe(this); super.dispose(); } @override void didPopNext() { () async { _log = await method.pkzComicViewLogByPkzNameAndId( p.basename(widget.pkzPath), widget.pkzComic.id, ); setState(() {}); }(); } _load() async { await method.viewPkzComic( p.basename(widget.pkzPath), widget.pkzPath, widget.pkzComic.id, widget.pkzComic.title, ); _log = await method.pkzComicViewLogByPkzNameAndId( p.basename(widget.pkzPath), widget.pkzComic.id, ); setState(() {}); } @override Widget build(BuildContext context) { List chapterButtons = []; for (var volume in widget.pkzComic.volumes) { for (var chapter in volume.chapters) { chapterButtons.add(MaterialButton( onPressed: () { Navigator.of(context).push(mixRoute( builder: (BuildContext context) { return PkzReaderScreen( comicInfo: widget.pkzComic, currentEpId: chapter.id, pkzPath: widget.pkzPath, ); }, )); }, color: Colors.white, child: Text( chapter.title, style: const TextStyle(color: Colors.black), ), )); } } final theme = Theme.of(context); return Scaffold( appBar: AppBar( title: Text( widget.pkzComic.title, ), ), body: PikaListView(children: [ PkzComicInfoCard(info: widget.pkzComic, pkzPath: widget.pkzPath), Container( padding: const EdgeInsets.only(top: 5, bottom: 5), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: theme.dividerColor, ), ), ), child: Wrap( children: widget.pkzComic.tags.map((e) { return Container( padding: const EdgeInsets.only( left: 10, right: 10, top: 3, bottom: 3, ), margin: const EdgeInsets.only( left: 5, right: 5, top: 3, bottom: 3, ), decoration: BoxDecoration( color: Colors.pink.shade100, border: Border.all( style: BorderStyle.solid, color: Colors.pink.shade400, ), borderRadius: const BorderRadius.all(Radius.circular(30)), ), child: Text( e, style: TextStyle( color: Colors.pink.shade500, height: 1.4, ), strutStyle: const StrutStyle( height: 1.4, ), ), ); }).toList(), ), ), Container( padding: const EdgeInsets.only( top: 5, bottom: 5, left: 10, right: 10, ), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: theme.dividerColor, ), ), ), child: SelectableText( widget.pkzComic.description, style: const TextStyle( fontSize: 13, color: Colors.grey, ), ), ), LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { PkzChapter? first; Map chapters = {}; for (var vol in widget.pkzComic.volumes) { for (var c in vol.chapters) { first ??= c; chapters[c.id] = (c); } } if (chapters.isEmpty) { return Container(); } final width = constraints.maxWidth; return Container( padding: const EdgeInsets.only(left: 10, right: 10), margin: const EdgeInsets.only(top: 10, bottom: 10), width: width, child: MaterialButton( onPressed: () { if (chapters.containsKey(_log?.lastViewEpId)) { Navigator.of(context).push(mixRoute( builder: (BuildContext context) { return PkzReaderScreen( comicInfo: widget.pkzComic, currentEpId: _log!.lastViewEpId, pkzPath: widget.pkzPath, initPicturePosition: _log!.lastViewPictureRank, ); }, )); return; } Navigator.of(context).push(mixRoute( builder: (BuildContext context) { return PkzReaderScreen( comicInfo: widget.pkzComic, currentEpId: first!.id, pkzPath: widget.pkzPath, ); }, )); }, child: Row( children: [ Expanded( child: Container( color: Theme.of(context) .textTheme .bodyText1! .color! .withOpacity(.05), padding: const EdgeInsets.all(10), child: Text( chapters.containsKey(_log?.lastViewEpId) ? "继续阅读 ${chapters[_log?.lastViewEpId]!.title}" : "开始阅读", textAlign: TextAlign.center, ), ), ) ], ), ), ); }, ), Wrap( spacing: 10, runSpacing: 10, alignment: WrapAlignment.spaceAround, children: chapterButtons, ), ]), ); } } ================================================ FILE: lib/screens/PkzReaderScreen.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as p; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/config/AutoFullScreen.dart'; import 'package:pikapika/basic/config/FullScreenUI.dart'; import 'package:pikapika/basic/config/ReaderDirection.dart'; import 'package:pikapika/basic/config/ReaderType.dart'; import 'package:pikapika/basic/Method.dart'; import '../basic/config/IconLoading.dart'; import 'components/ContentError.dart'; import 'components/ContentLoading.dart'; import 'components/ImageReader.dart'; import 'components/RightClickPop.dart'; // 阅读下载的内容 class PkzReaderScreen extends StatefulWidget { final String pkzPath; final PkzComic comicInfo; late final List epList; final String currentEpId; final int? initPicturePosition; final ReaderType pagerType = currentReaderType(); final ReaderDirection pagerDirection = gReaderDirection; late final bool autoFullScreen; PkzReaderScreen({ Key? key, required this.comicInfo, required this.currentEpId, this.initPicturePosition, bool? autoFullScreen, required this.pkzPath, }) : super(key: key) { epList = []; for (var volume in comicInfo.volumes) { for (var chapter in volume.chapters) { epList.add(chapter); } } this.autoFullScreen = autoFullScreen ?? currentAutoFullScreen(); } @override State createState() => _PkzReaderScreenState(); } class _PkzReaderScreenState extends State { late PkzChapter _ep; late int _epOrder; late bool _fullScreen = false; late List pictures = []; late Future _future = _load(); int? _lastChangeRank; bool _replacement = false; @override void initState() { // EP pictures.clear(); for (var ep in widget.epList) { if (ep.id == widget.currentEpId) { _ep = ep; _epOrder = widget.epList.indexOf(ep); pictures.addAll(ep.pictures); break; } } if (widget.autoFullScreen) { setState(() { SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: [], ); _fullScreen = true; }); } // INIT _future = _load(); super.initState(); } @override void dispose() { if (!_replacement) { switchFullScreenUI(); } super.dispose(); } Future _load() async { if (widget.initPicturePosition == null) { await method.viewPkzEpAndPicture( p.basename(widget.pkzPath), widget.pkzPath, widget.comicInfo.id, widget.comicInfo.title, _ep.id, _ep.title, 0, ); } } Future _onPositionChange(int position) async { _lastChangeRank = position; await method.viewPkzEpAndPicture( p.basename(widget.pkzPath), widget.pkzPath, widget.comicInfo.id, widget.comicInfo.title, _ep.id, _ep.title, position, ); return; } FutureOr _onDownload() async { defaultToast(context, tr("screen.pkz_reader.reading_downloaded_comic")); } FutureOr _onChangeEp(int epOrder) { final ep = widget.epList[epOrder]; _replacement = true; Navigator.of(context).pushReplacement( mixRoute( builder: (context) => PkzReaderScreen( comicInfo: widget.comicInfo, pkzPath: widget.pkzPath, currentEpId: ep.id, autoFullScreen: _fullScreen, ), ), ); } FutureOr _onReloadEp() { _replacement = true; Navigator.of(context).pushReplacement( mixRoute( builder: (context) => PkzReaderScreen( comicInfo: widget.comicInfo, currentEpId: widget.currentEpId, initPicturePosition: _lastChangeRank ?? widget.initPicturePosition, // maybe null autoFullScreen: _fullScreen, pkzPath: widget.pkzPath, ), ), ); } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return readerKeyboardHolder(_build(context)); } Widget _build(BuildContext context) { return FutureBuilder( future: _future, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return Scaffold( appBar: _fullScreen ? null : AppBar( title: Text("${_ep.title} - ${widget.comicInfo.title}"), ), body: ContentError( error: snapshot.error, stackTrace: snapshot.stackTrace, onRefresh: () async { setState(() { _future = _load(); }); }, ), ); } if (snapshot.connectionState != ConnectionState.done) { return Scaffold( appBar: _fullScreen ? null : AppBar( title: Text("${_ep.title} - ${widget.comicInfo.title}"), ), body: ContentLoading(label: tr('app.loading')), ); } var epNameMap = {}; for (var i = 0; i < widget.epList.length; i++) { epNameMap[i] = widget.epList[i].title; } return Scaffold( body: ImageReader( ImageReaderStruct( images: pictures .map((e) => ReaderImageInfo( "", "", "", e.width, e.height, e.format, 0, pkzFile: PkzFile(widget.pkzPath, e.picturePath), )) .toList(), fullScreen: _fullScreen, onFullScreenChange: _onFullScreenChange, onPositionChange: _onPositionChange, initPosition: widget.initPicturePosition, epOrder: _epOrder, epNameMap: epNameMap, comicTitle: widget.comicInfo.title, onReloadEp: _onReloadEp, onChangeEp: _onChangeEp, onDownload: _onDownload, ), ), ); }, ); } Future _onFullScreenChange(bool fullScreen) async { setState(() { if (fullScreen) { if (Platform.isAndroid || Platform.isIOS) { SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: [], ); } } else { switchFullScreenUI(); } _fullScreen = fullScreen; }); } } ================================================ FILE: lib/screens/ProScreen.dart ================================================ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:pikapika/i18.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/AccessKeyReplaceScreen.dart'; import '../basic/config/IconLoading.dart'; import '../basic/config/IsPro.dart'; import 'components/ListView.dart'; class ProScreen extends StatefulWidget { const ProScreen({Key? key}) : super(key: key); @override State createState() => _ProScreenState(); } class _ProScreenState extends State { String _username = ""; @override void initState() { method.getUsername().then((value) { setState(() { _username = value; }); }); proEvent.subscribe(_setState); super.initState(); } @override void dispose() { proEvent.unsubscribe(_setState); super.dispose(); } _setState(_) { setState(() {}); } @override Widget build(BuildContext context) { var size = MediaQuery.of(context).size; var min = size.width < size.height ? size.width : size.height; return Scaffold( appBar: AppBar( title: Text(tr('screen.pro.title')), ), body: PikaListView( children: [ SizedBox( width: min / 2, height: min / 2, child: Center( child: Icon( isPro ? Icons.offline_bolt : Icons.offline_bolt_outlined, size: min / 3, color: Colors.grey.shade500, ), ), ), Center(child: Text(_username)), Container(height: 20), const Divider(), Padding( padding: const EdgeInsets.all(20), child: Text(tr('screen.pro.power_guide')), ), const Divider(), Row( children: [ Expanded( child: ListTile( title: Text(tr('screen.pro.sign_in_exchange')), subtitle: Text( proInfoAf.isPro ? "${tr('screen.pro.powered')} (${DateTime.fromMillisecondsSinceEpoch(1000 * proInfoAf.expire).toString()})" : tr('screen.pro.not_powered'), ), ), ), Expanded( child: ListTile( title: Text(tr('screen.pro.pat_membership')), subtitle: Text( proInfoPat.isPro ? tr('screen.pro.powered') : tr('screen.pro.not_powered'), ), onTap: () { defaultToast(context, tr('screen.pro.click_pat_to_change')); }, ), ), ], ), const Divider(), ListTile( title: Text(tr('screen.pro.i_have_powered')), onTap: () async { try { await method.reloadPro(); defaultToast(context, "SUCCESS"); } catch (e, s) { defaultToast(context, "FAIL"); } await reloadIsPro(); setState(() {}); }, ), const Divider(), ListTile( title: Text(tr('screen.pro.i_just_powered')), onTap: () async { var code = await inputString(context, tr('screen.pro.enter_code')); if (code != null) { code = code.trim(); if (code.isNotEmpty) { try { await method.inputCdKey(code); defaultToast(context, "SUCCESS"); } catch (e, s) { defaultToast(context, "FAIL"); } } } await reloadIsPro(); setState(() {}); }, ), const Divider(), const ProServerNameWidget(), const Divider(), ...patPro(), const Divider(), const Divider(), ], ), ); } List patPro() { List widgets = []; if (proInfoPat.accessKey.isNotEmpty) { var text = tr('screen.pro.key_recorded'); if (proInfoPat.patId.isNotEmpty) { text += "\n${tr('screen.pro.pat_account')} : ${proInfoPat.patId}"; } if (proInfoPat.bindUid.isNotEmpty) { text += "\n${tr('screen.pro.bind_pika_account')} : ${proInfoPat.bindUid}"; } if (proInfoPat.requestDelete > 0) { DateTime dateTime = DateTime.fromMillisecondsSinceEpoch( proInfoPat.requestDelete * 1000, isUtc: true, ); String formattedDate = DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime.toLocal()); text += "\n${tr('screen.pro.bind_account_time')} : $formattedDate"; } if (proInfoPat.reBind > 0) { DateTime dateTime = DateTime.fromMillisecondsSinceEpoch( proInfoPat.reBind * 1000, isUtc: true, ); String formattedDate = DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime.toLocal()); text += "\n${tr('screen.pro.rebind_time')} : $formattedDate"; } List append = []; if (proInfoPat.bindUid == "") { append.add(TextSpan( text: "\n(${tr('screen.pro.pat_bind_hint')})", style: const TextStyle(color: Colors.blue), )); } else if (proInfoPat.bindUid != _username) { append.add(TextSpan( text: "\n(${tr('screen.pro.pat_rebind_hint')})", style: const TextStyle(color: Colors.red), )); } else if (proInfoPat.isPro == false) { append.add(TextSpan( text: "\n(${tr('screen.pro.pat_not_detected')})", style: const TextStyle(color: Colors.orange), )); } else { append.add(TextSpan( text: "\n(${tr('screen.pro.pat_normal')})", style: const TextStyle(color: Colors.green), )); } widgets.add(ListTile( onTap: () async { print(jsonEncode(proInfoPat)); var choose = await chooseMapDialog( context, { tr('screen.pro.update_pat_status'): 2, tr('screen.pro.bind_to_account'): 3, tr('screen.pro.change_pat_key'): 1, tr('screen.pro.clear_pat_info'): 4, }, tr('app.please_select'), ); switch (choose) { case 1: addPatAccount(); break; case 2: reloadPatAccount(); break; case 3: bindThisAccount(); break; case 4: clearPat(); break; } }, title: Text(tr('screen.pro.pat_membership')), subtitle: Text.rich(TextSpan(children: [ TextSpan(text: text), ...append, ])), )); } else { widgets.add(ListTile( onTap: () { addPatAccount(); }, title: Text(tr('screen.pro.pat_membership')), subtitle: Text(tr('screen.pro.click_to_bind')), )); } return widgets; } void addPatAccount() async { print(jsonEncode(proInfoPat)); String? key = await inputString(context, tr('screen.pro.enter_auth_code')); if (key != null) { await Navigator.of(context) .push(mixRoute(builder: (BuildContext context) { return AccessKeyReplaceScreen(accessKey: key); })); } } reloadPatAccount() async { defaultToast(context, tr('screen.pro.please_wait')); try { await method.reloadPatAccount(); await reloadIsPro(); defaultToast(context, "SUCCESS"); } catch (e) { defaultToast(context, "FAIL : $e"); } finally {} } bindThisAccount() async { defaultToast(context, tr('screen.pro.please_wait')); try { await method.bindThisAccount(); await method.reloadPatAccount(); await reloadIsPro(); defaultToast(context, "SUCCESS"); } catch (e) { defaultToast(context, "FAIL : $e"); } finally {} } clearPat() async { await method.clearPat(); await reloadIsPro(); defaultToast(context, "Success"); } } class ProServerNameWidget extends StatefulWidget { const ProServerNameWidget({Key? key}) : super(key: key); @override State createState() => _ProServerNameWidgetState(); } class _ProServerNameWidgetState extends State { String _serverName = ""; @override void initState() { method.getProServerName().then((value) { setState(() { _serverName = value; }); }); super.initState(); } @override Widget build(BuildContext context) { return ListTile( title: Text(tr('screen.pro.power_method')), subtitle: Text(_loadServerName()), onTap: () async { final serverName = await chooseMapDialog( context, { tr('screen.pro.wind_power'): "HK", tr('screen.pro.hydro_power'): "US", tr('screen.pro.solar_power'): "SIG", tr('screen.pro.nuclear_power'): "JPOS", }, tr('screen.pro.choose_power_method'), ); if (serverName != null && serverName.isNotEmpty) { await method.setProServerName(serverName); setState(() { _serverName = serverName; }); } }, ); } String _loadServerName() { switch (_serverName) { case "HK": return tr('screen.pro.wind_power'); case "US": return tr('screen.pro.hydro_power'); case "SIG": return tr('screen.pro.solar_power'); case "JPOS": return tr('screen.pro.nuclear_power'); default: return ""; } } } ================================================ FILE: lib/screens/RandomComicsScreen.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/i18.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/config/ListLayout.dart'; import 'package:pikapika/basic/config/ShadowCategories.dart'; import '../basic/config/Address.dart'; import 'components/ComicListBuilder.dart'; import 'components/Common.dart'; import 'components/RightClickPop.dart'; // 随机漫画页面 class RandomComicsScreen extends StatefulWidget { const RandomComicsScreen({Key? key}) : super(key: key); @override State createState() => _RandomComicsScreenState(); } class _RandomComicsScreenState extends State { @override Widget build(BuildContext context){ return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr('screen.random_comics.title')), actions: [ commonPopMenu(context), addressPopMenu(context), ], ), body: ComicListBuilder(method.randomComics), ); } } ================================================ FILE: lib/screens/RankingsScreen.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/i18.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/Avatar.dart'; import 'package:pikapika/screens/components/ContentBuilder.dart'; import '../basic/Cross.dart'; import '../basic/Navigator.dart'; import '../basic/config/Address.dart'; import 'ComicsScreen.dart'; import 'components/ComicListBuilder.dart'; import 'components/Common.dart'; import 'components/FitButton.dart'; import 'components/RightClickPop.dart'; // 排行榜 class RankingsScreen extends StatelessWidget { const RankingsScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { var theme = Theme.of(context); return Scaffold( appBar: AppBar( title: Text(tr('screen.rankings.title')), actions: [ commonPopMenu(context), addressPopMenu(context), ], ), body: DefaultTabController( length: 4, child: Column( children: [ Container( height: 40, color: theme.colorScheme.secondary.withOpacity(.025), child: TabBar( indicatorColor: theme.colorScheme.secondary, labelColor: theme.colorScheme.secondary, tabs: [ Tab(text: tr('screen.rankings.day')), Tab(text: tr('screen.rankings.week')), Tab(text: tr('screen.rankings.month')), Tab(text: tr('screen.rankings.knight')), ], ), ), const Expanded( child: TabBarView( children: [ _Leaderboard("H24"), _Leaderboard("D7"), _Leaderboard("D30"), _KnightLeaderBoard(), ], ), ), ], ), ), ); } } class _Leaderboard extends StatefulWidget { final String type; const _Leaderboard(this.type); @override State createState() => _LeaderboardState(); } class _LeaderboardState extends State<_Leaderboard> { @override Widget build(BuildContext context) { return ComicListBuilder(() => method.leaderboard(widget.type)); } } class _KnightLeaderBoard extends StatefulWidget { const _KnightLeaderBoard(); @override State createState() => _KnightLeaderBoardState(); } class _KnightLeaderBoardState extends State<_KnightLeaderBoard> { Future> _future = method.leaderboardOfKnight(); Key _key = UniqueKey(); @override Widget build(BuildContext context) { return ContentBuilder( key: _key, future: _future, onRefresh: () async { setState(() { _future = method.leaderboardOfKnight(); _key = UniqueKey(); }); }, successBuilder: ( BuildContext context, AsyncSnapshot> snapshot, ) { return RefreshIndicator( onRefresh: () async { setState(() { _future = method.leaderboardOfKnight(); }); }, child: ListView(children: [ ...snapshot.requireData.map(_knightCard).toList(), SizedBox( height: 80, child: FitButton( text: tr('screen.rankings.refresh'), onPressed: () async { setState(() { _future = method.leaderboardOfKnight(); }); }, ), ), ]), ); }, ); } Widget _knightCard(Knight e) { final theme = Theme.of(context); var nameStyle = const TextStyle(fontWeight: FontWeight.bold); var levelStyle = TextStyle( fontSize: 12, color: theme.colorScheme.secondary.withOpacity(.8)); var connectStyle = TextStyle(color: theme.textTheme.bodyText1?.color?.withOpacity(.8)); var datetimeStyle = TextStyle( color: theme.textTheme.bodyText1?.color?.withOpacity(.6), fontSize: 12); final card = Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( border: Border( top: BorderSide( width: .25, style: BorderStyle.solid, color: Colors.grey.shade500.withOpacity(.5), ), bottom: BorderSide( width: .25, style: BorderStyle.solid, color: Colors.grey.shade500.withOpacity(.5), ), ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Avatar(e.avatar), Container(width: 5), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text(e.name, style: nameStyle), Expanded(child: Container()), Text( "${e.comicsUploaded} ${tr('screen.rankings.comics_count')}", style: datetimeStyle, ), ], ), Text("Lv. ${e.level} (${e.title})", style: levelStyle), Text(e.slogan ?? "", style: connectStyle), ], ), ), ], ), ); return InkWell( onTap: () { navPushOrReplace( context, (context) => ComicsScreen( creatorId: e.id, creatorName: e.name, ), ); }, onLongPress: () { confirmCopy( context, e.name, ); }, child: card, ); } } ================================================ FILE: lib/screens/RegisterScreen.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/i18.dart'; // import 'package:flutter_datetime_picker/flutter_datetime_picker.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/NetworkSetting.dart'; import 'package:pikapika/screens/components/RightClickPop.dart'; import 'components/ContentLoading.dart'; import 'components/ListView.dart'; /// 注册页面 class RegisterScreen extends StatefulWidget { const RegisterScreen({Key? key}) : super(key: key); @override State createState() => _RegisterScreenState(); } class _RegisterScreenState extends State { late bool _registering = false; late bool _registerOver = false; late String _email = ""; late String _name = ""; late String _password = ""; late String _gender = "bot"; late String _birthday = "2000-01-01"; late String _question1 = "问题1"; late String _answer1 = "回答1"; late String _question2 = "问题2"; late String _answer2 = "回答2"; late String _question3 = "问题3"; late String _answer3 = "回答3"; Future _register() async { setState(() { _registering = true; }); try { var mustList = [ _email, _name, _password, _gender, _birthday, _question1, _answer1, _question2, _answer2, _question3, _answer3, ]; for (var a in mustList) { if (a.isEmpty) { throw tr('screen.register.check_form'); } } await method.register( _email, _name, _password, _gender, _birthday, _question1, _answer1, _question2, _answer2, _question3, _answer3, ); await method.setUsername(_email); await method.setPassword(_password); await method.clearToken(); setState(() { _registerOver = true; }); } catch (e) { String message = "$e"; if (message.contains("email is already exist")) { message = tr('screen.register.account_exists'); } else if (message.contains("name is already exist")) { message = tr('screen.register.name_exists'); } alertDialog(context, tr('screen.register.register_failed'), message); } finally { setState(() { _registering = false; }); } } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { if (_registerOver) { return Scaffold( appBar: AppBar( title: Text(tr('screen.register.register_success')), ), body: Center( child: Column( children: [ Expanded(child: Container()), Text(tr('screen.register.register_success_desc')), Text('${tr('screen.register.account_label')} : $_email'), Text('${tr('screen.register.nickname_label')} : $_name'), Expanded(child: Container()), Expanded(child: Container()), ], ), ), ); } if (_registering) { return Scaffold( appBar: AppBar(), body: ContentLoading(label: tr('screen.register.registering')), ); } return Scaffold( appBar: AppBar( title: Text(tr('screen.register.title')), actions: [ IconButton( onPressed: () => _register(), icon: const Icon(Icons.check), ), ], ), body: PikaListView( children: [ const Divider(), ListTile( title: Text(tr('screen.register.account')), subtitle: Text(_email == "" ? tr('screen.register.not_set') : _email), onTap: () async { String? input = await displayTextInputDialog( context, src: _email, title: tr('screen.register.account'), hint: tr('screen.register.please_enter_account'), desc: tr('screen.register.account_desc'), ); if (input != null) { setState(() { _email = input; }); } }, ), ListTile( title: Text(tr('screen.register.password')), subtitle: Text(_password == "" ? tr('screen.register.not_set') : '\u2022' * 10), onTap: () async { String? input = await displayTextInputDialog( context, src: _password, title: tr('screen.register.password'), hint: tr('screen.register.please_enter_password'), desc: tr('screen.register.password_desc'), isPasswd: true, ); if (input != null) { setState(() { _password = input; }); } }, ), ListTile( title: Text(tr('screen.register.nickname')), subtitle: Text(_name == "" ? tr('screen.register.not_set') : _name), onTap: () async { String? input = await displayTextInputDialog( context, src: _name, title: tr('screen.register.nickname'), hint: tr('screen.register.please_enter_nickname'), desc: tr('screen.register.nickname_desc'), ); if (input != null) { setState(() { _name = input; }); } }, ), const Divider(), ListTile( title: Text(tr('screen.register.gender')), subtitle: Text(_genderText(_gender)), onTap: () async { String? result = await showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: Text(tr('screen.register.choose_gender')), children: [ SimpleDialogOption( child: Text(tr('screen.register.futa')), onPressed: () { Navigator.pop(context, 'bot'); }, ), SimpleDialogOption( child: Text(tr('screen.register.male')), onPressed: () { Navigator.pop(context, 'm'); }, ), SimpleDialogOption( child: Text(tr('screen.register.female')), onPressed: () { Navigator.pop(context, 'f'); }, ), ], ); }, ); if (result != null) { setState(() { _gender = result; }); } }, ), ListTile( title: Text(tr('screen.register.birthday')), subtitle: Text(_birthday), ), const Divider(), ListTile( title: Text(tr('screen.register.question_1')), subtitle: Text(_question1 == "" ? tr('screen.register.not_set') : _question1), onTap: () async { String? input = await displayTextInputDialog( context, src: _question1, title: tr('screen.register.question_1'), hint: tr('screen.register.please_enter_question_1'), ); if (input != null) { setState(() { _question1 = input; }); } }, ), ListTile( title: Text(tr('screen.register.answer_1')), subtitle: Text(_answer1 == "" ? tr('screen.register.not_set') : _answer1), onTap: () async { String? input = await displayTextInputDialog( context, src: _answer1, title: tr('screen.register.answer_1'), hint: tr('screen.register.please_enter_answer_1'), ); if (input != null) { setState(() { _answer1 = input; }); } }, ), ListTile( title: Text(tr('screen.register.question_2')), subtitle: Text(_question2 == "" ? tr('screen.register.not_set') : _question2), onTap: () async { String? input = await displayTextInputDialog( context, src: _question2, title: tr('screen.register.question_2'), hint: tr('screen.register.please_enter_question_2'), ); if (input != null) { setState(() { _question2 = input; }); } }, ), ListTile( title: Text(tr('screen.register.answer_2')), subtitle: Text(_answer2 == "" ? tr('screen.register.not_set') : _answer2), onTap: () async { String? input = await displayTextInputDialog( context, src: _answer2, title: tr('screen.register.answer_2'), hint: tr('screen.register.please_enter_answer_2'), ); if (input != null) { setState(() { _answer2 = input; }); } }, ), ListTile( title: Text(tr('screen.register.question_3')), subtitle: Text(_question3 == "" ? tr('screen.register.not_set') : _question3), onTap: () async { String? input = await displayTextInputDialog( context, src: _question3, title: tr('screen.register.question_3'), hint: tr('screen.register.please_enter_question_3'), ); if (input != null) { setState(() { _question3 = input; }); } }, ), ListTile( title: Text(tr('screen.register.answer_3')), subtitle: Text(_answer3 == "" ? tr('screen.register.not_set') : _answer3), onTap: () async { String? input = await displayTextInputDialog( context, src: _answer3, title: tr('screen.register.answer_3'), hint: tr('screen.register.please_enter_answer_3'), ); if (input != null) { setState(() { _answer3 = input; }); } }, ), const Divider(), const NetworkSetting(), const Divider(), ], ), ); } String _genderText(String gender) { switch (gender) { case 'bot': return tr('screen.register.futa'); case "m": return tr('screen.register.male'); case "f": return tr('screen.register.female'); default: return ""; } } } ================================================ FILE: lib/screens/SearchAuthorScreen.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/i18.dart'; import '../basic/config/PagerAction.dart'; import 'components/flutter_search_bar.dart' as fsb; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/config/ShadowCategories.dart'; import 'package:pikapika/basic/store/Categories.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/RightClickPop.dart'; import '../basic/Entities.dart'; import '../basic/config/Address.dart'; import '../basic/config/IconLoading.dart'; import 'components/ComicList.dart'; import 'components/ComicPager.dart'; import 'components/Common.dart'; import 'components/GoDownloadSelect.dart'; class ComicSearchAuthorScreenButton extends StatelessWidget { const ComicSearchAuthorScreenButton({Key? key}) : super(key: key); @override Widget build(BuildContext context) => IconButton( icon: const Icon(Icons.person_search), onPressed: () async { Navigator.push( context, MaterialPageRoute( builder: (context) => const SearchAuthorScreen( author: '', ), ), ); }, ); } // 搜索页面 class SearchAuthorScreen extends StatefulWidget { final String author; final String? category; const SearchAuthorScreen({ Key? key, required this.author, this.category, }) : super(key: key); @override State createState() => _SearchAuthorScreenState(); } class _SearchAuthorScreenState extends State { late final _comicListController = ComicListController(); late final TextEditingController _textEditController = TextEditingController(text: widget.author); late final fsb.SearchBar _searchBar = fsb.SearchBar( hintText: '${tr('screen.search_author.search_hint')}${categoryTitle(widget.category)}', controller: _textEditController, inBar: false, setState: setState, onSubmitted: (value) { if (value.isNotEmpty) { Navigator.pushReplacement( context, mixRoute( builder: (context) => SearchAuthorScreen( author: value, category: widget.category, ), ), ); } }, buildDefaultAppBar: (BuildContext context) { return AppBar( title: Text("${tr('screen.search_author.by_author')}${widget.author} + ${categoryTitle(widget.category)}"), actions: [ commonPopMenu( context, setState: setState, comicListController: _comicListController, ), addressPopMenu(context), _chooseCategoryAction(), _searchBar.getSearchAction(context), ], ); }, ); Widget _chooseCategoryAction() => IconButton( onPressed: () async { String? category = await chooseListDialog(context, tr('screen.search.choose_category'), [ categoryTitle(null), ...filteredList( storedCategories, (c) => !shadowCategories.contains(c), ), ]); if (category != null) { if (category == categoryTitle(null)) { category = null; } Navigator.of(context).pushReplacement(mixRoute( builder: (context) { return SearchAuthorScreen( category: category, author: widget.author, ); }, )); } }, icon: const Icon(Icons.category), ); Future _fetch(String _currentSort, int _currentPage) { if (currentPagerAction() == PagerAction.CONTROLLER && _comicListController.selecting) { setState(() { _comicListController.selecting = false; }); } if (widget.category == null) { return method.comics( _currentSort, _currentPage, author: widget.author, ); } else { return method.comics( _currentSort, _currentPage, author: widget.author, category: widget.category!, ); } } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { var a = Scaffold( appBar: _comicListController.selecting ? downAppBar(context, _comicListController, setState) : _searchBar.build(context), body: ComicPager( fetchPage: _fetch, comicListController: _comicListController, ), ); return WillPopScope( onWillPop: () async { if (_comicListController.selecting) { setState(() { _comicListController.selecting = false; }); return false; } return true; }, child: a, ); } } ================================================ FILE: lib/screens/SearchScreen.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/i18.dart'; import '../basic/config/PagerAction.dart'; import 'components/flutter_search_bar.dart' as fsb; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/config/ShadowCategories.dart'; import 'package:pikapika/basic/store/Categories.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/RightClickPop.dart'; import '../basic/Entities.dart'; import '../basic/config/Address.dart'; import '../basic/config/IconLoading.dart'; import 'components/ComicList.dart'; import 'components/ComicPager.dart'; import 'components/Common.dart'; import 'components/GoDownloadSelect.dart'; // 搜索页面 class SearchScreen extends StatefulWidget { final String keyword; final String? category; const SearchScreen({ Key? key, required this.keyword, this.category, }) : super(key: key); @override State createState() => _SearchScreenState(); } class _SearchScreenState extends State { late final _comicListController = ComicListController(); late final TextEditingController _textEditController = TextEditingController(text: widget.keyword); late final fsb.SearchBar _searchBar = fsb.SearchBar( hintText: '${tr('screen.search.search_hint')} ${categoryTitle(widget.category)}', controller: _textEditController, inBar: false, setState: setState, onSubmitted: (value) { if (value.isNotEmpty) { Navigator.pushReplacement( context, mixRoute( builder: (context) => SearchScreen( keyword: value, category: widget.category, ), ), ); } }, buildDefaultAppBar: (BuildContext context) { return AppBar( title: Text("${categoryTitle(widget.category)} ${widget.keyword}"), actions: [ commonPopMenu( context, setState: setState, comicListController: _comicListController, ), addressPopMenu(context), _chooseCategoryAction(), _searchBar.getSearchAction(context), ], ); }, ); Widget _chooseCategoryAction() => IconButton( onPressed: () async { String? category = await chooseListDialog(context, tr('screen.search.choose_category'), [ categoryTitle(null), ...filteredList( storedCategories, (c) => !shadowCategories.contains(c), ), ]); if (category != null) { if (category == categoryTitle(null)) { category = null; } Navigator.of(context).pushReplacement(mixRoute( builder: (context) { return SearchScreen( category: category, keyword: widget.keyword, ); }, )); } }, icon: const Icon(Icons.category), ); Future _fetch(String _currentSort, int _currentPage) { if (currentPagerAction() == PagerAction.CONTROLLER && _comicListController.selecting) { setState(() { _comicListController.selecting = false; }); } if (widget.category == null) { return method.searchComics(widget.keyword, _currentSort, _currentPage); } else { return method.searchComicsInCategories( widget.keyword, _currentSort, _currentPage, [widget.category!], ); } } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { var a = Scaffold( appBar: _comicListController.selecting ? downAppBar(context, _comicListController, setState) : _searchBar.build(context), body: ComicPager( fetchPage: _fetch, comicListController: _comicListController, ), ); return WillPopScope( onWillPop: () async { if (_comicListController.selecting) { setState(() { _comicListController.selecting = false; }); return false; } return true; }, child: a, ); } } ================================================ FILE: lib/screens/SettingsScreen.dart ================================================ import 'dart:io'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/config/AndroidDisplayMode.dart'; import 'package:pikapika/basic/config/AndroidSecureFlag.dart'; import 'package:pikapika/basic/config/AutoClean.dart'; import 'package:pikapika/basic/config/AutoDeleteDownloadOnUnfavorite.dart'; import 'package:pikapika/basic/config/AutoDownloadOnFavorite.dart'; import 'package:pikapika/basic/config/DisableAutoDownloadOnMobile.dart'; import 'package:pikapika/basic/config/AutoFullScreen.dart'; import 'package:pikapika/basic/config/AutoFullScreenOnForward.dart'; import 'package:pikapika/basic/config/ChooserRoot.dart'; import 'package:pikapika/basic/config/ContentFailedReloadAction.dart'; import 'package:pikapika/basic/config/CopySkipConfirm.dart'; import 'package:pikapika/basic/config/DownloadAndExportPath.dart'; import 'package:pikapika/basic/config/DownloadThreadCount.dart'; import 'package:pikapika/basic/config/EBookScrollingRange.dart'; import 'package:pikapika/basic/config/EBookScrollingTrigger.dart'; import 'package:pikapika/basic/config/ExportRename.dart'; import 'package:pikapika/basic/config/FullScreenAction.dart'; import 'package:pikapika/basic/config/FullScreenUI.dart'; import 'package:pikapika/basic/config/HiddenSearchPersion.dart'; import 'package:pikapika/basic/config/IconLoading.dart'; import 'package:pikapika/basic/config/IgnoreUpgradeConfirm.dart'; import 'package:pikapika/basic/config/IsPro.dart'; import 'package:pikapika/basic/config/KeyboardController.dart'; import 'package:pikapika/basic/config/NoAnimation.dart'; import 'package:pikapika/basic/config/DragRegionLock.dart'; import 'package:pikapika/basic/config/GestureSpeed.dart'; import 'package:pikapika/basic/config/PagerAction.dart'; import 'package:pikapika/basic/config/Quality.dart'; import 'package:pikapika/basic/config/ReaderBackgroundColor.dart'; import 'package:pikapika/basic/config/ReaderDirection.dart'; import 'package:pikapika/basic/config/ReaderSliderPosition.dart'; import 'package:pikapika/basic/config/ReaderType.dart'; import 'package:pikapika/basic/config/ShadowCategories.dart'; import 'package:pikapika/basic/config/ShadowCategoriesMode.dart'; import 'package:pikapika/basic/config/ShowCommentAtDownload.dart'; import 'package:pikapika/basic/config/Themes.dart'; import 'package:pikapika/basic/config/TimeOffsetHour.dart'; import 'package:pikapika/basic/config/VolumeController.dart'; import 'package:pikapika/basic/config/VolumeNextChapter.dart'; import 'package:pikapika/screens/components/NetworkSetting.dart'; import 'package:pikapika/screens/components/RightClickPop.dart'; import '../basic/config/AppOrientation.dart'; import '../basic/config/Authentication.dart'; import '../basic/config/CategoriesColumnCount.dart'; import '../basic/config/CategoriesSort.dart'; import '../basic/config/CopyFullName.dart'; import '../basic/config/CopyFullNameTemplate.dart'; import '../basic/config/DownloadCachePath.dart'; import '../basic/config/EBookScrolling.dart'; import '../basic/config/HiddenFdIcon.dart'; import '../basic/config/HiddenSubIcon.dart'; import '../basic/config/HiddenViewed.dart'; import '../basic/config/HiddenWords.dart'; import '../basic/config/HideOnlineFavorite.dart'; import '../basic/config/IgnoreInfoHistory.dart'; import '../basic/config/ImageFilter.dart'; import '../basic/config/ReaderScrollByScreenPercentage.dart'; import '../basic/config/WebToonScrollMode.dart'; import '../basic/config/ReaderZoomScale.dart'; import '../basic/config/ReaderTwoPageDirection.dart'; import '../basic/config/StartupPic.dart'; import '../basic/config/ThreeKeepRight.dart'; import '../basic/config/TimeoutLock.dart'; import '../basic/config/UsingRightClickPop.dart'; import '../basic/config/WebDav.dart'; import '../basic/config/WillPopNotice.dart'; import '../basic/config/i18n.dart'; import 'CleanScreen.dart'; import 'MigrateScreen.dart'; import 'ModifyPasswordScreen.dart'; import 'ThemeScreen.dart'; import 'WebServerScreen.dart'; class SettingsScreen extends StatefulWidget { final bool hiddenAccountInfo; const SettingsScreen({Key? key, this.hiddenAccountInfo = false}) : super(key: key); @override State createState() => _SettingsScreenState(); } class _SettingsScreenState extends State { @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr('settings.settings')), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: [ ExpansionTile( leading: const Icon(Icons.ad_units), title: Text(tr('settings.interface')), children: [ const Divider(), ...themeWidgets(context, setState), appOrientationWidget(), const Divider(), pagerActionSetting(), contentFailedReloadActionSetting(), willPopNoticeSetting(), categoriesColumnCountSetting(), categoriesSortSetting(), const Divider(), setStartupPicTile(context), clearStartupPicTile(context), const Divider(), languageListTile(), timeZoneSetting(), fontSetting(), fullScreenUISetting(), usingRightClickPopSetting(), hiddenFdIconSetting(), hiddenSubIconSetting(), hiddenSearchPersionSetting(), const Divider(), ], ), ExpansionTile( leading: const Icon(Icons.lan), title: Text(tr('settings.network')), children: const [ Divider(), NetworkSetting(), ], ), ExpansionTile( leading: const Icon(Icons.dangerous), title: Text(tr('settings.seal')), children: [ const Divider(), shadowCategoriesModeSetting(), shadowCategoriesSetting(), hiddenWordsSetting(), hiddenViewedSetting(), ], ), ExpansionTile( leading: const Icon(Icons.open_in_browser), title: Text(tr('settings.interaction')), children: [ const Divider(), copySkipConfirmSetting(), copyFullNameSetting(), copyFullNameTemplateSetting(), const Divider(), ], ), ExpansionTile( leading: const Icon(Icons.menu_book_outlined), title: Text(tr('settings.reading')), children: [ const Divider(), qualitySetting(), readerTypeSettings(), readerTwoPageDirectionSetting(), readerDirectionSetting(), readerSliderPositionSetting(), autoFullScreenSetting(), autoFullScreenOnForwardSetting(), fullScreenActionSetting(), webToonScrollModeSetting(), readerScrollByScreenPercentageSetting(), readerZoomMinScaleSetting(), readerZoomMaxScaleSetting(), readerZoomDoubleTapScaleSetting(), dragRegionLockSetting(), gestureSpeedSetting(), const Divider(), volumeControllerSetting(), volumeNextChapterSetting(), keyboardControllerSetting(), const Divider(), noAnimationSetting(), readerBackgroundColorSetting(), const Divider(), threeKeepRightSetting(), ignoreInfoHistorySetting(), ], ), ExpansionTile( leading: const Icon(Icons.download), title: Text(tr('settings.download')), children: [ const Divider(), autoDownloadOnFavoriteSetting(), disableAutoDownloadOnMobileSetting(), autoDeleteDownloadOnUnfavoriteSetting(), const Divider(), ListTile( title: Text(tr('settings.web_server')), subtitle: Text(tr('settings.web_server_subtitle')), onTap: () { Navigator.of(context).push( mixRoute( builder: (BuildContext context) => const WebServerScreen(), ), ); }, ), const Divider(), chooserRootSetting(), downloadThreadCountSetting(), downloadAndExportPathSetting(), showCommentAtDownloadSetting(), exportRenameSetting(), ], ), ExpansionTile( leading: const Icon(Icons.backup), title: Text(tr('settings.sync')), children: [ const Divider(), ...webDavSettings(context), ], ), ExpansionTile( leading: const Icon(Icons.manage_accounts), title: Text(tr('settings.account')), children: [ const Divider(), widget.hiddenAccountInfo ? Container() : ListTile( onTap: () async { Navigator.push( context, mixRoute( builder: (context) => const ModifyPasswordScreen(), ), ); }, title: Text(tr('settings.modify_password')), ), const Divider(), useLocalFavoriteSetting(), const Divider(), hideOnlineFavoriteSetting(), ], ), ExpansionTile( leading: const Icon(Icons.chrome_reader_mode), title: Text(tr('settings.ebook')), children: [ const Divider(), iconLoadingSetting(), eBookScrollingSetting(), eBookScrollingRangeSetting(), eBookScrollingTriggerSetting(), imageFilterSetting(), const Divider(), ], ), ExpansionTile( leading: const Icon(Icons.ad_units), title: Text(tr('settings.system')), children: [ const Divider(), androidDisplayModeSetting(), androidSecureFlagSetting(), authenticationSetting(), lockTimeOutSecSetting(), lockTimeOutSecNotice(), const Divider(), autoCleanSecSetting(), ListTile( onTap: () { Navigator.push( context, mixRoute(builder: (context) => const CleanScreen()), ); }, title: Text(tr('settings.clear_cache')), ), const Divider(), migrate(context), const Divider(), downloadCachePathSetting(), importViewLogFromOff(), const Divider(), ignoreUpgradeConfirmSetting(), ], ), SafeArea( top: false, child: Container(), ), ], ), ), ); } Widget migrate(BuildContext context) { if (Platform.isAndroid) { return ListTile( title: Text( tr('settings.migrate') + (!isPro ? "(${tr('settings.app.pro')})" : ""), style: TextStyle( color: !isPro ? Colors.grey : null, ), ), subtitle: Text(tr('settings.migrate_subtitle')), onTap: () async { if (!isPro) { defaultToast(context, tr('app.pro_required')); return; } var f = await confirmDialog(context, tr('settings.migrate'), tr('settings.migrate_confirm')); if (f) { Navigator.of(context).pushAndRemoveUntil( mixRoute(builder: (BuildContext context) { return const MigrateScreen(); }), (route) => false, ); } }, ); } return Container(); } } class _IconAndWidgets { final IconData icon; final List widgets; _IconAndWidgets(this.icon, this.widgets); } ================================================ FILE: lib/screens/SpaceScreen.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/i18.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/config/HideOnlineFavorite.dart'; import 'package:pikapika/basic/config/HiddenFdIcon.dart'; import 'package:pikapika/basic/config/Version.dart'; import 'package:pikapika/screens/AboutScreen.dart'; import 'package:pikapika/screens/AccountScreen.dart'; import 'package:pikapika/screens/DownloadListScreen.dart'; import 'package:pikapika/screens/FavouritePaperScreen.dart'; import 'package:pikapika/screens/LocalFavoriteScreen.dart'; import 'package:pikapika/screens/ProScreen.dart'; import 'package:pikapika/screens/ViewLogsScreen.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/config/WebDav.dart'; import 'package:pikapika/screens/components/RecommendLinksPanel.dart'; import '../basic/config/IconLoading.dart'; import '../basic/config/IsPro.dart'; import 'SettingsScreen.dart'; import 'components/Badge.dart'; import 'components/UserProfileCard.dart'; // 个人空间页面 class SpaceScreen extends StatefulWidget { const SpaceScreen({Key? key}) : super(key: key); @override State createState() => _SpaceScreenState(); } class _SpaceScreenState extends State { @override void initState() { versionEvent.subscribe(_onEvent); proEvent.subscribe(_onEvent); hiddenFdIconEvent.subscribe(_onEvent); useLocalFavoriteEvent.subscribe(_onEvent); hideOnlineFavoriteEvent.subscribe(_onEvent); super.initState(); } @override void dispose() { versionEvent.unsubscribe(_onEvent); proEvent.unsubscribe(_onEvent); hiddenFdIconEvent.unsubscribe(_onEvent); useLocalFavoriteEvent.unsubscribe(_onEvent); hideOnlineFavoriteEvent.unsubscribe(_onEvent); super.dispose(); } void _onEvent(dynamic a) { setState(() {}); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr('screen.space.title')), actions: [ IconButton( onPressed: () async { bool result = await confirmDialog(context, tr('screen.space.logout'), tr('screen.space.logout_confirm')); if (result) { await method.clearToken(); await method.setPassword(""); Navigator.pushReplacement( context, mixRoute(builder: (context) => const AccountScreen()), ); } }, icon: const Icon(Icons.exit_to_app), ), IconButton( onPressed: () { Navigator.push( context, mixRoute(builder: (context) => const AboutScreen()), ); }, icon: Badged( child: const Icon(Icons.info_outline), badge: latestVersion() == null ? null : "1", ), ), ...hiddenFdIcon ? [] : [ IconButton( onPressed: () { Navigator.of(context) .push(mixRoute(builder: (BuildContext context) { return const ProScreen(); })); }, icon: Icon( isPro ? Icons.offline_bolt : Icons.offline_bolt_outlined, ), ), ], IconButton( onPressed: () { Navigator.push( context, mixRoute(builder: (context) => const SettingsScreen()), ); }, icon: const Icon(Icons.settings), ), ], ), body: ListView( children: [ const Divider(), const UserProfileCard(), const Divider(), if (!hideOnlineFavorite) ...[ ListTile( onTap: () { Navigator.push( context, mixRoute(builder: (context) => const FavouritePaperScreen()), ); }, title: Text(tr('screen.space.my_favourites')), ), const Divider(), ], if (useLocalFavorite) ...[ ListTile( onTap: () { Navigator.push( context, mixRoute(builder: (context) => const LocalFavoriteScreen()), ); }, title: Text(tr('local_favorite.title')), ), const Divider(), ], ListTile( onTap: () { Navigator.push( context, mixRoute(builder: (context) => const ViewLogsScreen()), ); }, title: Text(tr('screen.space.view_history')), ), const Divider(), ListTile( onTap: () { Navigator.push( context, mixRoute(builder: (context) => const DownloadListScreen()), ); }, title: Text(tr('screen.space.my_downloads')), ), const Divider(), const RecommendLinksPanel( padding: EdgeInsets.fromLTRB(16, 0, 16, 16), ), ], ), ); } } ================================================ FILE: lib/screens/ThemeScreen.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/basic/config/Platform.dart'; import 'package:pikapika/i18.dart'; import '../basic/config/Themes.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; class ThemeScreen extends StatefulWidget { const ThemeScreen({Key? key}) : super(key: key); @override State createState() => _ThemeScreenState(); } class _ThemeScreenState extends State { @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(tr('screen.theme.title'))), body: PikaListView( children: themeWidgets(context, setState), ), ); } } List themeWidgets( BuildContext context, void Function(VoidCallback fn) setState) { return [ ListTile( onTap: () async { await chooseLightTheme(context); setState(() {}); }, title: Text(tr('screen.theme.theme')), subtitle: Text(currentLightThemeName()), ), ...androidNightModeDisplay ? [ SwitchListTile( title: Text(tr('screen.theme.dark_mode_different_theme')), value: androidNightMode, onChanged: (value) async { await setAndroidNightMode(value); setState(() {}); }), ] : [], ...androidNightModeDisplay && androidNightMode ? [ ListTile( onTap: () async { await chooseDarkTheme(context); setState(() {}); }, title: Text(tr('screen.theme.dark_mode_theme')), subtitle: Text(currentDarkThemeName()), ), ] : [], // ...androidVersion > 0 ? [enableStatusBarColorSetting()] : [], ]; } ================================================ FILE: lib/screens/ViewLogsScreen.dart ================================================ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:pikapika/i18.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/ComicInfoCard.dart'; import 'package:pikapika/screens/components/RightClickPop.dart'; import '../basic/Entities.dart'; import '../basic/config/IconLoading.dart'; import 'ComicInfoScreen.dart'; import 'components/Images.dart'; import 'components/ListView.dart'; // 浏览记录 class ViewLogsScreen extends StatefulWidget { const ViewLogsScreen({Key? key}) : super(key: key); @override State createState() => _ViewLogsScreenState(); } class _ViewLogsScreenState extends State { static const _pageSize = 24; static const _scrollPhysics = AlwaysScrollableScrollPhysics(); // 即使不足一页仍可滚动 final _scrollController = ScrollController(); final _comicList = []; var _isLoading = false; // 是否加载中 var _scrollOvered = false; // 滚动到最后 var _offset = 0; var _inSelection = false; // 是否进入选择模式 var _selectedList = []; // 选择列表 Future _clearAll() async { if (await confirmDialog( context, tr('screen.view_logs.clear_all'), tr('screen.view_logs.clear_all_desc'), )) { await method.clearAllViewLog(); setState(() { _comicList.clear(); _isLoading = false; _scrollOvered = true; _offset = 0; }); } } Future _deleteSelected() async { if (_selectedList.isNotEmpty) { var confirm = await confirmDialog( context, tr('screen.view_logs.clear_selected'), tr('screen.view_logs.clear_selected_desc'), ); if (!confirm) { return; } } if (_selectedList.isNotEmpty) { await method.deleteViewLog(_selectedList.join(',')); } setState(() { _inSelection = false; _selectedList.clear(); _comicList.clear(); _isLoading = false; _scrollOvered = true; _offset = 0; }); _loadPage(); } Future _viewSelected() async { if (_selectedList.isNotEmpty) { await method.viewComic(_selectedList.join(',')); } setState(() { _inSelection = false; _selectedList.clear(); _comicList.clear(); _isLoading = false; _scrollOvered = true; _offset = 0; }); _loadPage(); } Future _clearOnce(String id) async { if (await confirmDialog( context, tr('screen.view_logs.clear_one'), tr('screen.view_logs.clear_one_desc'), )) { await method.deleteViewLog(id); setState(() { for (var i = 0; i < _comicList.length; i++) { if (_comicList[i].id == id) { _comicList.removeAt(i); _offset--; break; } } }); } } // 加载一页 Future _loadPage() async { setState(() { _isLoading = true; }); try { var page = await method.viewLogPage(_offset, _pageSize); if (page.isEmpty) { _scrollOvered = true; } else { _comicList.addAll(page); } _offset += _pageSize; } finally { setState(() { _isLoading = false; }); } } // 滚动事件 void _handScroll() { if (_scrollController.position.pixels + MediaQuery.of(context).size.height / 2 < _scrollController.position.maxScrollExtent) { return; } if (_isLoading || _scrollOvered || _inSelection) return; _loadPage(); } @override void initState() { _loadPage(); super.initState(); } @override void dispose() { _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { var entries = _comicList.map((e) { Widget card = InkWell( onTap: () { if (_inSelection) { if (_selectedList.contains(e.id)) { _selectedList.remove(e.id); } else { _selectedList.add(e.id); } } else { _chooseComic(e.id); } setState(() {}); }, onLongPress: () { if (_inSelection) { if (_selectedList.contains(e.id)) { _selectedList.remove(e.id); } else { _selectedList.add(e.id); } } else { _clearOnce(e.id); } setState(() {}); }, child: ViewInfoCard( fileServer: e.thumbFileServer, author: e.author, categories: _decodeCate(e.categories), path: e.thumbPath, title: e.title, ), ); if (_inSelection) { card = Stack( children: [ card, Positioned( top: 10, right: 10, child: Icon( _selectedList.contains(e.id) ? Icons.check_box : Icons.check_box_outline_blank, color: _selectedList.contains(e.id) ? Theme.of(context).colorScheme.primary : null, ), ), ], ); } return card; }); final screen = NotificationListener( child: Scaffold( appBar: AppBar( title: Text(tr('screen.view_logs.title')), actions: [ ..._inSelection ? [ IconButton( onPressed: () { setState(() { _inSelection = false; _selectedList.clear(); }); }, icon: Icon(Icons.cancel), ), IconButton( onPressed: _viewSelected, icon: const Icon(Icons.move_up), ), IconButton( onPressed: _deleteSelected, icon: const Icon(Icons.delete), ) ] : [ IconButton( icon: const Icon(Icons.rule), onPressed: () { setState(() { _inSelection = !_inSelection; _selectedList.clear(); }); }, ), IconButton( onPressed: _clearAll, icon: const Icon(Icons.auto_delete)), ], ], ), body: PikaListView( physics: _scrollPhysics, controller: _scrollController, children: entries.toList(), ), ), onNotification: (scrollNotification) { if (scrollNotification is ScrollStartNotification) { _handScroll(); } return true; }, ); return rightClickPop( child: WillPopScope( onWillPop: () async { if (_inSelection) { setState(() { _inSelection = false; _selectedList.clear(); }); return false; } return true; }, child: screen, ), context: context, canPop: true, ); } void _chooseComic(String comicId) { Navigator.push( context, mixRoute( builder: (context) => ComicInfoScreen( comicId: comicId, ), ), ); } List _decodeCate(String categories) { try { var decode = jsonDecode(categories); if (decode is List) { return List.of(decode).cast(); } return [decode]; } catch (e) { return [categories]; } } } class ViewInfoCard extends StatelessWidget { final String fileServer; final String path; final String title; final String author; final List categories; const ViewInfoCard({ Key? key, required this.fileServer, required this.path, required this.title, required this.author, required this.categories, }) : super(key: key); @override Widget build(BuildContext context) { var theme = Theme.of(context); return Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: theme.dividerColor, ), ), ), child: Row( children: [ Container( padding: const EdgeInsets.only(right: 10), child: RemoteImage( key: Key("$fileServer:$path"), fileServer: fileServer, path: path, width: imageWidth, height: imageHeight, ), ), Expanded( child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: titleStyle), Container(height: 5), Text(author, style: authorStyle), Container(height: 5), Text.rich( TextSpan( text: "${tr('screen.view_logs.categories')} : ${categories.join(' ')}"), style: TextStyle( fontSize: 13, color: Theme.of(context) .textTheme .bodyText1! .color! .withAlpha(0xCC), ), ), Container(height: 5), ], ), ), ], ), ), ], ), ); } } ================================================ FILE: lib/screens/WebServerScreen.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/i18.dart'; import '../basic/Method.dart'; import 'components/ContentError.dart'; import 'components/ContentLoading.dart'; import 'components/ListView.dart'; import 'components/RightClickPop.dart'; class WebServerScreen extends StatefulWidget { const WebServerScreen({Key? key}) : super(key: key); @override State createState() => _WebServerScreenState(); } class _WebServerScreenState extends State { late final Future _ipFuture = method.clientIpSet(); late Future _future = method.startWebServer(); @override void dispose() { method.stopWebServer(); super.dispose(); } @override Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, canPop: true, ); } Widget buildScreen(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr('screen.web_server.title')), ), body: FutureBuilder( future: _future, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return ContentError( error: snapshot.error, stackTrace: snapshot.stackTrace, onRefresh: () async { setState(() { _future = method.startWebServer(); }); }); } if (snapshot.connectionState != ConnectionState.done) { return ContentLoading(label: tr('app.loading')); } return PikaListView( children: [ Container( padding: const EdgeInsets.all(8), child: Column( children: [ FutureBuilder( future: _ipFuture, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return Text(tr('screen.web_server.get_ip_failed')); } if (snapshot.connectionState != ConnectionState.done) { return Text(tr('screen.web_server.getting_ip')); } return Text('${snapshot.data}'); }, ), Text(tr('screen.web_server.port')), const Text(''), Text(tr('screen.web_server.usage_instruction')), const Text(''), Text(tr('screen.web_server.leave_notice')), ], ), ), ], ); }, ), ); } } ================================================ FILE: lib/screens/calculator_screen.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import '../basic/config/passed.dart'; import 'CloseAppScreen.dart'; class CalculatorScreen extends StatelessWidget { const CalculatorScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) => Scaffold( body: Container( padding: const EdgeInsets.only(top: 10.0, left: 10.0), color: Colors.black, child: const ContentBody(), ), ); } class ContentBody extends StatefulWidget { const ContentBody({Key? key}) : super(key: key); @override State createState() => ContentBodyState(); } class ContentBodyState extends State { String sums = '0'; String total = '0'; String flag = ''; bool isDouble = false; int tag = 0; List list = [ {'bgc': '0xFFFF9800', 'color': '0xFFFFFFFFF'}, {'bgc': '0xFFFF9800', 'color': '0xFFFFFFFFF'}, {'bgc': '0xFFFF9800', 'color': '0xFFFFFFFFF'}, {'bgc': '0xFFFF9800', 'color': '0xFFFFFFFFF'}, ]; @override Widget build(BuildContext context) { return Column( children: [ Expanded(child: Container()), Container( padding: const EdgeInsets.only( top: 10.0, left: 10.0, right: 20.0, bottom: 10.0), child: Container( width: 750, alignment: Alignment.bottomRight, child: Text( sums, maxLines: 8, style: const TextStyle(fontSize: 33, color: Colors.white), ), ), ), Column( children: [ Center( child: Row( children: [ Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(45), color: Colors.grey, splashColor: Colors.white, onPressed: () { btnclick('重置'); }, child: const Text('AC', style: TextStyle( color: Colors.black, fontSize: 20)), shape: const CircleBorder( side: BorderSide(color: Colors.grey), ), ), alignment: Alignment.center, ), flex: 1, ), Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(45), color: Colors.grey, splashColor: Colors.white, onPressed: () { btnclick('加/减'); }, child: const Text('+/-', style: TextStyle( color: Colors.black, fontSize: 20)), shape: const CircleBorder( side: BorderSide(color: Colors.grey)), ), alignment: Alignment.center, ), flex: 1, ), Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(40), color: Colors.grey, splashColor: Colors.white, onPressed: () { btnclick('百分号'); }, child: const Text('%', style: TextStyle( color: Colors.black, fontSize: 25)), shape: const CircleBorder( side: BorderSide(color: Colors.grey)), ), alignment: Alignment.center, ), flex: 1, ), Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(34), color: Color(int.parse(list[0]['bgc'])), splashColor: Color(int.parse(list[0]['bgc'])), onPressed: () { btnclick('除'); }, child: Text('÷', style: TextStyle( color: Color(int.parse(list[0]['color'])), fontSize: 30)), shape: CircleBorder( side: BorderSide( color: Color(int.parse(list[0]['bgc'])))), ), alignment: Alignment.center, ), flex: 1, ), ], ), ), Center( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(36), color: const Color(0xFF3B3B3B), splashColor: Colors.grey, onPressed: () { numClick('7'); }, child: const Text('7', style: TextStyle( color: Colors.white, fontSize: 30)), shape: const CircleBorder( side: BorderSide(color: Color(0xFF3B3B3B))), ), alignment: Alignment.center, ), flex: 1, ), Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(36), color: const Color(0xFF3B3B3B), splashColor: Colors.grey, onPressed: () { numClick('8'); }, child: const Text('8', style: TextStyle( color: Colors.white, fontSize: 30)), shape: const CircleBorder( side: BorderSide(color: Color(0xFF3B3B3B))), ), alignment: Alignment.center, ), flex: 1, ), Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(36), color: const Color(0xFF3B3B3B), splashColor: Colors.grey, onPressed: () { numClick('9'); }, child: const Text('9', style: TextStyle( color: Colors.white, fontSize: 30)), shape: const CircleBorder( side: BorderSide(color: Color(0xFF3B3B3B))), ), alignment: Alignment.center, ), flex: 1, ), Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(34), color: Color(int.parse(list[1]['bgc'])), splashColor: Color(int.parse(list[1]['bgc'])), onPressed: () { btnclick('乘'); }, child: Text('×', style: TextStyle( color: Color(int.parse(list[1]['color'])), fontSize: 30)), shape: CircleBorder( side: BorderSide( color: Color(int.parse(list[1]['bgc'])))), ), alignment: Alignment.center, ), flex: 1, ), ], ), ), Center( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(36), color: const Color(0xFF3B3B3B), splashColor: Colors.grey, onPressed: () { numClick('4'); }, child: const Text('4', style: TextStyle( color: Colors.white, fontSize: 30)), shape: const CircleBorder( side: BorderSide(color: Color(0xFF3B3B3B))), ), alignment: Alignment.center, ), flex: 1, ), Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(36), color: const Color(0xFF3B3B3B), splashColor: Colors.grey, onPressed: () { numClick('5'); }, child: const Text('5', style: TextStyle( color: Colors.white, fontSize: 30)), shape: const CircleBorder( side: BorderSide(color: Color(0xFF3B3B3B))), ), alignment: Alignment.center, ), flex: 1, ), Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(36), color: const Color(0xFF3B3B3B), splashColor: Colors.grey, onPressed: () { numClick('6'); }, child: const Text('6', style: TextStyle( color: Colors.white, fontSize: 30)), shape: const CircleBorder( side: BorderSide(color: Color(0xFF3B3B3B))), ), alignment: Alignment.center, ), flex: 1, ), Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(34), color: Color(int.parse(list[2]['bgc'])), splashColor: Color(int.parse(list[2]['bgc'])), onPressed: () { btnclick('减'); }, child: Text('—', style: TextStyle( color: Color(int.parse(list[2]['color'])), fontSize: 30)), shape: CircleBorder( side: BorderSide( color: Color(int.parse(list[2]['bgc'])))), ), alignment: Alignment.center, ), flex: 1, ), ], ), ), Center( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(36), color: const Color(0xFF3B3B3B), splashColor: Colors.grey, onPressed: () { numClick('1'); }, child: const Text('1', style: TextStyle( color: Colors.white, fontSize: 30)), shape: const CircleBorder( side: BorderSide(color: Color(0xFF3B3B3B))), ), alignment: Alignment.center, ), flex: 1, ), Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(36), color: const Color(0xFF3B3B3B), splashColor: Colors.grey, onPressed: () { numClick('2'); }, child: const Text('2', style: TextStyle( color: Colors.white, fontSize: 30)), shape: const CircleBorder( side: BorderSide(color: Color(0xFF3B3B3B))), ), alignment: Alignment.center, ), flex: 1, ), Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(36), color: const Color(0xFF3B3B3B), splashColor: Colors.grey, onPressed: () { numClick('3'); }, child: const Text('3', style: TextStyle( color: Colors.white, fontSize: 30)), shape: const CircleBorder( side: BorderSide(color: Color(0xFF3B3B3B))), ), alignment: Alignment.center, ), flex: 1, ), Expanded( child: Container( child: MaterialButton( padding: EdgeInsets.all(34), color: Color(int.parse(list[3]['bgc'])), splashColor: Color(int.parse(list[3]['bgc'])), onPressed: () { btnclick('加'); }, child: Text('+', style: TextStyle( color: Color(int.parse(list[3]['color'])), fontSize: 30)), shape: CircleBorder( side: BorderSide( color: Color(int.parse(list[3]['bgc'])))), ), alignment: Alignment.center, ), flex: 1, ), ], ), ), Center( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Container( child: MaterialButton( padding: const EdgeInsets.only( left: 70.0, top: 20.0, bottom: 20.0, right: 76.0), color: const Color(0xFF3B3B3B), splashColor: Colors.grey, onPressed: () { numClick('0'); }, child: const Text('0', style: TextStyle( color: Colors.white, fontSize: 30)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40)), ), margin: const EdgeInsets.only(left: 10.0), alignment: Alignment.center, ), Container( child: MaterialButton( padding: EdgeInsets.all(36), color: const Color(0xFF3B3B3B), splashColor: Colors.grey, onPressed: () { numClick('.'); }, child: const Text('.', style: TextStyle( color: Colors.white, fontSize: 30)), shape: const CircleBorder( side: BorderSide(color: Color(0xFF3B3B3B))), ), alignment: Alignment.center, ), Container( child: MaterialButton( padding: EdgeInsets.all(34), color: Colors.orange, splashColor: Colors.orange, onPressed: () { btnclick('等于'); }, child: const Text('=', style: TextStyle( color: Colors.white, fontSize: 30)), shape: const CircleBorder( side: BorderSide(color: Colors.orange)), ), alignment: Alignment.center, ), ], ), ), ], ), Expanded(child: Container()), ], ); } numClick(e) { if (sums == '0') { if (e == '.') { setState(() { isDouble = true; sums += e; }); } else { setState(() { sums = e; }); } } else { if (flag != '') { if (tag == 0) { if (sums.length < 20) { setState(() { sums += e; }); } } else { setState(() { sums = e; tag = 0; }); } } else { if (sums.length < 20) { setState(() { sums += e; }); } } } } // 计算点击 btnclick(e) { if (sums == "77554422") { firstPassed().then((value) { Navigator.pushReplacement(context, MaterialPageRoute( builder: (BuildContext context) { return const CloseAppScreen(); }, )); }); } for (var element in list) { element['color'] = '0xFFFFFFFFF'; element['bgc'] = '0xFFFF9800'; } switch (e) { case '重置': setState(() { sums = '0'; tag = 0; flag = ''; }); break; case '加': setState(() { total = sums; tag = 1; flag = '加'; list[3]['bgc'] = '0xFFFFFFFFF'; list[3]['color'] = '0xFFFF9800'; }); break; case '减': setState(() { total = sums; tag = 1; flag = '减'; list[2]['bgc'] = '0xFFFFFFFFF'; list[2]['color'] = '0xFFFF9800'; }); break; case '乘': setState(() { total = sums; tag = 1; flag = '乘'; list[1]['bgc'] = '0xFFFFFFFFF'; list[1]['color'] = '0xFFFF9800'; }); break; case '除': setState(() { total = sums; tag = 1; flag = '除'; list[0]['bgc'] = '0xFFFFFFFFF'; list[0]['color'] = '0xFFFF9800'; }); break; case '百分号': setState(() { total = sums; tag = 1; flag = '百分号'; sums = (int.parse(sums) / 100).toString(); isDouble = true; }); break; case '等于': sumClac(); setState(() { tag = 1; flag = 'true'; }); clacVlaue(); } } // 计算函数 sumClac() { if (flag == '加') { if (isDouble) { double c = double.parse(total) + double.parse(sums); setState(() { sums = c.toString(); }); } else { int c = int.parse(total) + int.parse(sums); setState(() { sums = c.toString(); }); } setState(() { total = ''; isDouble = false; flag = ''; }); } else if (flag == '减') { if (isDouble) { double c = double.parse(total) - double.parse(sums); setState(() { sums = c.toString(); }); } else { int c = int.parse(total) - int.parse(sums); setState(() { sums = c.toString(); }); } setState(() { total = ''; flag = ''; isDouble = false; }); } else if (flag == '乘') { if (isDouble) { double c = double.parse(total) * double.parse(sums); setState(() { sums = c.toString(); }); } else { int c = int.parse(total) * int.parse(sums); setState(() { sums = c.toString(); }); } setState(() { total = ''; flag = ''; isDouble = false; }); } else if (flag == '除') { if (isDouble) { double c = double.parse(total) * double.parse(sums); setState(() { sums = c.toString(); }); } else { double c = int.parse(total) / int.parse(sums); if (int.parse(total) % int.parse(sums) == 0) { setState(() { sums = c.toInt().toString(); }); } else { setState(() { sums = c.toString(); }); } } setState(() { total = ''; flag = ''; isDouble = false; }); } } // 判断计算值 clacVlaue() { // if(sums.length >10){ // setState(() { // sums = sums.substring(0, 10); // }); // } } } ================================================ FILE: lib/screens/components/Avatar.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/basic/Entities.dart'; import 'Images.dart'; const double _avatarMargin = 5; const double _avatarBorderSize = 1.5; // 头像 class Avatar extends StatelessWidget { final RemoteImageInfo avatarImage; final double size; const Avatar(this.avatarImage, {this.size = 50, Key? key}) : super(key: key); @override Widget build(BuildContext context) { var theme = Theme.of(context); return Container( margin: const EdgeInsets.all(_avatarMargin), decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: theme.colorScheme.secondary, style: BorderStyle.solid, width: _avatarBorderSize, )), child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(this.size)), child: RemoteImage( fileServer: this.avatarImage.fileServer, path: this.avatarImage.path, width: this.size, height: this.size, ), ), ); } } ================================================ FILE: lib/screens/components/Badge.dart ================================================ import 'package:flutter/material.dart'; // 提示信息, 组件右上角的小红点 class Badged extends StatelessWidget { final String? badge; final Widget child; const Badged({Key? key, required this.child, this.badge}) : super(key: key); @override Widget build(BuildContext context) { if (badge == null) { return child; } return Stack( children: [ child, Positioned( right: 0, child: Container( padding: const EdgeInsets.all(1), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(6), ), constraints: const BoxConstraints( minWidth: 12, minHeight: 12, ), child: Text( badge!, style: const TextStyle( color: Colors.white, fontSize: 8, ), textAlign: TextAlign.center, ), ), ), ], ); } } ================================================ FILE: lib/screens/components/BottomSheetInput.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; Future showInputModalBottomSheet({ required BuildContext context, required FutureOr Function(String) onSubmitted, required String? hintText, String? initialValue, }) async { Navigator.of(context).push( PageRouteBuilder( opaque: false, pageBuilder: (context, _, __) { return BottomSheetInput( onSubmitted: onSubmitted, hintText: hintText, initialValue: initialValue, ); }, ), ); } class BottomSheetInput extends StatefulWidget { final FutureOr Function(String) onSubmitted; final String? hintText; final String? initialValue; const BottomSheetInput({ Key? key, required this.onSubmitted, required this.hintText, this.initialValue, }) : super(key: key); @override State createState() => _BottomSheetInputState(); } class _BottomSheetInputState extends State { late TextEditingController _controller; bool submitting = false; @override void initState() { _controller = TextEditingController(text: widget.initialValue); super.initState(); } @override dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { var mq = MediaQuery.of(context); var size = mq.size; return SizedBox( width: size.width, height: size.height, child: Material( color: Colors.black.withAlpha(50), child: Column( children: [ SafeArea( bottom: false, child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { if (!submitting) Navigator.of(context).pop(); }, child: Container(), ), ), Material( child: Padding( padding: const EdgeInsets.only( left: 20, right: 20, top: 30, bottom: 30, ), child: Column( children: [ if (submitting) const Center( child: CircularProgressIndicator(), ), if (!submitting) _buildTextField(), ], ), ), ), Expanded( child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { if (!submitting) Navigator.of(context).pop(); }, child: Container(), ), ), ], ), ), ); } /* Column( children: [ SafeArea( child: Container(), bottom: false, ), Material( child: Column( children: [ Container( padding: const EdgeInsets.only( left: 20, right: 20, top: 30, bottom: 30, ), child: Column( children: [ if (submitting) const Center( child: CircularProgressIndicator(), ), if (!submitting) _buildTextField(), ], ), ), Expanded(child: Container()), ], ), ), Expanded( child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { if (!submitting) Navigator.of(context).pop(); }, child: Container(), ), ), ], ), */ Widget _buildTextField() { return TextField( controller: _controller, autofocus: true, decoration: InputDecoration( hintText: widget.hintText, border: InputBorder.none, contentPadding: const EdgeInsets.all(16), ), onSubmitted: (text) { _onSubmitted(text); }, ); } _onSubmitted(String text) async { setState(() { submitting = true; }); try { await widget.onSubmitted(text); Navigator.of(context).pop(); } catch (e, s) { defaultToast(context, e.toString()); } finally { setState(() { submitting = false; }); } } } ================================================ FILE: lib/screens/components/ComicDescriptionCard.dart ================================================ import 'package:flutter/material.dart'; // 漫画的说明 class ComicDescriptionCard extends StatelessWidget { final String description; const ComicDescriptionCard({Key? key, required this.description}) : super(key: key); @override Widget build(BuildContext context) { var theme = Theme.of(context); return Container( padding: const EdgeInsets.only( top: 5, bottom: 5, left: 10, right: 10, ), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: theme.dividerColor, ), ), ), child: SelectableText(description, style: _categoriesStyle), ); } } const _categoriesStyle = TextStyle( fontSize: 13, color: Colors.grey, ); ================================================ FILE: lib/screens/components/ComicInfoCard.dart ================================================ import 'dart:convert'; import 'package:pikapika/i18.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Cross.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/config/AutoDeleteDownloadOnUnfavorite.dart'; import 'package:pikapika/basic/config/AutoDownloadOnFavorite.dart'; import 'package:pikapika/basic/config/CopyFullName.dart'; import 'package:pikapika/basic/config/CopyFullNameTemplate.dart'; import 'package:pikapika/basic/config/DisableAutoDownloadOnMobile.dart'; import 'package:pikapika/basic/config/HideOnlineFavorite.dart'; import 'package:pikapika/basic/config/IsPro.dart'; import 'package:pikapika/basic/config/WebDav.dart'; import 'package:pikapika/screens/SearchAuthorScreen.dart'; import 'package:pikapika/basic/Navigator.dart'; import '../ComicsScreen.dart'; import 'Images.dart'; import 'package:pikapika/basic/connect.dart'; // 漫画卡片 class ComicInfoCard extends StatefulWidget { final bool linkItem; final ComicSimple info; final bool viewed; const ComicInfoCard( this.info, { Key? key, this.linkItem = false, this.viewed = false, }) : super(key: key); @override State createState() => _ComicInfoCard(); } class _ComicInfoCard extends State { bool _favouriteLoading = false; bool _likeLoading = false; bool _localFavoriteLoading = false; LocalFavoriteComic? _localFavoriteComic; @override void initState() { super.initState(); if (useLocalFavorite) { _loadLocalFavoriteStatus(); } } Future _loadLocalFavoriteStatus() async { try { _localFavoriteComic = await method.getLocalFavoriteComic(widget.info.id); if (mounted) { setState(() {}); } } catch (e) { print("Load local favorite status error: $e"); } } String _encodeComicSimpleInfo(ComicSimple info) { return jsonEncode({ "_id": info.id, "title": info.title, "author": info.author, "pagesCount": info.pagesCount, "epsCount": info.epsCount, "finished": info.finished, "categories": info.categories, "likesCount": info.likesCount, "thumb": { "originalName": info.thumb.originalName, "fileServer": info.thumb.fileServer, "path": info.thumb.path, }, }); } @override Widget build(BuildContext context) { var info = widget.info; var theme = Theme.of(context); var view = info is ComicInfo ? info.viewsCount : 0; bool? like = info is ComicInfo ? info.isLiked : null; bool? favourite = (hideOnlineFavorite || info is! ComicInfo) ? null : info.isFavourite; return Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: theme.dividerColor, ), ), ), child: Row( children: [ Container( padding: const EdgeInsets.only(right: 10), child: RemoteImage( fileServer: info.thumb.fileServer, path: info.thumb.path, width: imageWidth, height: imageHeight, ), ), Expanded( child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ widget.linkItem ? GestureDetector( onLongPress: () { if (copyFullName()) { var fin = copyFullNameTemplate() .replaceAll("{title}", info.title) .replaceAll("{author}", info.author); if (fin.isEmpty) { fin = info.title; } confirmCopy( context, fin, ); } else { confirmCopy(context, info.title); } }, child: Text(info.title, style: titleStyle), ) : Text(info.title, style: titleStyle), Container(height: 5), widget.linkItem ? InkWell( onTap: () { navPushOrReplace( context, (context) => SearchAuthorScreen( author: info.author)); }, onLongPress: () { confirmCopy(context, info.author); }, child: Text(info.author, style: authorStyle), ) : Text(info.author, style: authorStyle), Container(height: 5), Text.rich( widget.linkItem ? TextSpan( children: [ TextSpan( text: '${tr('components.comic_info_card.categories')} :'), ...info.categories.map( (e) => TextSpan( children: [ const TextSpan(text: ' '), TextSpan( text: e, recognizer: TapGestureRecognizer() ..onTap = () => navPushOrReplace( context, (context) => ComicsScreen( category: e, ), ), ), ], ), ), ], ) : TextSpan( text: "${tr('components.comic_info_card.categories')} : ${info.categories.join(' ')}"), style: TextStyle( fontSize: 13, color: Theme.of(context) .textTheme .bodyText1! .color! .withAlpha(0xCC), ), ), Container(height: 5), Wrap( crossAxisAlignment: WrapCrossAlignment.center, runSpacing: 5, children: [ ...info.likesCount > 0 ? [ iconFavorite, iconSpacing, Text( '${info.likesCount}', style: iconLabelStyle, strutStyle: iconLabelStrutStyle, ), iconMargin, ] : [], ...(view > 0 ? [ iconVisibility, iconSpacing, Text( '$view', style: iconLabelStyle, strutStyle: iconLabelStrutStyle, ), iconMargin, ] : []), ...(info.epsCount > 0 ? [ Text.rich(TextSpan(children: [ const WidgetSpan(child: iconPage), WidgetSpan(child: iconSpacing), WidgetSpan( child: Text( "${info.epsCount}E / ${info.pagesCount}P", style: countLabelStyle, strutStyle: iconLabelStrutStyle, softWrap: false, )), WidgetSpan(child: iconMargin), ])), ] : []), iconMargin, ], ), ], ), ), SizedBox( height: imageHeight, child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ buildFinished(info.finished), ...buildViewed(widget.viewed), Expanded(child: Container()), ...(like == null ? [] : [ Container(height: 10), SizedBox( height: 26, child: _likeLoading ? IconButton( color: Colors.pink[400], onPressed: () {}, icon: const Icon( Icons.sync, ), ) : IconButton( color: Colors.pink[400], onPressed: _changeLike, icon: Icon( like ? Icons.favorite : Icons.favorite_border, ), ), ), ]), ...((!useLocalFavorite && favourite == null) ? [] : [ Container(height: 10), SizedBox( height: 26, child: Row( mainAxisSize: MainAxisSize.min, children: [ if (useLocalFavorite) ...[ _localFavoriteLoading ? IconButton( color: Colors.pink[400], padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () {}, icon: const Icon(Icons.sync), ) : IconButton( color: Colors.pink[400], padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: _changeLocalFavorite, icon: Icon( _localFavoriteComic != null ? Icons.folder_special : Icons.folder_open, ), ), ], if (useLocalFavorite && favourite != null) const SizedBox(width: 8), if (favourite != null) _favouriteLoading ? IconButton( color: Colors.pink[400], padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () {}, icon: const Icon(Icons.sync), ) : IconButton( color: Colors.pink[400], padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: _changeFavourite, icon: Icon( favourite ? Icons.bookmark : Icons.bookmark_border, ), ), ], ), ), ]), Container(height: 10), ], ), ), ], ), ), ], ), ); } Future _changeFavourite() async { setState(() { _favouriteLoading = true; }); try { var rst = await method.switchFavourite(widget.info.id); final isNowFavourite = !rst.startsWith("un"); setState(() { (widget.info as ComicInfo).isFavourite = isNowFavourite; }); if (isNowFavourite) { await _maybeAutoDownloadOnFavorite(); } else { await _maybeAutoDeleteOnUnfavorite(); } } finally { setState(() { _favouriteLoading = false; }); } } Future _maybeAutoDownloadOnFavorite() async { if (!autoDownloadOnFavorite()) { return; } if (!isPro) { defaultToast(context, tr('app.pro_required')); return; } try { if (disableAutoDownloadOnMobile() && await isMobileNetwork()) { return; } await method.downloadAll([widget.info.id]); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.download_failed'))), ); } } } Future _maybeAutoDeleteOnUnfavorite() async { if (!autoDeleteDownloadOnUnfavorite()) { return; } try { await method.deleteDownloadComic(widget.info.id); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.remove_failed'))), ); } } } Future _changeLike() async { setState(() { _likeLoading = true; }); try { var rst = await method.switchLike(widget.info.id); setState(() { (widget.info as ComicInfo).isLiked = !rst.startsWith("un"); }); } finally { setState(() { _likeLoading = false; }); } } Future _changeLocalFavorite() async { if (_localFavoriteComic != null) { // 已收藏,显示确认删除对话框 bool? confirm = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(tr('local_favorite.remove_confirm_title')), content: Text(tr('local_favorite.remove_confirm_content')), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(false); }, child: Text(tr('app.cancel')), ), TextButton( onPressed: () { Navigator.of(context).pop(true); }, child: Text(tr('app.confirm')), ), ], ); }, ); if (confirm == true) { setState(() { _localFavoriteLoading = true; }); try { await method.removeLocalFavoriteComic(widget.info.id); setState(() { _localFavoriteComic = null; }); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.remove_failed'))), ); } } finally { setState(() { _localFavoriteLoading = false; }); } } } else { // 未收藏,显示文件夹选择对话框 await _showFolderSelector(); } } Future _showFolderSelector() async { setState(() { _localFavoriteLoading = true; }); try { List folders = await method.listLocalFavoriteFolders(); int folderCount = await method.countLocalFavoriteFolders(); if (!mounted) return; setState(() { _localFavoriteLoading = false; }); // 显示文件夹选择对话框 String? selectedFolderId = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(tr('local_favorite.select_folder')), content: SizedBox( width: double.maxFinite, child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.folder_special), title: Text(tr('local_favorite.all_folders')), onTap: () { Navigator.of(context).pop('__ALL__'); }, ), const Divider(), if (folders.isEmpty) Padding( padding: const EdgeInsets.all(16.0), child: Text(tr('local_favorite.no_folders')), ) else ListView.builder( shrinkWrap: true, itemCount: folders.length, itemBuilder: (context, index) { final folder = folders[index]; return ListTile( leading: const Icon(Icons.folder), title: Text(folder.name), onTap: () { Navigator.of(context).pop(folder.id); }, ); }, ), const Divider(), ListTile( leading: Icon( Icons.create_new_folder, color: (isPro || folderCount < 3) ? null : Colors.grey, ), title: Text( tr('local_favorite.new_folder') + (isPro || folderCount < 3 ? "" : " (${tr('app.pro')})"), style: TextStyle( color: (isPro || folderCount < 3) ? null : Colors.grey, ), ), onTap: () async { if (!isPro && folderCount >= 3) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(tr('local_favorite.folder_limit_reached')), ), ); return; } Navigator.of(context).pop('__CREATE_NEW__'); }, ), ], ), ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text(tr('app.cancel')), ), ], ); }, ); if (selectedFolderId == '__ALL__') { await _addToFolder(""); } else if (selectedFolderId == '__CREATE_NEW__') { // 创建新文件夹 await _createNewFolder(); } else if (selectedFolderId != null) { // 添加到选中的文件夹 await _addToFolder(selectedFolderId); } } catch (e) { if (mounted) { setState(() { _localFavoriteLoading = false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.load_failed'))), ); } } } Future _createNewFolder() async { String? folderName = await showDialog( context: context, builder: (BuildContext context) { final controller = TextEditingController(); return AlertDialog( title: Text(tr('local_favorite.new_folder')), content: TextField( controller: controller, decoration: InputDecoration( hintText: tr('local_favorite.folder_name'), ), autofocus: true, ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text(tr('app.cancel')), ), TextButton( onPressed: () { Navigator.of(context).pop(controller.text.trim()); }, child: Text(tr('app.confirm')), ), ], ); }, ); if (folderName != null && folderName.isNotEmpty) { setState(() { _localFavoriteLoading = true; }); try { LocalFavoriteFolder folder = await method.createLocalFavoriteFolder(folderName); await _addToFolder(folder.id); } catch (e) { if (mounted) { setState(() { _localFavoriteLoading = false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.create_folder_failed'))), ); } } } } Future _addToFolder(String folderId) async { setState(() { _localFavoriteLoading = true; }); try { await method.addLocalFavoriteComic( widget.info.id, folderId, info: _encodeComicSimpleInfo(widget.info), ); await _loadLocalFavoriteStatus(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.add_success'))), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('local_favorite.add_failed'))), ); } } finally { if (mounted) { setState(() { _localFavoriteLoading = false; }); } } } } double imageWidth = 210 / 3.15; double imageHeight = 315 / 3.15; Widget buildFinished(bool comicFinished) { if (comicFinished) { return Container( padding: const EdgeInsets.only(left: 8, right: 8), decoration: BoxDecoration( color: Colors.orange.shade800, borderRadius: BorderRadius.circular(30), ), child: Text( tr('components.comic_info_card.finished'), style: const TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white, height: 1.2, ), strutStyle: const StrutStyle( height: 1.2, ), ), ); } return Container(); } List buildViewed(viewed) { if (!viewed) { return []; } return [ Container(height: 5), Container( padding: const EdgeInsets.only(left: 8, right: 8), decoration: BoxDecoration( color: Colors.yellow.shade800, borderRadius: BorderRadius.circular(30), ), child: Text( tr('components.comic_info_card.viewed'), style: const TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white, height: 1.2, ), strutStyle: const StrutStyle( height: 1.2, ), ), ), ]; } const double _iconSize = 15; final iconFavorite = Icon(Icons.favorite, size: _iconSize, color: Colors.pink[400]); final iconDownload = Icon(Icons.download_rounded, size: _iconSize, color: Colors.pink[400]); final iconVisibility = Icon(Icons.visibility, size: _iconSize, color: Colors.pink[400]); final iconLabelStyle = TextStyle( fontSize: 13, color: Colors.pink.shade400, height: 1.2, ); const iconLabelStrutStyle = StrutStyle( height: 1.2, ); const iconPage = Icon(Icons.ballot_outlined, size: _iconSize, color: Colors.grey); const countLabelStyle = TextStyle( fontSize: 13, color: Colors.grey, height: 1.2, ); final iconMargin = Container(width: 20); final iconSpacing = Container(width: 5); const titleStyle = TextStyle(fontWeight: FontWeight.bold); final authorStyle = TextStyle( fontSize: 13, color: Colors.pink.shade300, ); final authorStyleX = TextStyle( fontSize: 13, color: Colors.pink.shade300.withOpacity(.7), ); ================================================ FILE: lib/screens/components/ComicList.dart ================================================ import 'dart:math'; import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/config/HiddenWords.dart'; import 'package:pikapika/basic/config/ShadowCategories.dart'; import 'package:pikapika/basic/config/ListLayout.dart'; import 'package:pikapika/basic/config/ShadowCategoriesMode.dart'; import 'package:pikapika/screens/components/CommonData.dart'; import '../../basic/config/HiddenViewed.dart'; import 'ComicInfoCard.dart'; import 'Images.dart'; import 'LinkToComicInfo.dart'; import 'ListView.dart'; class ComicListController { _ComicListState? _state; bool get selecting => _state?._selecting ?? false; set selecting(bool value) => _state?._setSelect(value); List get selected => _state?._selected ?? []; selectAll() { _state?._selectAll(); } loadViewed() { WidgetsBinding.instance?.addPostFrameCallback((_) { _state?._loadViewed(); }); } } // 漫画列表页 class ComicList extends StatefulWidget { final Widget? appendWidget; final List comicList; final ScrollController? scrollController; final ComicListController? listController; const ComicList( this.comicList, { this.appendWidget, this.scrollController, Key? key, // required this.listController, }) : super(key: key); @override State createState() => _ComicListState(); } class _ComicListState extends State { final List viewedList = []; bool _selecting = false; List _selected = []; _selectAll() { setState(() { if (_selected.length == widget.comicList.length) { _selected.clear(); } else { _selected.addAll(widget.comicList.map((e) => e.id)); } }); } _setSelect(bool value) { setState(() { _selected.clear(); _selecting = value; }); } Future _loadViewed() async { if (widget.comicList.isNotEmpty) { var list = await method .loadViewedList(widget.comicList.map((e) => e.id).toList()); viewedList.clear(); viewedList.addAll(list); setState(() {}); } } @override void initState() { widget.listController?._state = this; _loadViewed(); listLayoutEvent.subscribe(_onLayoutChange); super.initState(); } @override void dispose() { if (widget.listController?._state == this) { widget.listController?._state = null; } listLayoutEvent.unsubscribe(_onLayoutChange); super.dispose(); } void _onLayoutChange(EventArgs? args) { setState(() {}); } @override Widget build(BuildContext context) { switch (currentLayout) { case ListLayout.INFO_CARD: return _buildInfoCardList(); case ListLayout.ONLY_IMAGE: return _buildGridImageWarp(); case ListLayout.COVER_AND_TITLE: return _buildGridImageTitleWarp(); default: return Container(); } } Widget _buildInfoCardList() { return PikaListView( controller: widget.scrollController, physics: const AlwaysScrollableScrollPhysics(), children: [ ...widget.comicList.map((e) { bool viewed = viewedList.contains(e.id); if (hiddenViewed && viewed) { return InkWell( onTap: () {}, child: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: Theme.of(context).dividerColor, ), ), ), child: Center( child: Text( tr('components.comic_info_card.viewed'), style: TextStyle( fontSize: 12, color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.3), ), ), ), ), ); } late bool shadow; X: switch (currentShadowCategoriesMode()) { case ShadowCategoriesMode.BLACK_LIST: shadow = e.categories .map((c) => shadowCategories.contains(c)) .reduce((value, element) => value || element); break; case ShadowCategoriesMode.WHITE_LIST: for (var c in e.categories) { if (shadowCategories.contains(c)) { shadow = false; break X; } } shadow = true; break; } if (!shadow) { for (var value in hiddenWords) { if (e.title.toLowerCase().contains(value.toLowerCase()) || e.author.toLowerCase().contains(value.toLowerCase())) { shadow = true; break; } } } if (shadow) { return InkWell( onTap: () {}, child: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: Theme.of(context).dividerColor, ), ), ), child: Center( child: Text( tr('components.comic_list.shadow'), style: TextStyle( fontSize: 12, color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.3), ), ), ), ), ); } if (_selecting) { return GestureDetector( onTap: () { setState(() { if (_selected.contains(e.id)) { _selected.remove(e.id); } else { _selected.add(e.id); } }); }, child: Stack(children: [ AbsorbPointer( child: ComicInfoCard( e, viewed: viewedList.contains(e.id), ), ), SizedBox( height: imageHeight, child: Align( alignment: Alignment.bottomRight, child: Container( margin: const EdgeInsets.only(top: 5), padding: const EdgeInsets.only(right: 10, left: 5), decoration: BoxDecoration( color: Colors.grey.shade500.withOpacity(.1), borderRadius: const BorderRadius.only( topLeft: Radius.circular(5), bottomLeft: Radius.circular(5), ), ), child: Padding( padding: const EdgeInsets.all(5), child: Icon( _selected.contains(e.id) ? Icons.check_circle_sharp : Icons.circle_outlined, color: Theme.of(context).colorScheme.secondary, ), ), ), ), ), ]), ); } Widget card = ComicInfoCard( e, viewed: viewedList.contains(e.id), ); if (allSubscribed.containsKey(e.id)) { final subscribed = allSubscribed[e.id]!; if (subscribed.newEpCount > 0) { card = Stack( children: [ card, Positioned( top: 0, left: 0, child: Container( padding: const EdgeInsets.all(2), decoration: const BoxDecoration( color: Colors.red, borderRadius: BorderRadius.only( bottomRight: Radius.circular(5), ), ), child: Text( subscribed.newEpCount.toString(), style: const TextStyle( color: Colors.white, fontSize: 12, ), ), ), ), ], ); } } return LinkToComicInfo(comicId: e.id, child: card); }).toList(), ...widget.appendWidget != null ? [ SizedBox( height: 80, child: widget.appendWidget, ), ] : [], ], ); } Widget _buildGridImageWarp() { var gap = 3.0; var size = MediaQuery.of(context).size; var min = size.width < size.height ? size.width : size.height; var widthAndGap = min / 4; int rowCap = size.width ~/ widthAndGap; var width = widthAndGap - gap * 2; var height = width * coverHeight / coverWidth; List wraps = []; List tmp = []; for (var e in widget.comicList) { late bool shadow; X: switch (currentShadowCategoriesMode()) { case ShadowCategoriesMode.BLACK_LIST: shadow = e.categories .map((c) => shadowCategories.contains(c)) .reduce((value, element) => value || element); break; case ShadowCategoriesMode.WHITE_LIST: for (var c in e.categories) { if (shadowCategories.contains(c)) { shadow = false; break X; } } shadow = true; break; } if (!shadow) { for (var value in hiddenWords) { if (e.title.toLowerCase().contains(value.toLowerCase()) || e.author.toLowerCase().contains(value.toLowerCase())) { shadow = true; break; } } } bool viewed = viewedList.contains(e.id); if (hiddenViewed && viewed) { tmp.add( Container( padding: EdgeInsets.all(gap), child: Container( width: width, height: height, color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.05), child: Center( child: Text( tr('components.comic_info_card.viewed'), textAlign: TextAlign.center, style: TextStyle( fontSize: 12, color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.5), ), ), ), ), ), ); } else if (shadow) { tmp.add( Container( padding: EdgeInsets.all(gap), child: Container( width: width, height: height, color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.05), child: Center( child: Text( tr('components.comic_list.shadow'), textAlign: TextAlign.center, style: TextStyle( fontSize: 12, color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.5), ), ), ), ), ), ); } else if (_selecting) { Widget c = Container( padding: EdgeInsets.all(gap), child: RemoteImage( fileServer: e.thumb.fileServer, path: e.thumb.path, width: width, height: height, ), ); c = GestureDetector( onTap: () { setState(() { if (_selected.contains(e.id)) { _selected.remove(e.id); } else { _selected.add(e.id); } }); }, child: Stack(children: [ AbsorbPointer( child: c, ), SizedBox( width: width, height: height, child: Align( alignment: Alignment.bottomRight, child: Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( color: Colors.grey.shade800.withOpacity(.75), borderRadius: const BorderRadius.all( Radius.circular(5), ), ), child: Padding( padding: const EdgeInsets.all(5), child: Icon( _selected.contains(e.id) ? Icons.check_circle_sharp : Icons.circle_outlined, color: Theme.of(context).colorScheme.secondary, ), ), ), ), ), ]), ); tmp.add(c); } else { Widget c = LinkToComicInfo( comicId: e.id, child: Container( padding: EdgeInsets.all(gap), child: RemoteImage( fileServer: e.thumb.fileServer, path: e.thumb.path, width: width, height: height, ), ), ); if (allSubscribed.containsKey(e.id)) { final subscribed = allSubscribed[e.id]!; if (subscribed.newEpCount > 0) { c = Stack( children: [ c, Positioned( top: 0, left: 0, child: Container( padding: const EdgeInsets.all(2), decoration: const BoxDecoration( color: Colors.red, borderRadius: BorderRadius.only( bottomRight: Radius.circular(5), ), ), child: Text( subscribed.newEpCount.toString(), style: const TextStyle( color: Colors.white, fontSize: 12, ), ), ), ), ], ); } } tmp.add(c); } if (tmp.length == rowCap) { wraps.add(Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceAround, children: tmp, )); tmp = []; } } // 追加特殊按钮 if (widget.appendWidget != null) { tmp.add(Container( color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.transparent) .withOpacity(.1), margin: EdgeInsets.only( left: (rowCap - tmp.length) * gap, right: (rowCap - tmp.length) * gap, top: gap, bottom: gap, ), width: (rowCap - tmp.length) * width, height: height, child: widget.appendWidget, )); } // 最后一页没有下一页所有有可能为空 if (tmp.isNotEmpty) { wraps.add(Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceAround, children: tmp, )); tmp = []; } // 返回 return PikaListView( controller: widget.scrollController, physics: const AlwaysScrollableScrollPhysics(), padding: EdgeInsets.only(top: gap, bottom: gap), children: wraps, ); } Widget _buildGridImageTitleWarp() { var gap = 3.0; var size = MediaQuery.of(context).size; var min = size.width < size.height ? size.width : size.height; var widthAndGap = min / 3; int rowCap = size.width ~/ widthAndGap; var width = widthAndGap - gap * 2; var height = width * coverHeight / coverWidth; double titleFontSize = max(width / 11, 10); double shadowFontSize = max(width / 9, 12); List wraps = []; List tmp = []; for (var e in widget.comicList) { late bool shadow; X: switch (currentShadowCategoriesMode()) { case ShadowCategoriesMode.BLACK_LIST: shadow = e.categories .map((c) => shadowCategories.contains(c)) .reduce((value, element) => value || element); break; case ShadowCategoriesMode.WHITE_LIST: for (var c in e.categories) { if (shadowCategories.contains(c)) { shadow = false; break X; } } shadow = true; break; } if (!shadow) { for (var value in hiddenWords) { if (e.title.toLowerCase().contains(value.toLowerCase()) || e.author.toLowerCase().contains(value.toLowerCase())) { shadow = true; break; } } } bool viewed = viewedList.contains(e.id); if (hiddenViewed && viewed) { tmp.add( Container( padding: EdgeInsets.all(gap), child: Container( width: width, height: height, color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.05), child: Center( child: Text( tr('components.comic_info_card.viewed'), textAlign: TextAlign.center, style: TextStyle( fontSize: shadowFontSize, color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.5), ), ), ), ), ), ); } else if (shadow) { tmp.add( Container( padding: EdgeInsets.all(gap), child: Container( width: width, height: height, color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.05), child: Center( child: Text( tr('components.comic_list.shadow'), textAlign: TextAlign.center, style: TextStyle( fontSize: shadowFontSize, color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.5), ), ), ), ), ), ); } else if (_selecting) { Widget c = Container( margin: EdgeInsets.all(gap), width: width, height: height, child: Stack( children: [ RemoteImage( fileServer: e.thumb.fileServer, path: e.thumb.path, width: width, height: height, ), Align( alignment: Alignment.bottomCenter, child: Container( color: Colors.black.withOpacity(.3), child: Row( children: [ Expanded( child: Text( e.title + '\n', style: TextStyle( color: Colors.white, fontSize: titleFontSize, height: 1.2, ), strutStyle: const StrutStyle(height: 1.2), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ), ), ], ), ); c = GestureDetector( onTap: () { setState(() { if (_selected.contains(e.id)) { _selected.remove(e.id); } else { _selected.add(e.id); } }); }, child: Stack(children: [ AbsorbPointer( child: c, ), SizedBox( width: width, height: height, child: Align( alignment: Alignment.topRight, child: Container( padding: const EdgeInsets.all(5), margin: const EdgeInsets.only(top: 5), decoration: BoxDecoration( color: Colors.grey.shade800.withOpacity(.75), borderRadius: const BorderRadius.all( Radius.circular(5), ), ), child: Padding( padding: const EdgeInsets.all(5), child: Icon( _selected.contains(e.id) ? Icons.check_circle_sharp : Icons.circle_outlined, color: Theme.of(context).colorScheme.secondary, ), ), ), ), ), ]), ); tmp.add(c); } else { Widget c = LinkToComicInfo( comicId: e.id, child: Container( margin: EdgeInsets.all(gap), width: width, height: height, child: Stack( children: [ RemoteImage( fileServer: e.thumb.fileServer, path: e.thumb.path, width: width, height: height, ), Align( alignment: Alignment.bottomCenter, child: Container( color: Colors.black.withOpacity(.3), child: Row( children: [ Expanded( child: Text( e.title + '\n', style: TextStyle( color: Colors.white, fontSize: titleFontSize, height: 1.2, ), strutStyle: const StrutStyle(height: 1.2), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ), ), ], ), ), ); if (allSubscribed.containsKey(e.id)) { final subscribed = allSubscribed[e.id]!; if (subscribed.newEpCount > 0) { c = Stack( children: [ c, Positioned( top: 0, left: 0, child: Container( padding: const EdgeInsets.all(2), decoration: const BoxDecoration( color: Colors.red, borderRadius: BorderRadius.only( bottomRight: Radius.circular(5), ), ), child: Text( subscribed.newEpCount.toString(), style: const TextStyle( color: Colors.white, fontSize: 12, ), ), ), ), ], ); } } tmp.add(c); } if (tmp.length == rowCap) { wraps.add(Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceAround, children: tmp, )); tmp = []; } } // 追加特殊按钮 if (widget.appendWidget != null) { tmp.add(Container( color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.transparent) .withOpacity(.1), margin: EdgeInsets.only( left: (rowCap - tmp.length) * gap, right: (rowCap - tmp.length) * gap, top: gap, bottom: gap, ), width: (rowCap - tmp.length) * width, height: height, child: widget.appendWidget, )); } // 最后一页没有下一页所有有可能为空 if (tmp.isNotEmpty) { wraps.add(Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceAround, children: tmp, )); tmp = []; } // 返回 return PikaListView( controller: widget.scrollController, physics: const AlwaysScrollableScrollPhysics(), padding: EdgeInsets.only(top: gap, bottom: gap), children: wraps, ); } } ================================================ FILE: lib/screens/components/ComicListBuilder.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/config/ShadowCategoriesEvent.dart'; import 'package:pikapika/screens/components/ComicList.dart'; import 'package:pikapika/screens/components/FitButton.dart'; import 'ContentBuilder.dart'; class ComicListBuilder extends StatefulWidget { final Future> Function() takeList; const ComicListBuilder(this.takeList, {Key? key}) : super(key: key); @override State createState() => _ComicListBuilderState(); } class _ComicListBuilderState extends State { late Future> _future = widget.takeList(); late Key _key = UniqueKey(); @override void initState() { shadowCategoriesEvent.subscribe(_onShadowChange); super.initState(); } @override void dispose() { shadowCategoriesEvent.unsubscribe(_onShadowChange); super.dispose(); } void _onShadowChange(EventArgs? args) { setState(() {}); } Future _reload() async { setState(() { _future = widget.takeList(); _key = UniqueKey(); }); } @override Widget build(BuildContext context) { return ContentBuilder( key: _key, future: _future, onRefresh: _reload, successBuilder: (BuildContext context, AsyncSnapshot> snapshot) { return RefreshIndicator( onRefresh: _reload, child: ComicList( snapshot.data!, appendWidget: FitButton( onPressed: _reload, text: tr('app.refresh'), ), ), ); }, ); } } ================================================ FILE: lib/screens/components/ComicPager.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/config/PagerAction.dart'; import 'package:pikapika/basic/config/ShadowCategoriesEvent.dart'; import 'package:pikapika/basic/enum/Sort.dart'; import 'package:pikapika/screens/components/ComicList.dart'; import 'package:pikapika/screens/components/ContentError.dart'; import 'package:pikapika/screens/components/FitButton.dart'; import '../../basic/Common.dart'; import '../../basic/config/IsPro.dart'; import 'ContentLoading.dart'; // 漫画列页 class ComicPager extends StatefulWidget { final bool coll; final ComicListController? comicListController; final Future Function(String sort, int page) fetchPage; const ComicPager({ required this.fetchPage, this.coll = false, Key? key, // required this.comicListController, }) : super(key: key); @override State createState() => _ComicPagerState(); } class _ComicPagerState extends State { @override void initState() { shadowCategoriesEvent.subscribe(_onShadowChange); super.initState(); } @override void dispose() { shadowCategoriesEvent.unsubscribe(_onShadowChange); super.dispose(); } void _onShadowChange(EventArgs? args) { setState(() {}); } @override Widget build(BuildContext context) { switch (currentPagerAction()) { case PagerAction.CONTROLLER: return ControllerComicPager( fetchPage: widget.fetchPage, comicListController: widget.comicListController, coll: widget.coll, ); case PagerAction.STREAM: return StreamComicPager( fetchPage: widget.fetchPage, comicListController: widget.comicListController, coll: widget.coll, ); default: return Container(); } } } class ControllerComicPager extends StatefulWidget { final bool coll; final ComicListController? comicListController; final Future Function(String sort, int page) fetchPage; const ControllerComicPager({ Key? key, required this.fetchPage, required this.comicListController, required this.coll, }) : super(key: key); @override State createState() => _ControllerComicPagerState(); } class _ControllerComicPagerState extends State { final TextEditingController _textEditController = TextEditingController(text: ''); late String _currentSort = SORT_TIME_NEWEST; late int _currentPage = 1; late Future _pageFuture; Future _load() async { setState(() { _pageFuture = widget.fetchPage(_currentSort, _currentPage); }); } @override void initState() { _load(); super.initState(); } @override Widget build(BuildContext context) { return FutureBuilder( future: _pageFuture, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.none) { return Text(tr('app.initializing')); } if (snapshot.connectionState != ConnectionState.done) { return ContentLoading(label: tr('app.loading')); } if (snapshot.hasError) { return ContentError( error: snapshot.error, stackTrace: snapshot.stackTrace, onRefresh: _load, ); } var comicsPage = snapshot.data!; return Scaffold( appBar: _buildAppBar(comicsPage, context), body: ComicList( comicsPage.docs, appendWidget: _buildNextButton(comicsPage), listController: widget.comicListController, ), ); }, ); } PreferredSize _buildAppBar(ComicsPage comicsPage, BuildContext context) { return PreferredSize( preferredSize: const Size.fromHeight(40), child: Container( decoration: BoxDecoration( border: Border( bottom: BorderSide( width: .5, style: BorderStyle.solid, color: Colors.grey[200]!, ), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container(width: 10), DropdownButton( items: widget.coll ? collItems : items, value: _currentSort, onChanged: (String? value) { if (value != null) { _currentPage = 1; _currentSort = value; _load(); } }, ), ], ), InkWell( onTap: () { _textEditController.clear(); showDialog( context: context, builder: (context) { return AlertDialog( content: Card( child: TextField( controller: _textEditController, decoration: const InputDecoration( labelText: "请输入页数:", ), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'\d+')), ], ), ), actions: [ MaterialButton( onPressed: () { Navigator.pop(context); }, child: const Text('取消'), ), MaterialButton( onPressed: () { Navigator.pop(context); var text = _textEditController.text; if (text.isEmpty || text.length > 5) { return; } var num = int.parse(text); if (num == 0 || num > comicsPage.pages) { return; } if (num > 10 && !isPro) { defaultToast(context, "发电以后才能看10页以后的内容"); return; } _currentPage = num; _load(); }, child: const Text('确定'), ), ], ); }, ); }, child: Row( children: [ Text("第 ${comicsPage.page} / ${comicsPage.pages} 页"), ], ), ), Row( children: [ MaterialButton( minWidth: 0, onPressed: () { if (comicsPage.page > 1) { _currentPage = comicsPage.page - 1; _load(); } }, child: const Text('上一页'), ), MaterialButton( minWidth: 0, onPressed: () { if (comicsPage.page < comicsPage.pages) { if (_currentPage >= 10 && !isPro) { defaultToast(context, "发电以后才能看10页以后的内容"); return; } _currentPage = comicsPage.page + 1; _load(); } }, child: const Text('下一页'), ) ], ), ], ), ), ); } Widget? _buildNextButton(ComicsPage comicsPage) { if (comicsPage.page < comicsPage.pages) { return FitButton( onPressed: () { if (_currentPage >= 10 && !isPro) { defaultToast(context, "发电以后才能看10页以后的内容"); return; } _currentPage = comicsPage.page + 1; _load(); }, text: '下一页', ); } return null; } } class StreamComicPager extends StatefulWidget { final bool coll; final ComicListController? comicListController; final Future Function(String sort, int page) fetchPage; const StreamComicPager({ Key? key, required this.fetchPage, required this.comicListController, required this.coll, }) : super(key: key); @override State createState() => _StreamComicPagerState(); } class _StreamComicPagerState extends State { final TextEditingController _textEditController = TextEditingController(text: ''); final _scrollController = ScrollController(); late String _currentSort = SORT_DEFAULT; late int _currentPage = 1; late int _maxPage = 0; late List _list = []; late bool _loading = false; late bool _over = false; late bool _error = false; late bool _noPro = false; // late Future _pageFuture; _onSetOffset(int i) { _list.clear(); _currentPage = i; _load(); } void _onScroll() { if (_over || _error || _loading || _noPro) { return; } if (_scrollController.offset + MediaQuery.of(context).size.height / 2 < _scrollController.position.maxScrollExtent) { return; } _load(); } Future _load() async { setState(() { //_pageFuture = _fetch(); }); } Future _fetch() async { _error = false; setState(() { _loading = true; }); try { var page = await widget.fetchPage(_currentSort, _currentPage); // setState(() { _currentPage++; _maxPage = page.pages; _list.addAll(page.docs); _over = page.page >= page.pages; _noPro = _currentPage > 10 && !isPro; // }); widget.comicListController?.loadViewed(); } catch (e, s) { _error = true; print("$e\n$s"); rethrow; } finally { setState(() { _loading = false; }); } } @override void initState() { _load(); _scrollController.addListener(_onScroll); super.initState(); } @override void dispose() { _scrollController.removeListener(_onScroll); _scrollController.dispose(); _textEditController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: _buildAppBar(context), body: ComicList( _list, scrollController: _scrollController, appendWidget: _buildLoadingCell(), listController: widget.comicListController, ), ); } PreferredSize _buildAppBar(BuildContext context) { return PreferredSize( preferredSize: const Size.fromHeight(40), child: Container( decoration: BoxDecoration( border: Border( bottom: BorderSide( width: .5, style: BorderStyle.solid, color: Colors.grey[200]!, ), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container(width: 10), DropdownButton( items: widget.coll ? collItems : items, value: _currentSort, onChanged: (String? value) { if (value != null) { _list = []; _currentPage = 1; _currentSort = value; _load(); } }, ), ], ), Row( children: [ InkWell( onTap: () { _textEditController.clear(); showDialog( context: context, builder: (context) { return AlertDialog( content: Card( child: TextField( controller: _textEditController, decoration: const InputDecoration( labelText: "请输入页数:", ), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.allow( RegExp(r'\d+')), ], ), ), actions: [ MaterialButton( onPressed: () { Navigator.pop(context); }, child: const Text('取消'), ), MaterialButton( onPressed: () { Navigator.pop(context); var text = _textEditController.text; if (text.isEmpty || text.length > 5) { return; } var num = int.parse(text); if (num == 0 || num > _maxPage) { return; } if (_currentPage >= 10 && !isPro) { defaultToast(context, "发电以后才能看10页以后的内容"); return; } _currentPage = num; _onSetOffset(num); }, child: const Text('确定'), ), ], ); }, ); }, child: Row( children: [ Text("已经加载 ${_currentPage - 1} / $_maxPage 页"), ], ), ), ], ), ], ), ), ); } Widget? _buildLoadingCell() { if (_noPro) { return FitButton(onPressed: () {}, text: '发电以后才能看10页以后的内容'); } if (_error) { return FitButton( onPressed: () { setState(() { _error = false; }); _load(); }, text: '网络错误 / 点击刷新'); } if (_loading) { return FitButton(onPressed: () {}, text: '加载中'); } return null; } } ================================================ FILE: lib/screens/components/ComicTagsCard.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/screens/ComicsScreen.dart'; import 'package:pikapika/basic/Navigator.dart'; // 漫画tag class ComicTagsCard extends StatelessWidget { final List tags; const ComicTagsCard(this.tags, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { var theme = Theme.of(context); return Container( padding: const EdgeInsets.only(top: 5, bottom: 5), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: theme.dividerColor, ), ), ), child: Wrap( children: tags.map((e) { return InkWell( onTap: () { navPushOrReplace(context, (context) => ComicsScreen(tag: e)); }, child: Container( padding: const EdgeInsets.only( left: 10, right: 10, top: 3, bottom: 3, ), margin: const EdgeInsets.only( left: 5, right: 5, top: 3, bottom: 3, ), decoration: BoxDecoration( color: Colors.pink.shade100, border: Border.all( style: BorderStyle.solid, color: Colors.pink.shade400, ), borderRadius: const BorderRadius.all(Radius.circular(30)), ), child: Text( e, style: TextStyle( color: Colors.pink.shade500, height: 1.4, ), strutStyle: const StrutStyle( height: 1.4, ), ), ), ); }).toList(), ), ); } } ================================================ FILE: lib/screens/components/CommentItem.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import '../../basic/Cross.dart'; import 'Avatar.dart'; import 'CommentMainType.dart'; class ComicCommentItem extends StatefulWidget { final CommentMainType mainType; final String mainId; final CommentBase comment; const ComicCommentItem(this.mainType, this.mainId, this.comment, {Key? key}) : super(key: key); @override State createState() => _ComicCommentItemState(); } class _ComicCommentItemState extends State { var likeLoading = false; @override Widget build(BuildContext context) { var comment = widget.comment; var theme = Theme.of(context); var nameStyle = const TextStyle(fontWeight: FontWeight.bold); var levelStyle = TextStyle( fontSize: 12, color: theme.colorScheme.secondary.withOpacity(.8)); var connectStyle = TextStyle(color: theme.textTheme.bodyText1?.color?.withOpacity(.8)); var datetimeStyle = TextStyle( color: theme.textTheme.bodyText1?.color?.withOpacity(.6), fontSize: 12); return Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( border: Border( top: BorderSide( width: .25, style: BorderStyle.solid, color: Colors.grey.shade500.withOpacity(.5), ), bottom: BorderSide( width: .25, style: BorderStyle.solid, color: Colors.grey.shade500.withOpacity(.5), ), ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Avatar(comment.user.avatar), Container(width: 5), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return SizedBox( width: constraints.maxWidth, child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, alignment: WrapAlignment.spaceBetween, children: [ Text(comment.user.name, style: nameStyle), Text( formatTimeToDateTime(comment.createdAt), style: datetimeStyle, ), ], ), ); }, ), Container(height: 3), LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return SizedBox( width: constraints.maxWidth, child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, alignment: WrapAlignment.spaceBetween, children: [ Text( "Lv. ${comment.user.level} (${comment.user.title})", style: levelStyle), Text.rich(TextSpan( style: levelStyle, children: [ comment.commentsCount > 0 ? TextSpan(children: [ WidgetSpan( alignment: PlaceholderAlignment.middle, child: Icon(Icons.message, size: 13, color: theme.colorScheme.secondary .withOpacity(.7)), ), WidgetSpan(child: Container(width: 5)), TextSpan( text: '${comment.commentsCount}', ), ]) : const TextSpan(), WidgetSpan(child: Container(width: 12)), WidgetSpan( child: GestureDetector( onTap: () async { setState(() { likeLoading = true; }); try { switch (widget.mainType) { case CommentMainType.COMIC: await method.switchLikeComment( comment.id, widget.mainId, ); break; case CommentMainType.GAME: await method.switchLikeGameComment( comment.id, widget.mainId, ); break; } setState(() { if (comment.isLiked) { comment.isLiked = false; comment.likesCount--; } else { comment.isLiked = true; comment.likesCount++; } }); } catch (e, s) { print("$e\n$s"); defaultToast(context, tr("app.like_failed")); } finally { setState(() { likeLoading = false; }); } }, child: Text.rich( TextSpan(style: levelStyle, children: [ WidgetSpan( alignment: PlaceholderAlignment.middle, child: Icon( likeLoading ? Icons.refresh : comment.isLiked ? Icons.favorite : Icons.favorite_border, size: 13, color: theme.colorScheme.secondary .withOpacity(.7)), ), WidgetSpan(child: Container(width: 5)), TextSpan( text: '${comment.likesCount}', ), ]), ), )), ], )), ], ), ); }, ), Container(height: 5), GestureDetector( onLongPress: () { confirmCopy(context, comment.content); }, child: Text(comment.content, style: connectStyle), ), ], ), ), ], ), ); } } ================================================ FILE: lib/screens/components/CommentList.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Entities.dart' as e; import 'package:pikapika/screens/CommentScreen.dart'; import 'package:pikapika/screens/components/BottomSheetInput.dart'; import 'package:pikapika/screens/components/ItemBuilder.dart'; import 'package:pikapika/basic/Method.dart'; import '../../basic/config/IconLoading.dart'; import 'CommentItem.dart'; import 'CommentMainType.dart'; class _CommentBasePage extends e.Page { late List docs; _CommentBasePage.ofComic(CommentPage commentPage) : super.of(commentPage.total, commentPage.limit, commentPage.page, commentPage.pages) { this.docs = commentPage.docs; } _CommentBasePage.ofGame(GameCommentPage commentPage) : super.of(commentPage.total, commentPage.limit, commentPage.page, commentPage.pages) { this.docs = commentPage.docs; } } // 漫画的评论列表 class CommentList extends StatefulWidget { final CommentMainType mainType; final String mainId; const CommentList(this.mainType, this.mainId, {Key? key}) : super(key: key); @override State createState() => _CommentListState(); } class _CommentListState extends State { late int _currentPage = 1; late Future<_CommentBasePage> _future = _loadPage(); Future<_CommentBasePage> _loadPage() async { switch (widget.mainType) { case CommentMainType.COMIC: return _CommentBasePage.ofComic( await method.comments(widget.mainId, _currentPage), ); case CommentMainType.GAME: return _CommentBasePage.ofGame( await method.gameComments(widget.mainId, _currentPage), ); } } @override Widget build(BuildContext context) { return ItemBuilder( future: _future, successBuilder: (BuildContext context, AsyncSnapshot<_CommentBasePage> snapshot) { var page = snapshot.data!; return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildPrePage(page), ...page.docs.map((e) => _buildComment(e)), _buildNextPage(page), _buildPostComment(), ], ); }, onRefresh: () async => { setState(() { _future = _loadPage(); }) }, ); } Widget _buildComment(CommentBase comment) { return InkWell( onTap: () { Navigator.of(context).push( mixRoute( builder: (context) => CommentScreen(widget.mainType, widget.mainId, comment), ), ); }, child: ComicCommentItem(widget.mainType, widget.mainId, comment), ); } Widget _buildPostComment() { return InkWell( onTap: () async { showInputModalBottomSheet( context: context, onSubmitted: (text) async { switch (widget.mainType) { case CommentMainType.COMIC: await method.postComment(widget.mainId, text); break; case CommentMainType.GAME: await method.postGameComment(widget.mainId, text); break; } setState(() { _future = _loadPage(); }); defaultToast(context, tr("screen.comment.comment_success")); }, hintText: tr("screen.comment.please_enter_comment"), ); }, child: Container( decoration: BoxDecoration( border: Border( top: BorderSide( width: .25, style: BorderStyle.solid, color: Colors.grey.shade500.withOpacity(.5), ), bottom: BorderSide( width: .25, style: BorderStyle.solid, color: Colors.grey.shade500.withOpacity(.5), ), ), ), padding: const EdgeInsets.all(30), child: Center( child: Text(tr("screen.comment.i_have_something_to_say")), ), ), ); } Widget _buildPrePage(_CommentBasePage page) { if (page.page > 1) { return InkWell( onTap: () { setState(() { _currentPage = page.page - 1; _future = _loadPage(); }); }, child: Container( padding: const EdgeInsets.all(30), child: Center( child: Text(tr("app.previous_page")), ), ), ); } return Container(); } Widget _buildNextPage(_CommentBasePage page) { if (page.page < page.pages) { return InkWell( onTap: () { setState(() { _currentPage = page.page + 1; _future = _loadPage(); }); }, child: Container( padding: const EdgeInsets.all(30), child: Center( child: Text(tr("app.next_page")), ), ), ); } return Container(); } } ================================================ FILE: lib/screens/components/CommentMainType.dart ================================================ enum CommentMainType { COMIC, GAME, } ================================================ FILE: lib/screens/components/Common.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/screens/components/ComicList.dart'; import '../../basic/config/IsPro.dart'; import '../../basic/config/ListLayout.dart'; import '../../basic/config/ShadowCategories.dart'; import '../../basic/config/ShadowCategoriesMode.dart'; Widget commonPopMenu( BuildContext context, { ComicListController? comicListController, void Function(VoidCallback fn)? setState, }) { return PopupMenuButton( itemBuilder: (BuildContext context) => >[ PopupMenuItem( value: 0, child: ListTile( leading: const Icon(Icons.view_quilt), title: Text(tr("components.common.display_mode")), ), ), PopupMenuItem( value: 1, child: ListTile( leading: const Icon(Icons.do_not_disturb_on_outlined), title: Text(tr("components.common.shadow_mode")), ), ), PopupMenuItem( value: 2, child: ListTile( leading: const Icon(Icons.hide_source), title: Text(tr("components.common.shadow_list")), ), ), ...comicListController != null && setState != null ? [ PopupMenuItem( value: 3, child: ListTile( leading: Icon( Icons.download, color: isPro ? null : Colors.grey, ), title: Text( tr("components.common.batch_download") + (isPro ? "" : "(${tr('app.pro')})"), style: TextStyle( color: isPro ? null : Colors.grey, ), ), ), ) ] : [], ], onSelected: (int value) { switch (value) { case 0: chooseListLayout(context); break; case 1: chooseShadowCategoriesMode(context); break; case 2: chooseShadowCategories(context); break; case 3: if (!isPro) { defaultToast(context, tr("app.pro_required")); return; } if (setState != null) { if (comicListController != null) { setState(() { comicListController.selecting = !comicListController.selecting; }); } } break; } }, ); } ================================================ FILE: lib/screens/components/CommonData.dart ================================================ import 'package:event/event.dart'; import '../../basic/Entities.dart'; import '../../basic/Method.dart'; final subscribedEvent = Event(); final Map allSubscribed = {}; Future updateSubscribed() async { await method.updateSubscribed(); await _update(); } Future updateSubscribedForce() async { await method.updateSubscribedForce(); await _update(); } Future _update() async { final _allSubscribed = await method.allSubscribed(); allSubscribed.clear(); for (var subscribed in _allSubscribed) { allSubscribed[subscribed.id] = subscribed; } subscribedEvent.broadcast(); } Future removeAllSubscribed() async { await method.removeAllSubscribed(); allSubscribed.clear(); subscribedEvent.broadcast(); } Future subscribedViewed(String id) async { if (allSubscribed.containsKey(id)) { allSubscribed.remove(id); subscribedEvent.broadcast(); } } ================================================ FILE: lib/screens/components/ContentBuilder.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'ContentError.dart'; import 'ContentLoading.dart'; class ContentBuilder extends StatelessWidget { final Future future; final Future Function() onRefresh; final AsyncWidgetBuilder successBuilder; const ContentBuilder({ required Key? key, required this.future, required this.onRefresh, required this.successBuilder, }) : super(key: key); @override Widget build(BuildContext context) { return FutureBuilder( future: future, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return ContentError( error: snapshot.error, stackTrace: snapshot.stackTrace, onRefresh: onRefresh, ); } if (snapshot.connectionState != ConnectionState.done) { return ContentLoading(label: tr('app.loading')); } return successBuilder(context, snapshot); }, ); } } ================================================ FILE: lib/screens/components/ContentError.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/config/ContentFailedReloadAction.dart'; import 'package:pikapika/basic/enum/ErrorTypes.dart'; class ContentError extends StatelessWidget { final Object? error; final StackTrace? stackTrace; final Future Function() onRefresh; const ContentError({ Key? key, required this.error, required this.stackTrace, required this.onRefresh, }) : super(key: key); @override Widget build(BuildContext context) { var type = errorType("$error"); late String message; late IconData iconData; switch (type) { case ERROR_TYPE_NETWORK: iconData = Icons.wifi_off_rounded; message = tr("app.network_error"); break; case ERROR_TYPE_PERMISSION: iconData = Icons.highlight_off; message = tr("app.no_permission"); break; case ERROR_TYPE_TIME: iconData = Icons.timer_off; message = tr("app.check_device_time"); break; case ERROR_TYPE_UNDER_REVIEW: iconData = Icons.highlight_off; message = tr("app.resource_not_available"); break; default: iconData = Icons.highlight_off; message = tr("app.something_went_wrong"); break; } return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { print("$error"); print("$stackTrace"); var width = constraints.maxWidth; var height = constraints.maxHeight; var min = width < height ? width : height; var iconSize = min / 2.3; var textSize = min / 16; var tipSize = min / 20; var infoSize = min / 30; if (contentFailedReloadAction == ContentFailedReloadAction.TOUCH_LOADER) { return GestureDetector( onTap: onRefresh, child: ListView( children: [ SizedBox( height: height, child: Column( children: [ Expanded(child: Container()), Icon( iconData, size: iconSize, color: Colors.grey.shade600, ), Container(height: min / 10), Container( padding: const EdgeInsets.only( left: 30, right: 30, ), child: Text( message, style: TextStyle(fontSize: textSize), textAlign: TextAlign.center, ), ), Text('(${tr("app.click_refresh")})', style: TextStyle(fontSize: tipSize)), Container(height: min / 15), Text('$error', style: TextStyle(fontSize: infoSize)), Expanded(child: Container()), ], ), ), ], ), ); } return RefreshIndicator( onRefresh: onRefresh, child: ListView( children: [ SizedBox( height: height, child: Column( children: [ Expanded(child: Container()), Icon( iconData, size: iconSize, color: Colors.grey.shade600, ), Container(height: min / 10), Container( padding: const EdgeInsets.only( left: 30, right: 30, ), child: Text( message, style: TextStyle(fontSize: textSize), textAlign: TextAlign.center, ), ), Text('(${tr("app.pull_down_refresh")})', style: TextStyle(fontSize: tipSize)), Container(height: min / 15), Text('$error', style: TextStyle(fontSize: infoSize)), Expanded(child: Container()), ], ), ), ], ), ); }, ); } } ================================================ FILE: lib/screens/components/ContentLoading.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/basic/config/IconLoading.dart'; class ContentLoading extends StatelessWidget { final String label; const ContentLoading({Key? key, required this.label}) : super(key: key); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var width = constraints.maxWidth; var height = constraints.maxHeight; var min = width < height ? width : height; var theme = Theme.of(context); return Center( child: Column( children: [ Expanded(child: Container()), SizedBox( width: min / 2, height: min / 2, child: currentIconLoading() ? Icon(Icons.refresh, color: Colors.grey[400], size: min / 2) : CircularProgressIndicator( color: theme.colorScheme.secondary, backgroundColor: Colors.grey[100], ), ), Container(height: min / 10), Text(label, style: TextStyle(fontSize: min / 15)), Expanded(child: Container()), ], ), ); }, ); } } ================================================ FILE: lib/screens/components/ContentMessage.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import '../../basic/config/ContentFailedReloadAction.dart'; class ContentMessage extends StatelessWidget { final RefreshCallback? onRefresh; final IconData icon; final String message; const ContentMessage({ required this.message, required this.icon, this.onRefresh, Key? key, }) : super(key: key); @override Widget build(BuildContext context) { if (onRefresh != null) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var width = constraints.maxWidth; var height = constraints.maxHeight; var min = width < height ? width : height; var iconSize = min / 2.3; var textSize = min / 16; var tipSize = min / 20; if (contentFailedReloadAction == ContentFailedReloadAction.TOUCH_LOADER) { return GestureDetector( onTap: () { onRefresh!(); }, child: ListView( children: [ SizedBox( height: height, child: Column( children: [ Expanded(child: Container()), Icon( icon, size: iconSize, color: Colors.grey.shade600, ), Container(height: min / 10), Container( padding: const EdgeInsets.only( left: 30, right: 30, ), child: Text( message, style: TextStyle(fontSize: textSize), textAlign: TextAlign.center, ), ), Text('(${tr("app.click_refresh")})', style: TextStyle(fontSize: tipSize)), Expanded(child: Container()), ], ), ), ], ), ); } return RefreshIndicator( onRefresh: () async { onRefresh!(); }, child: ListView( children: [ SizedBox( height: height, child: Column( children: [ Expanded(child: Container()), Icon( icon, size: iconSize, color: Colors.grey.shade600, ), Container(height: min / 10), Container( padding: const EdgeInsets.only( left: 30, right: 30, ), child: Text( message, style: TextStyle(fontSize: textSize), textAlign: TextAlign.center, ), ), Text('(${tr("app.pull_down_refresh")})', style: TextStyle(fontSize: tipSize)), Container(height: min / 15), Expanded(child: Container()), ], ), ), ], ), ); }, ); } return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var width = constraints.maxWidth; var height = constraints.maxHeight; var min = width < height ? width : height; return Center( child: Column( children: [ Expanded(child: Container()), SizedBox( width: min / 2, height: min / 2, child: Icon(icon, color: Colors.grey[100]), ), Container(height: min / 10), Text(message, style: TextStyle(fontSize: min / 15)), Expanded(child: Container()), ], ), ); }, ); } } ================================================ FILE: lib/screens/components/ContinueReadButton.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Entities.dart'; // 继续阅读按钮 class ContinueReadButton extends StatefulWidget { final Future viewFuture; final Function(int? epOrder, int? pictureRank) onChoose; const ContinueReadButton({ Key? key, required this.viewFuture, required this.onChoose, }) : super(key: key); @override State createState() => _ContinueReadButtonState(); } class _ContinueReadButtonState extends State { @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var width = constraints.maxWidth; return FutureBuilder( future: widget.viewFuture, builder: (BuildContext context, AsyncSnapshot snapshot) { late void Function() onPressed; late String text; if (snapshot.connectionState != ConnectionState.done) { onPressed = () {}; text = tr('app.loading'); } if (snapshot.data != null && snapshot.data!.lastViewEpOrder > 0) { onPressed = () => widget.onChoose( snapshot.data?.lastViewEpOrder, snapshot.data?.lastViewPictureRank, ); text = '${tr('app.continue_reading')} ${snapshot.data?.lastViewEpTitle} P. ${(snapshot.data?.lastViewPictureRank ?? 0) + 1}'; } else { onPressed = () => widget.onChoose(null, null); text = tr('app.start_reading'); } return Container( padding: const EdgeInsets.only(left: 10, right: 10), margin: const EdgeInsets.only(bottom: 10), width: width, child: MaterialButton( onPressed: onPressed, child: Row( children: [ Expanded( child: Container( color: Theme.of(context) .textTheme .bodyText1! .color! .withOpacity(.05), padding: const EdgeInsets.all(10), child: Text( text, textAlign: TextAlign.center, ), ), ) ], ), ), ); }, ); }, ); } } ================================================ FILE: lib/screens/components/DesktopCropper.dart ================================================ import 'dart:io'; import 'dart:ui' as ui; import 'package:pikapika/i18.dart'; import 'package:image/image.dart' as image; import 'package:crop_image/crop_image.dart'; import 'package:flutter/material.dart'; class DesktopCropper extends StatefulWidget { final String? title; final double? aspectRatio; final String file; const DesktopCropper({ Key? key, this.title, this.aspectRatio, required this.file, }) : super(key: key); @override State createState() => _DesktopCropperState(); } class _DesktopCropperState extends State { late final _controller = CropController( aspectRatio: widget.aspectRatio, ); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title ?? tr("app.image_crop")), actions: [ IconButton(onPressed: _finish, icon: const Icon(Icons.done)), ], ), body: Center( child: Padding( padding: const EdgeInsets.all(12.0), child: CropImage( controller: _controller, image: Image.file(File(widget.file)), ), ), ), ); } Future _finish() async { var cropped = await _controller.croppedBitmap(); var data = await cropped.toByteData(format: ui.ImageByteFormat.png); if (data != null) { var u8list = data.buffer.asUint8List(); image.Image? baseSizeImage = image.decodePng(u8list); if (baseSizeImage != null) { if (cropped.width > 200) { baseSizeImage = image.copyResize(baseSizeImage, height: 200, width: 200); } var f = image.encodeJpg(baseSizeImage); Navigator.of(context).pop(f); } } } } ================================================ FILE: lib/screens/components/DownloadComicsScreen.dart ================================================ import 'package:flutter/material.dart'; import '../../basic/Channels.dart'; import '../../basic/Common.dart'; import '../../basic/Method.dart'; import 'ContentLoading.dart'; class DownloadComicsScreen extends StatefulWidget { final List comicIds; const DownloadComicsScreen(this.comicIds, {Key? key}) : super(key: key); @override State createState() => _DownloadComicsScreenState(); } class _DownloadComicsScreenState extends State { bool exporting = false; bool exported = false; bool exportFail = false; dynamic e; String exportMessage = "正在创建下载任务"; @override void initState() { registerEvent(_onMessageChange, "EXPORT"); super.initState(); } @override void dispose() { unregisterEvent(_onMessageChange); super.dispose(); } void _onMessageChange(event) { setState(() { exportMessage = event; }); } @override Widget build(BuildContext context) { return WillPopScope( child: Scaffold( appBar: AppBar( title: const Text("批量下载"), ), body: _body(), ), onWillPop: () async { if (exporting) { defaultToast(context, "创建下载任务中, 请稍后"); return false; } return true; }, ); } Widget _body() { if (exporting) { return ContentLoading(label: exportMessage); } if (exportFail) { return Center(child: Text("失败\n$e")); } if (exported) { return const Center(child: Text("成功")); } return ListView( children: [ Container(height: 20), Container(height: 20), _buildButtonInner("您即将下载${widget.comicIds.length}部漫画, 如果漫画已经存在, 则补充新增加的章节"), Container(height: 20), Container(height: 20), MaterialButton( onPressed: _create, child: _buildButtonInner("确认"), ), Container(height: 20), Container(height: 20), Container(height: 20), ], ); } _create() async { var name = ""; try { setState(() { exporting = true; }); await method.downloadAll( widget.comicIds, ); exported = true; } catch (err) { e = err; exportFail = true; } finally { setState(() { exporting = false; }); } } Widget _buildButtonInner(String text) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Container( width: constraints.maxWidth, padding: const EdgeInsets.all(15), color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black) .withOpacity(.05), child: Text( text, textAlign: TextAlign.center, ), ); }, ); } } ================================================ FILE: lib/screens/components/DownloadInfoCard.dart ================================================ import 'dart:convert'; import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Cross.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/screens/components/Images.dart'; import '../../basic/config/CopyFullName.dart'; import '../../basic/config/CopyFullNameTemplate.dart'; import 'ComicInfoCard.dart'; // 下载项 class DownloadInfoCard extends StatelessWidget { final DownloadComic task; final bool downloading; final bool linkItem; const DownloadInfoCard({ Key? key, required this.task, this.downloading = false, this.linkItem = false, }) : super(key: key); @override Widget build(BuildContext context) { var theme = Theme.of(context); var textColor = theme.textTheme.bodyText1!.color!; var textColorAlpha = textColor.withAlpha(0x33); var textColorSummary = textColor.withAlpha(0xCC); var titleStyle = TextStyle( color: textColor, fontWeight: FontWeight.bold, ); var categoriesStyle = TextStyle( fontSize: 13, color: textColorSummary, ); var authorStyle = TextStyle( fontSize: 13, color: Colors.pink.shade300, ); var iconColor = Colors.pink.shade300; var iconLabelStyle = TextStyle( fontSize: 13, color: iconColor, ); List categories = json.decode(task.categories); var categoriesString = categories.map((e) => "$e").join(" "); return Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: theme.dividerColor, ), ), ), child: Row( children: [ Container( padding: const EdgeInsets.only(right: 10), child: task.thumbLocalPath == "" ? RemoteImage( fileServer: task.thumbFileServer, path: task.thumbPath, width: imageWidth, height: imageHeight, ) : DownloadImage( path: task.thumbLocalPath, width: imageWidth, height: imageHeight, ), ), Expanded( child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ linkItem ? GestureDetector( onLongPress: () { if (copyFullName()) { var fin = copyFullNameTemplate() .replaceAll("{title}", task.title) .replaceAll("{author}", task.author); if (fin.isEmpty) { fin = task.title; } confirmCopy( context, fin); } else { confirmCopy(context, task.title); } }, child: Text(task.title, style: titleStyle), ) : Text(task.title, style: titleStyle), Container(height: 5), linkItem ? GestureDetector( onLongPress: () { confirmCopy(context, task.author); }, child: Text(task.author, style: authorStyle), ) : Text(task.author, style: authorStyle), Container(height: 5), Text( "${tr('app.categories')}: $categoriesString", style: categoriesStyle, ), Container(height: 5), Row( children: [ Icon( Icons.download, size: iconSize, color: iconColor, ), Container(width: 5), Text( '${tr('app.download')} ${task.downloadPictureCount} / ${task.selectedPictureCount}', style: iconLabelStyle, ), Container(width: 20), task.deleting ? Text(tr('app.deleting'), style: TextStyle( color: Color.alphaBlend( textColor.withAlpha(0x33), Colors.red.shade500))) : task.downloadFailed ? Text(tr('app.download_failed'), style: TextStyle( color: Color.alphaBlend( textColor.withAlpha(0x33), Colors.red.shade500))) : task.downloadFinished ? Text(tr('app.download_finished'), style: TextStyle( color: Color.alphaBlend( textColorAlpha, Colors.green.shade500))) : downloading // downloader.downloadingTask() == task.id ? Text(tr('app.downloading'), style: TextStyle( color: Color.alphaBlend( textColorAlpha, Colors .blue.shade500))) : Text(tr('app.queue'), style: TextStyle( color: Color.alphaBlend( textColorAlpha, Colors.lightBlue .shade500))), ], ), ], ), ), Container( padding: const EdgeInsets.only(left: 8), height: imageHeight, child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ buildFinished(task.finished), ], ), ), ], ), ), ], ), ); } } double imageWidth = 210 / 3.15; double imageHeight = 315 / 3.15; double iconSize = 15; ================================================ FILE: lib/screens/components/FitButton.dart ================================================ import 'package:flutter/material.dart'; class FitButton extends StatelessWidget { final void Function() onPressed; final String text; const FitButton({Key? key, required this.onPressed, required this.text}) : super(key: key); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return SizedBox( width: constraints.maxWidth, height: constraints.maxHeight, child: Container( padding: const EdgeInsets.all(10), child: MaterialButton( onPressed: onPressed, child: Center( child: Text(text), ), ), ), ); }, ); } } ================================================ FILE: lib/screens/components/GameTitleCard.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:pikapika/basic/Cross.dart'; import 'package:pikapika/basic/Entities.dart'; import 'Images.dart'; // 游戏信息卡 class GameTitleCard extends StatelessWidget { final GameInfo info; const GameTitleCard(this.info, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { double iconMargin = 20; double iconSize = 60; BorderRadius iconRadius = const BorderRadius.all(Radius.circular(6)); TextStyle titleStyle = const TextStyle(fontSize: 16, fontWeight: FontWeight.bold); TextStyle publisherStyle = TextStyle( color: Theme.of(context).colorScheme.secondary, fontSize: 12.5, ); TextStyle versionStyle = const TextStyle(fontSize: 12.5); double platformMargin = 10; double platformSize = 25; return Row( children: [ Container( padding: EdgeInsets.all(iconMargin), child: ClipRRect( borderRadius: iconRadius, child: RemoteImage( width: iconSize, height: iconSize, fileServer: info.icon.fileServer, path: info.icon.path, ), ), ), Container(width: 10), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( onLongPress: () { confirmCopy(context, info.title); }, child: Text(info.title, style: titleStyle), ), GestureDetector( onLongPress: () { confirmCopy(context, info.publisher); }, child: Text(info.publisher, style: publisherStyle), ), Text(info.version, style: versionStyle), ], ), ), Container( margin: EdgeInsets.only(right: iconMargin), // padding: const EdgeInsets.only( // left: platformMargin, // right: platformMargin, // ), child: Column( children: [ ...info.android ? [ SvgPicture.asset( 'lib/assets/android.svg', fit: BoxFit.contain, width: platformSize, height: platformSize, color: Colors.green.shade500, ), ] : [], Container( height: platformMargin, ), ...info.ios ? [ SvgPicture.asset( 'lib/assets/apple.svg', fit: BoxFit.contain, width: platformSize, height: platformSize, color: Colors.grey.shade500, ), ] : [], ], ), ), ], ); } } ================================================ FILE: lib/screens/components/GoDownloadSelect.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/screens/components/ComicList.dart'; import 'DownloadComicsScreen.dart'; AppBar downAppBar( BuildContext context, ComicListController _comicListController, void Function(VoidCallback fn) setState, ) { return AppBar( actions: [ MaterialButton( minWidth: 0, onPressed: () async { setState(() { _comicListController.selecting = false; }); }, child: Column( children: [ Expanded(child: Container()), const Icon( Icons.cancel_outlined, size: 18, color: Colors.white, ), Text( tr('app.cancel'), style: const TextStyle(fontSize: 14, color: Colors.white), ), Expanded(child: Container()), ], ), ), MaterialButton( minWidth: 0, onPressed: () async { _comicListController.selectAll(); }, child: Column( children: [ Expanded(child: Container()), const Icon( Icons.select_all, size: 18, color: Colors.white, ), Text( tr('app.select_all'), style: const TextStyle(fontSize: 14, color: Colors.white), ), Expanded(child: Container()), ], ), ), MaterialButton( minWidth: 0, onPressed: () async { var list = _comicListController.selected; if (list.isEmpty) { defaultToast(context, tr("app.please_select_comic")); return; } list = list.toList(); setState(() { _comicListController.selecting = false; }); Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) { return DownloadComicsScreen(list); }, )); }, child: Column( children: [ Expanded(child: Container()), const Icon( Icons.check, size: 18, color: Colors.white, ), Text( tr('app.confirm'), style: const TextStyle(fontSize: 14, color: Colors.white), ), Expanded(child: Container()), ], ), ), ], ); } ================================================ FILE: lib/screens/components/ImageReader.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:another_xlider/another_xlider.dart'; import 'package:pikapika/i18.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Cross.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/config/Address.dart'; import 'package:pikapika/basic/config/AutoFullScreen.dart'; import 'package:pikapika/basic/config/AutoFullScreenOnForward.dart'; import 'package:pikapika/basic/config/CategoriesColumnCount.dart'; import 'package:pikapika/basic/config/FullScreenAction.dart'; import 'package:pikapika/basic/config/ImageAddress.dart'; import 'package:pikapika/basic/config/ImageFilter.dart'; import 'package:pikapika/basic/config/KeyboardController.dart'; import 'package:pikapika/basic/config/NoAnimation.dart'; import 'package:pikapika/basic/config/DragRegionLock.dart'; import 'package:pikapika/basic/config/GestureSpeed.dart'; import 'package:pikapika/basic/config/Quality.dart'; import 'package:pikapika/basic/config/ReaderDirection.dart'; import 'package:pikapika/basic/config/ReaderSliderPosition.dart'; import 'package:pikapika/basic/config/ReaderTwoPageDirection.dart'; import 'package:pikapika/basic/config/ReaderType.dart'; import 'package:pikapika/basic/config/ThreeKeepRight.dart'; import 'package:pikapika/basic/config/VolumeController.dart'; import 'package:pikapika/screens/components/PkzImages.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:zoomable_positioned_list/zoomable_positioned_list.dart' as zoomable; import 'gesture_zoom_box.dart'; import '../../basic/config/IconLoading.dart'; import '../../basic/config/ReaderBackgroundColor.dart'; import '../../basic/config/ReaderScrollByScreenPercentage.dart'; import '../../basic/config/WebToonScrollMode.dart'; import '../../basic/config/ReaderZoomScale.dart'; import '../../basic/config/UseApiLoadImage.dart'; import '../../basic/config/VolumeNextChapter.dart'; import '../FilePhotoViewScreen.dart'; import 'Images.dart'; /////////////// Event<_ReaderControllerEventArgs> _readerControllerEvent = Event<_ReaderControllerEventArgs>(); class _ReaderControllerEventArgs extends EventArgs { final String key; _ReaderControllerEventArgs(this.key); } Widget readerKeyboardHolder(Widget widget) { if (keyboardController && (Platform.isWindows || Platform.isMacOS || Platform.isLinux)) { widget = RawKeyboardListener( focusNode: FocusNode(), child: widget, autofocus: true, onKey: (event) { if (event is RawKeyDownEvent) { if (event.isKeyPressed(LogicalKeyboardKey.arrowUp)) { _readerControllerEvent.broadcast(_ReaderControllerEventArgs("UP")); } if (event.isKeyPressed(LogicalKeyboardKey.arrowDown)) { _readerControllerEvent .broadcast(_ReaderControllerEventArgs("DOWN")); } } }, ); } return widget; } void _onVolumeEvent(dynamic args) { _readerControllerEvent.broadcast(_ReaderControllerEventArgs("$args")); } var _volumeListenCount = 0; // 仅支持安卓 // 监听后会拦截安卓手机音量键 // 仅最后一次监听生效 // event可能为DOWN/UP EventChannel volumeButtonChannel = const EventChannel("volume_button"); StreamSubscription? volumeS; void addVolumeListen() { _volumeListenCount++; if (_volumeListenCount == 1) { volumeS = volumeButtonChannel.receiveBroadcastStream().listen(_onVolumeEvent); } } void delVolumeListen() { _volumeListenCount--; if (_volumeListenCount == 0) { volumeS?.cancel(); } } /////////////////////////////////////////////////////////////////////////////// // 对Reader的传参以及封装 class PkzFile { final String pkzPath; final String path; PkzFile(this.pkzPath, this.path); } class ImageReaderItemPosition { final int index; final double itemLeadingEdge; final double itemTrailingEdge; ImageReaderItemPosition( this.index, this.itemLeadingEdge, this.itemTrailingEdge); } class ReaderImageInfo { final String fileServer; final String path; final String? downloadLocalPath; final int? width; final int? height; final String? format; final int? fileSize; final PkzFile? pkzFile; ReaderImageInfo( this.fileServer, this.path, this.downloadLocalPath, this.width, this.height, this.format, this.fileSize, { this.pkzFile, }); } class ImageReaderStruct { final List images; final bool fullScreen; final FutureOr Function(bool fullScreen) onFullScreenChange; final FutureOr Function(int) onPositionChange; final int? initPosition; final Map epNameMap; final int epOrder; final String comicTitle; final FutureOr Function(int) onChangeEp; final FutureOr Function() onReloadEp; final FutureOr Function() onDownload; const ImageReaderStruct({ required this.images, required this.fullScreen, required this.onFullScreenChange, required this.onPositionChange, this.initPosition, required this.epNameMap, required this.epOrder, required this.comicTitle, required this.onChangeEp, required this.onReloadEp, required this.onDownload, }); } // class ImageReader extends StatefulWidget { final ImageReaderStruct struct; const ImageReader(this.struct, {Key? key}) : super(key: key); @override State createState() => _ImageReaderState(); } class _ImageReaderState extends State { // 记录初始方向 final ReaderDirection _pagerDirection = gReaderDirection; // 记录初始阅读器类型 final ReaderType _pagerType = currentReaderType(); // 记录了控制器 late final FullScreenAction _fullScreenAction = currentFullScreenAction(); late final ReaderSliderPosition _readerSliderPosition = currentReaderSliderPosition(); @override Widget build(BuildContext context) { return _ImageReaderContent( widget.struct, _pagerDirection, _pagerType, _fullScreenAction, _readerSliderPosition, ); } } // class _ImageReaderContent extends StatefulWidget { // 记录初始方向 final ReaderDirection pagerDirection; // 记录初始阅读器类型 final ReaderType pagerType; final FullScreenAction fullScreenAction; final ReaderSliderPosition readerSliderPosition; final ImageReaderStruct struct; const _ImageReaderContent(this.struct, this.pagerDirection, this.pagerType, this.fullScreenAction, this.readerSliderPosition); @override State createState() { switch (pagerType) { case ReaderType.WEB_TOON: return _WebToonReaderState(); case ReaderType.WEB_TOON_ZOOM: return _WebToonZoomReaderState(); case ReaderType.GALLERY: return _GalleryReaderState(); // case ReaderType.WEB_TOON_FREE_ZOOM: // return _ListViewReaderState(); case ReaderType.TWO_PAGE_GALLERY: return _TwoPageGalleryReaderState(); default: throw Exception("ERROR READER TYPE"); } } } abstract class _ImageReaderContentState extends State<_ImageReaderContent> { bool _sliderDragging = false; // 阅读器 Widget _buildViewer(); Widget _buildViewerProcess() { return Stack( children: [ processImageFilter(_buildViewer()), if (_sliderDragging) _sliderDraggingText(), ], ); } Widget _sliderDraggingText() { return Center( child: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: const Color(0x88000000), borderRadius: BorderRadius.circular(10), ), child: Text( "${_slider + 1} / ${widget.struct.images.length}", style: const TextStyle( color: Colors.white, fontSize: 30, ), ), ), ); } // 键盘, 音量键 等事件 void _needJumpTo(int index, bool animation); void _needScrollForward(); void _needScrollBackward(); double? get _remainingScrollHeight => null; // 记录了是否切换了音量 late bool _listVolume; // 屏幕上的多个块的信息 List _currentPositions = []; void _onPositionsChange(List positions) { if (positions.isEmpty) return; _currentPositions = positions; var first = positions.reduce((a, b) => a.index < b.index ? a : b); _onCurrentChange(first.index); } void _commonWebToonScrollForward( Function(double offset) animateScroll, Function(int index) jumpTo, ) { if (currentWebToonScrollMode() == WebToonScrollMode.SCREEN) { double s; if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) { s = MediaQuery.of(context).size.height; } else { s = MediaQuery.of(context).size.width; } var scrollSize = s * readerScrollByScreenPercentage; animateScroll(scrollSize); } else { if (_currentPositions.isNotEmpty) { var min = _currentPositions.reduce((a, b) => a.index < b.index ? a : b); var max = _currentPositions.reduce((a, b) => a.index > b.index ? a : b); if (min.index != max.index) { // 多个图片 // 只要最后一个图片没有显示完全, 就把最后一个图片对齐 if (max.itemTrailingEdge > 1) { jumpTo(max.index); } else { jumpTo(max.index + 1); } } else { // 一个图片 jumpTo(max.index + 1); } } else { jumpTo(_current + 1); } } } void _commonWebToonScrollBackward( Function(double offset) animateScroll, Function(int index) jumpTo, ) { if (currentWebToonScrollMode() == WebToonScrollMode.SCREEN) { double s; if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) { s = MediaQuery.of(context).size.height; } else { s = MediaQuery.of(context).size.width; } var scrollSize = s * readerScrollByScreenPercentage; animateScroll(-scrollSize); } else { if (_currentPositions.isNotEmpty) { var min = _currentPositions.reduce((a, b) => a.index < b.index ? a : b); // 第一张图片没有显示的部分超过10像素则对齐, 否则要显示更上一张 var leading = min.itemLeadingEdge; var height = MediaQuery.of(context).size.height; if (leading < 0 && leading.abs() * height > 10) { jumpTo(min.index); } else { jumpTo(min.index - 1); } } else { jumpTo(_current - 1); } } } // 和初始化与翻页有关 @override void initState() { _initCurrent(); _readerControllerEvent.subscribe(_onPageControl); _listVolume = volumeController; if (_listVolume) { addVolumeListen(); } super.initState(); } @override void dispose() { _readerControllerEvent.unsubscribe(_onPageControl); if (_listVolume) { delVolumeListen(); } super.dispose(); } void _onPageControl(_ReaderControllerEventArgs? args) { if (args != null) { var event = args.key; switch (event) { case "UP": if (ReaderType.WEB_TOON == currentReaderType() || // ReaderType.WEB_TOON_FREE_ZOOM == currentReaderType() || (ReaderType.WEB_TOON_ZOOM == currentReaderType() && currentWebToonScrollMode() == WebToonScrollMode.SCREEN)) { _needScrollBackward(); break; } if (_current > 0) { _needJumpTo(_current - 1, true); } break; case "DOWN": if (currentAutoFullScreenOnForward() && !widget.struct.fullScreen) { widget.struct.onFullScreenChange(true); } if (ReaderType.WEB_TOON == currentReaderType() || // ReaderType.WEB_TOON_FREE_ZOOM == currentReaderType() || (ReaderType.WEB_TOON_ZOOM == currentReaderType() && currentWebToonScrollMode() == WebToonScrollMode.SCREEN)) { _needScrollForward(); if ((_remainingScrollHeight ?? 10000) < 100) { if (volumeNextChapter()) { final now = DateTime.now().millisecondsSinceEpoch; if (_noticeTime + 3000 > now) { if (_hasNextEp()) { _onNextAction(); } else { defaultToast(context, tr('components.image_reader.already_at_the_end')); } } else { _noticeTime = now; defaultToast(context, tr('components.image_reader.click_to_next_chapter')); } } } break; } int point = 1; if (ReaderType.TWO_PAGE_GALLERY == currentReaderType()) { point = 2; } if (_current < widget.struct.images.length - point) { _needJumpTo(_current + point, true); } else { if (volumeNextChapter()) { final now = DateTime.now().millisecondsSinceEpoch; if (_noticeTime + 3000 > now) { if (_hasNextEp()) { _onNextAction(); } else { defaultToast(context, tr('components.image_reader.already_at_the_end')); } } else { _noticeTime = now; defaultToast(context, tr('components.image_reader.click_to_next_chapter')); } } } break; } } } int _noticeTime = 0; late int _startIndex; late int _current; late int _slider; bool? _hasNextEpCache; void _initCurrent() { if (widget.struct.initPosition != null && widget.struct.images.length > widget.struct.initPosition!) { _startIndex = widget.struct.initPosition!; } else { _startIndex = 0; } _current = _startIndex; _slider = _startIndex; } void _onCurrentChange(int index) { if (index != _current) { setState(() { _current = index; _slider = index; widget.struct.onPositionChange(index); }); } } // 与显示有关的方法 @override Widget build(BuildContext context) { switch (currentFullScreenAction()) { // 按钮 case FullScreenAction.CONTROLLER: return Stack( children: [ _buildViewerProcess(), _buildBar(_buildFullScreenControllerStackItem()), ], ); case FullScreenAction.TOUCH_ONCE: return Stack( children: [ _buildTouchOnceControllerAction(_buildViewerProcess()), _buildBar(Container()), ], ); case FullScreenAction.TOUCH_DOUBLE: return Stack( children: [ _buildTouchDoubleControllerAction(_buildViewerProcess()), _buildBar(Container()), ], ); case FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT: return Stack( children: [ _buildTouchDoubleOnceNextControllerAction(_buildViewerProcess()), _buildBar(Container()), ], ); case FullScreenAction.THREE_AREA: return Stack( children: [ _buildViewerProcess(), _buildBar(_buildThreeAreaControllerAction()), ], ); } } Widget _buildBar(Widget child) { switch (widget.readerSliderPosition) { case ReaderSliderPosition.BOTTOM: return Column( children: [ _buildAppBar(), Expanded(child: child), widget.struct.fullScreen ? Container() : Container( height: 45, color: const Color(0x88000000), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Container(width: 15), IconButton( icon: const Icon(Icons.fullscreen), color: Colors.white, onPressed: () { widget.struct .onFullScreenChange(!widget.struct.fullScreen); }, ), Container(width: 10), Expanded( child: //widget.pagerType != ReaderType.WEB_TOON_FREE_ZOOM // ? _buildSliderBottom() // : Container(), ), Container(width: 10), IconButton( icon: const Icon(Icons.skip_next_outlined), color: Colors.white, onPressed: _onNextAction, ), Container(width: 15), ], ), ), widget.struct.fullScreen ? Container() : Container( color: const Color(0x88000000), child: SafeArea( top: false, child: Container(), ), ), ], ); case ReaderSliderPosition.RIGHT: return Column( children: [ _buildAppBar(), Expanded( child: Stack( children: [ child, _buildSliderRight(), ], ), ), ], ); case ReaderSliderPosition.LEFT: return Column( children: [ _buildAppBar(), Expanded( child: Stack( children: [ child, _buildSliderLeft(), ], ), ), ], ); } } Widget _buildAppBar() => widget.struct.fullScreen ? Container() : AppBar( title: Text( "${widget.struct.epNameMap[widget.struct.epOrder] ?? ""} - ${widget.struct.comicTitle}"), actions: [ IconButton( onPressed: _onChooseEp, icon: const Icon(Icons.menu_open), ), IconButton( onPressed: _onMoreSetting, icon: const Icon(Icons.more_horiz), ), ], ); Widget _buildSliderBottom() { return Column( children: [ Expanded(child: Container()), SizedBox( height: 25, child: _buildSliderWidget(Axis.horizontal), ), Expanded(child: Container()), ], ); } Widget _buildSliderLeft() => widget.struct.fullScreen ? Container() : Align( alignment: Alignment.centerLeft, child: Material( color: Colors.transparent, child: Container( width: 35, height: 300, decoration: const BoxDecoration( color: Color(0x66000000), borderRadius: BorderRadius.only( topRight: Radius.circular(10), bottomRight: Radius.circular(10), ), ), padding: const EdgeInsets.only(top: 10, bottom: 10, left: 6, right: 5), child: Center( child: _buildSliderWidget(Axis.vertical), ), ), ), ); Widget _buildSliderRight() => widget.struct.fullScreen ? Container() : Align( alignment: Alignment.centerRight, child: Material( color: Colors.transparent, child: Container( width: 35, height: 300, decoration: const BoxDecoration( color: Color(0x66000000), borderRadius: BorderRadius.only( topLeft: Radius.circular(10), bottomLeft: Radius.circular(10), ), ), padding: const EdgeInsets.only(top: 10, bottom: 10, left: 5, right: 6), child: Center( child: _buildSliderWidget(Axis.vertical), ), ), ), ); Widget _buildSliderWidget(Axis axis) { return FlutterSlider( axis: axis, values: [_slider.toDouble()], min: 0, max: (widget.struct.images.length - 1).toDouble(), onDragStarted: (handlerIndex, lowerValue, upperValue) { setState(() { _sliderDragging = true; }); }, onDragging: (handlerIndex, lowerValue, upperValue) { setState(() { _slider = (lowerValue.toInt()); }); }, onDragCompleted: (handlerIndex, lowerValue, upperValue) { setState(() { _sliderDragging = false; }); _slider = (lowerValue.toInt()); if (_slider != _current) { _needJumpTo(_slider, false); } }, trackBar: FlutterSliderTrackBar( inactiveTrackBar: BoxDecoration( borderRadius: BorderRadius.circular(20), color: Colors.grey.shade300, ), activeTrackBar: BoxDecoration( borderRadius: BorderRadius.circular(4), color: Theme.of(context).colorScheme.secondary, ), ), step: const FlutterSliderStep( step: 1, isPercentRange: false, ), tooltip: FlutterSliderTooltip(disabled: true), ); } Widget _buildFullScreenControllerStackItem() { if (widget.readerSliderPosition == ReaderSliderPosition.BOTTOM && !widget.struct.fullScreen) { return Container(); } if (widget.readerSliderPosition == ReaderSliderPosition.RIGHT) { return SafeArea( child: Align( alignment: Alignment.bottomRight, child: Material( color: Colors.transparent, child: Container( padding: const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4), margin: const EdgeInsets.only(bottom: 10), decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(10), bottomLeft: Radius.circular(10), ), color: Color(0x88000000), ), child: GestureDetector( onTap: () { widget.struct.onFullScreenChange(!widget.struct.fullScreen); }, child: Icon( widget.struct.fullScreen ? Icons.fullscreen_exit : Icons.fullscreen_outlined, size: 30, color: Colors.white, ), ), ), ), )); } return SafeArea( child: Align( alignment: Alignment.bottomLeft, child: Material( color: Colors.transparent, child: Container( padding: const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4), margin: const EdgeInsets.only(bottom: 10), decoration: const BoxDecoration( borderRadius: BorderRadius.only( topRight: Radius.circular(10), bottomRight: Radius.circular(10), ), color: Color(0x88000000), ), child: GestureDetector( onTap: () { widget.struct.onFullScreenChange(!widget.struct.fullScreen); }, child: Icon( widget.struct.fullScreen ? Icons.fullscreen_exit : Icons.fullscreen_outlined, size: 30, color: Colors.white, ), ), ), ), )); } Widget _buildTouchOnceControllerAction(Widget child) { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { widget.struct.onFullScreenChange(!widget.struct.fullScreen); }, child: child, ); } Widget _buildTouchDoubleControllerAction(Widget child) { return GestureDetector( behavior: HitTestBehavior.translucent, onDoubleTap: () { widget.struct.onFullScreenChange(!widget.struct.fullScreen); }, child: child, ); } Widget _buildTouchDoubleOnceNextControllerAction(Widget child) { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { _readerControllerEvent.broadcast(_ReaderControllerEventArgs("DOWN")); }, onDoubleTap: () { widget.struct.onFullScreenChange(!widget.struct.fullScreen); }, child: child, ); } Widget _buildThreeAreaControllerAction() { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var up = Expanded( child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { _readerControllerEvent .broadcast(_ReaderControllerEventArgs("UP")); }, child: Container(), ), ); var down = Expanded( child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { _readerControllerEvent .broadcast(_ReaderControllerEventArgs("DOWN")); }, child: Container(), ), ); var fullScreen = Expanded( child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => widget.struct.onFullScreenChange(!widget.struct.fullScreen), child: Container(), ), ); late Widget child; switch (widget.pagerDirection) { case ReaderDirection.TOP_TO_BOTTOM: child = Column(children: [ up, fullScreen, down, ]); break; case ReaderDirection.LEFT_TO_RIGHT: child = Row(children: [ up, fullScreen, down, ]); break; case ReaderDirection.RIGHT_TO_LEFT: if (threeKeepRight) { child = Row(children: [ up, fullScreen, down, ]); break; } child = Row(children: [ down, fullScreen, up, ]); break; } return SizedBox( width: constraints.maxWidth, height: constraints.maxHeight, child: child, ); }, ); } Future _onChooseEp() async { showModalBottomSheet( context: context, backgroundColor: const Color(0xAA000000), isScrollControlled: true, builder: (context) { return SizedBox( height: MediaQuery.of(context).size.height * (.45), child: _EpChooser( widget.struct.epNameMap, widget.struct.epOrder, widget.struct.onChangeEp, ), ); }, ); } Future _onMoreSetting() async { // 记录开始的画质 final currentQuality = currentQualityCode(); final cReaderSliderPosition = currentReaderSliderPosition(); // await showModalBottomSheet( context: context, backgroundColor: const Color(0xAA000000), isScrollControlled: true, builder: (context) { return SizedBox( height: MediaQuery.of(context).size.height * (.45), child: _SettingPanel( widget.struct.onReloadEp, widget.struct.onDownload, ), ); }, ); setState(() {}); if (widget.pagerDirection != gReaderDirection || widget.pagerType != currentReaderType() || currentQuality != currentQualityCode() || widget.fullScreenAction != currentFullScreenAction() || cReaderSliderPosition != currentReaderSliderPosition()) { widget.struct.onReloadEp(); } } // 给子类调用的方法 bool _fullscreenController() { switch (currentFullScreenAction()) { case FullScreenAction.CONTROLLER: return false; case FullScreenAction.TOUCH_ONCE: return false; case FullScreenAction.TOUCH_DOUBLE: return false; case FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT: return false; case FullScreenAction.THREE_AREA: return true; } } Future _onNextAction() async { if (_hasNextEp()) { widget.struct.onChangeEp(widget.struct.epOrder + 1); } else { defaultToast(context, tr('components.image_reader.already_at_the_end')); } } bool _hasNextEp() { if (_hasNextEpCache != null) { return _hasNextEpCache!; } for (var element in widget.struct.epNameMap.keys) { if (element > widget.struct.epOrder) { _hasNextEpCache = true; return true; } } _hasNextEpCache = false; return false; } double _topBarHeight() => Scaffold.of(context).appBarMaxHeight ?? 0; double _bottomBarHeight() => widget.readerSliderPosition == ReaderSliderPosition.BOTTOM ? 45 : 0; } class _EpChooser extends StatefulWidget { final Map epNameMap; final int epOrder; final FutureOr Function(int) onChangeEp; _EpChooser(this.epNameMap, this.epOrder, this.onChangeEp); @override State createState() => _EpChooserState(); } class _EpChooserState extends State<_EpChooser> { @override Widget build(BuildContext context) { var entries = widget.epNameMap.entries.toList(); entries.sort((a, b) => a.key - b.key); var widgets = [ Container(height: 20), ...entries.map((e) { return Container( margin: const EdgeInsets.only(left: 15, right: 15, top: 5, bottom: 5), decoration: BoxDecoration( color: widget.epOrder == e.key ? Colors.grey.withAlpha(100) : null, border: Border.all( color: const Color(0xff484c60), style: BorderStyle.solid, width: .5, ), ), child: MaterialButton( onPressed: () { Navigator.of(context).pop(); widget.onChangeEp(e.key); }, textColor: Colors.white, child: Text(e.value), ), ); }) ]; return ScrollablePositionedList.builder( initialScrollIndex: widget.epOrder < 2 ? 0 : widget.epOrder - 2, itemCount: widgets.length, itemBuilder: (BuildContext context, int index) => widgets[index], ); } } class _SettingPanel extends StatefulWidget { final FutureOr Function() onReloadEp; final FutureOr Function() onDownload; _SettingPanel(this.onReloadEp, this.onDownload); @override State createState() => _SettingPanelState(); } class _SettingPanelState extends State<_SettingPanel> { @override Widget build(BuildContext context) { return ListView( padding: const EdgeInsets.all(15), children: [ _chooseTile( icon: Icons.crop_sharp, title: tr("settings.reader_direction.title"), value: gReaderDirectionName(), onTap: () async { await choosePagerDirection(context); setState(() {}); }, ), _chooseTile( icon: Icons.view_day_outlined, title: tr("settings.reader_type.title"), value: currentReaderTypeName(), onTap: () async { await choosePagerType(context); setState(() {}); }, ), _chooseTile( icon: Icons.image_aspect_ratio_outlined, title: tr("settings.quality.title"), value: currentQualityName(), onTap: () async { await chooseQuality(context); setState(() {}); }, ), _chooseTile( icon: Icons.control_camera_outlined, title: tr("settings.full_screen_action.title"), value: currentFullScreenActionName(), onTap: () async { await chooseFullScreenAction(context); setState(() {}); }, ), const Divider(color: Colors.white24), _chooseTile( icon: Icons.swap_vert, title: tr("settings.web_toon_scroll_mode.title"), value: currentWebToonScrollModeName(), onTap: () async { await chooseWebToonScrollMode(context); setState(() {}); }, ), _sliderTile( icon: Icons.straighten, title: tr("settings.reader_scroll_by_screen_percentage.title"), valueLabel: "${currentReaderScrollByScreenPercentage()}%${tr("settings.reader_scroll_by_screen_percentage.screen_size")}", min: 5, max: 110, divisions: 110 - 5, value: currentReaderScrollByScreenPercentage().toDouble(), onChanged: (v) async { await setReaderScrollByScreenPercentage(v.toInt()); setState(() {}); }, ), const Divider(color: Colors.white24), _sliderTile( icon: Icons.zoom_out_map, title: tr("settings.reader_zoom.out_title"), valueLabel: "${readerZoomMinScale.toStringAsFixed(1)}x", min: 0.1, max: 1.0, divisions: 9, value: readerZoomMinScale.clamp(0.1, 1.0).toDouble(), onChanged: (v) async { final newValue = (v * 10).roundToDouble() / 10; await setReaderZoomMinScale(newValue); setState(() {}); }, ), _sliderTile( icon: Icons.zoom_in, title: tr("settings.reader_zoom.in_title"), valueLabel: "${readerZoomMaxScale.toStringAsFixed(1)}x", min: 1.0, max: 30.0, divisions: 29, value: readerZoomMaxScale.clamp(1.0, 30.0).toDouble(), onChanged: (v) async { final newValue = v.roundToDouble(); await setReaderZoomMaxScale(newValue); setState(() {}); }, ), _sliderTile( icon: Icons.touch_app, title: tr("settings.reader_zoom.double_tap_title"), valueLabel: "${readerZoomDoubleTapScale.toStringAsFixed(1)}x", min: 1.5, max: 5.0, divisions: 7, value: readerZoomDoubleTapScale.clamp(1.5, 5.0).toDouble(), onChanged: (v) async { final newValue = (v * 2).roundToDouble() / 2; await setReaderZoomDoubleTapScale(newValue); setState(() {}); }, ), _switchTile( icon: Icons.border_inner, title: tr('settings.drag_region_lock.title'), value: dragRegionLock(), onChanged: (v) async { await setDragRegionLock(v); setState(() {}); }, ), _sliderTile( icon: Icons.speed, title: tr('settings.gesture_speed.title'), valueLabel: "${currentGestureSpeed().toStringAsFixed(1)}x", min: 0.1, max: 5.0, divisions: 49, value: currentGestureSpeed(), onChanged: (v) async { await setGestureSpeed(v); setState(() {}); }, ), _switchTile( icon: Icons.arrow_downward, title: tr("settings.auto_full_screen_on_forward.title"), value: currentAutoFullScreenOnForward(), onChanged: (v) async { await setAutoFullScreenOnForward(v); setState(() {}); }, ), const Divider(color: Colors.white24), _chooseTile( icon: Icons.share, title: tr('net.address'), value: currentAddressName(), onTap: () async { await chooseAddressAndSwitch(context); setState(() {}); }, ), _chooseTile( icon: Icons.image_search, title: tr('settings.image_address.title'), value: currentImageAddressName(), onTap: () async { await chooseImageAddress(context); setState(() {}); }, ), _chooseTile( icon: Icons.network_ping, title: tr('net.use_api_load_image'), value: currentUseApiLoadImageName(), onTap: () async { await chooseUseApiLoadImage(context); setState(() {}); }, ), const Divider(color: Colors.white24), _switchTile( icon: Icons.access_time_filled_outlined, title: tr('settings.no_animation.title'), value: noAnimation(), onChanged: (v) async { await setNoAnimation(v); setState(() {}); }, ), _actionTile( icon: Icons.refresh, title: tr('components.image_reader.reload_page'), onTap: () { Navigator.of(context).pop(); widget.onReloadEp(); }, ), ], ); } Widget _chooseTile({ required IconData icon, required String title, required String value, required FutureOr Function() onTap, }) { return ListTile( leading: Icon(icon, color: Colors.white), title: Text(title, style: const TextStyle(color: Colors.white)), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ ConstrainedBox( constraints: const BoxConstraints(maxWidth: 100), child: Text( value, textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Colors.white70), ), ), const SizedBox(width: 4), const Icon(Icons.chevron_right, color: Colors.white70), ], ), onTap: () async => await onTap(), ); } Widget _actionTile({ required IconData icon, required String title, required VoidCallback onTap, }) { return ListTile( leading: Icon(icon, color: Colors.white), title: Text(title, style: const TextStyle(color: Colors.white)), trailing: const Icon(Icons.chevron_right, color: Colors.white70), onTap: onTap, ); } Widget _switchTile({ required IconData icon, required String title, required bool value, required FutureOr Function(bool) onChanged, }) { return ListTile( leading: Icon(icon, color: Colors.white), title: Text(title, style: const TextStyle(color: Colors.white)), trailing: Switch( value: value, onChanged: (v) async => await onChanged(v), activeColor: Theme.of(context).colorScheme.secondary, ), ); } Widget _sliderTile({ required IconData icon, required String title, required String valueLabel, required double min, required double max, required int divisions, required double value, required FutureOr Function(double) onChanged, }) { return ListTile( leading: Icon(icon, color: Colors.white), title: Text(title, style: const TextStyle(color: Colors.white)), subtitle: Row( children: [ Expanded( child: Slider( min: min, max: max, divisions: divisions, value: value, onChanged: (v) async => await onChanged(v), ), ), SizedBox( width: 50, child: Text( valueLabel, style: const TextStyle(color: Colors.white70, fontSize: 13), textAlign: TextAlign.end, ), ), ], ), ); } } /////////////////////////////////////////////////////////////////////////////// class _WebToonReaderState extends _ImageReaderContentState { var _controllerTime = DateTime.now().millisecondsSinceEpoch + 400; late final List _trueSizes = []; late final zoomable.ItemScrollController _itemScrollController; late final zoomable.ItemPositionsListener _itemPositionsListener; late final zoomable.ScrollOffsetController _scrollOffsetController; late final zoomable.ScrollOffsetListener _scrollOffsetListener; StreamSubscription? _scrollOffsetSubscription; @override void initState() { for (var e in widget.struct.images) { if (e.pkzFile != null && e.width != null && e.height != null && e.width! > 0 && e.height! > 0) { _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); } else if (e.downloadLocalPath != null) { _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); } else { _trueSizes.add(null); } } _itemScrollController = zoomable.ItemScrollController(); _itemPositionsListener = zoomable.ItemPositionsListener.create(); _itemPositionsListener.itemPositions.addListener(_onListCurrentChange); _scrollOffsetController = zoomable.ScrollOffsetController(); _scrollOffsetListener = zoomable.ScrollOffsetListener.create(); _scrollOffsetSubscription = _scrollOffsetListener.changes.listen(_onScroll); super.initState(); } void _onScroll(double delta) { if (currentAutoFullScreenOnForward() && !widget.struct.fullScreen) { if (delta > 0) { widget.struct.onFullScreenChange(true); } } } @override double? get _remainingScrollHeight { try { return _scrollOffsetController.maxScrollExtent - _scrollOffsetController.offset; } catch (e) { return null; } } @override void dispose() { _itemPositionsListener.itemPositions.removeListener(_onListCurrentChange); _scrollOffsetSubscription?.cancel(); super.dispose(); } void _onListCurrentChange() { var positions = _itemPositionsListener.itemPositions.value; if (positions.isNotEmpty) { super._onPositionsChange(positions .map((e) => ImageReaderItemPosition( e.index, e.itemLeadingEdge, e.itemTrailingEdge)) .toList()); } } @override void _needJumpTo(int index, bool animation) { if (noAnimation() || animation == false) { _itemScrollController.jumpTo( index: index, ); } else { if (DateTime.now().millisecondsSinceEpoch < _controllerTime) { return; } _controllerTime = DateTime.now().millisecondsSinceEpoch + 400; _itemScrollController.scrollTo( index: index, duration: const Duration(milliseconds: 400), ); } } @override void _needScrollForward() { _commonWebToonScrollForward( (offset) { _scrollOffsetController.animateScroll( offset: offset, duration: noAnimation() ? Duration.zero : const Duration(milliseconds: 200), curve: Curves.easeOut, ); }, (index) { _needJumpTo(index, true); }, ); } @override void _needScrollBackward() { _commonWebToonScrollBackward( (offset) { _scrollOffsetController.animateScroll( offset: offset, duration: noAnimation() ? Duration.zero : const Duration(milliseconds: 200), curve: Curves.easeOut, ); }, (index) { _needJumpTo(index, true); }, ); } @override Widget _buildViewer() { return Container( decoration: BoxDecoration( color: readerBackgroundColorObj, ), child: _buildList(), ); } Widget _buildList() { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { // reload _images size List _images = []; for (var index = 0; index < widget.struct.images.length; index++) { late Size renderSize; if (_trueSizes[index] != null) { if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) { renderSize = Size( constraints.maxWidth, constraints.maxWidth * _trueSizes[index]!.height / _trueSizes[index]!.width, ); } else { var maxHeight = constraints.maxHeight - super._topBarHeight() - super._bottomBarHeight() - MediaQuery.of(context).padding.bottom; renderSize = Size( maxHeight * _trueSizes[index]!.width / _trueSizes[index]!.height, maxHeight, ); } } else { if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) { renderSize = Size(constraints.maxWidth, constraints.maxWidth / 2); } else { // ReaderDirection.LEFT_TO_RIGHT // ReaderDirection.RIGHT_TO_LEFT renderSize = Size(constraints.maxWidth / 2, constraints.maxHeight); } } var currentIndex = index; onTrueSize(Size size) { setState(() { _trueSizes[currentIndex] = size; }); } var e = widget.struct.images[index]; if (e.pkzFile != null) { _images.add(_WebToonPkzImage( width: e.width!, height: e.height!, format: e.format!, size: renderSize, onTrueSize: onTrueSize, pkzFile: e.pkzFile!, )); } else if (e.downloadLocalPath != null) { _images.add(_WebToonDownloadImage( fileServer: e.fileServer, path: e.path, localPath: e.downloadLocalPath!, fileSize: e.fileSize!, width: e.width!, height: e.height!, format: e.format!, size: renderSize, onTrueSize: onTrueSize, )); } else { _images.add(_WebToonRemoteImage( e.fileServer, e.path, renderSize, onTrueSize, )); } } return zoomable.ZoomablePositionedList.builder( enableZoom: false, scrollOffsetController: _scrollOffsetController, scrollOffsetListener: _scrollOffsetListener, initialScrollIndex: super._startIndex, scrollDirection: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? Axis.vertical : Axis.horizontal, reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT, padding: EdgeInsets.only( // 不管全屏与否, 滚动方向如何, 顶部永远保持间距 top: super._topBarHeight(), bottom: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? 130 // 纵向滚动 底部永远都是130的空白 : (super._bottomBarHeight() + MediaQuery.of(context).padding.bottom) // 非全屏时, 顶部去掉顶部BAR的高度, 底部去掉底部BAR的高度, 形成看似填充的效果 , ), itemScrollController: _itemScrollController, itemPositionsListener: _itemPositionsListener, itemCount: widget.struct.images.length + 1, itemBuilder: (BuildContext context, int index) { if (index >= widget.struct.images.length) { return _buildNextEp(); } return _images[index]; }, ); }, ); } Widget _buildNextEp() { if (super._fullscreenController()) { return Container(); } return Container( color: Colors.transparent, padding: const EdgeInsets.all(20), child: MaterialButton( onPressed: () { if (super._hasNextEp()) { super._onNextAction(); } else { Navigator.of(context).pop(); } }, textColor: invertColor(readerBackgroundColorObj), child: Container( padding: const EdgeInsets.only(top: 40, bottom: 40), child: Text(super._hasNextEp() ? tr('components.image_reader.next_chapter') : tr('components.image_reader.end_reading')), ), ), ); } } // 来自下载 class _WebToonDownloadImage extends _WebToonReaderImage { final String fileServer; final String path; final String localPath; final int fileSize; final int width; final int height; final String format; _WebToonDownloadImage({ required this.fileServer, required this.path, required this.localPath, required this.fileSize, required this.width, required this.height, required this.format, required Size size, Function(Size)? onTrueSize, }) : super(size, onTrueSize); @override Future imageData() async { if (localPath == "") { return method.remoteImageData(fileServer, path); } var finalPath = await method.downloadImagePath(localPath); return RemoteImageData.forData( fileSize, format, width, height, finalPath, ); } } // 来自PKZ class _WebToonPkzImage extends StatelessWidget { final PkzFile pkzFile; final int width; final int height; final String format; final Size size; Function(Size)? onTrueSize; _WebToonPkzImage({ required this.pkzFile, required this.width, required this.height, required this.format, required this.size, required this.onTrueSize, }); @override Widget build(BuildContext context) { return PkzLoadingImage( pkzPath: pkzFile.pkzPath, path: pkzFile.path, width: size.width, height: size.height, onTrueSize: onTrueSize, ); } } // 来自远端 class _WebToonRemoteImage extends _WebToonReaderImage { final String fileServer; final String path; _WebToonRemoteImage( this.fileServer, this.path, Size size, Function(Size)? onTrueSize, ) : super(size, onTrueSize); @override Future imageData() async { return method.remoteImageData(fileServer, path); } } // 通用 abstract class _WebToonReaderImage extends StatefulWidget { final Size size; final Function(Size)? onTrueSize; _WebToonReaderImage(this.size, this.onTrueSize); @override State createState() => _WebToonReaderImageState(); Future imageData(); } class _WebToonReaderImageState extends State<_WebToonReaderImage> { late Future _future = _load(); Future _load() { return widget.imageData().then((value) { widget.onTrueSize?.call( Size(value.width.toDouble(), value.height.toDouble()), ); return value; }); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return FutureBuilder( future: _future, builder: ( BuildContext context, AsyncSnapshot snapshot, ) { if (snapshot.hasError) { return GestureDetector( onLongPress: () async { String? choose = await chooseListDialog(context, tr('app.please_choose'), [tr('components.image_reader.reload_image')]); if (choose == null) { return; } if (choose == tr('components.image_reader.reload_image')) { setState(() { _future = _load(); }); } }, child: buildError(widget.size.width, widget.size.height), ); } if (snapshot.connectionState != ConnectionState.done) { return buildLoading(widget.size.width, widget.size.height); } var data = snapshot.data!; return buildFile( data.finalPath, widget.size.width, widget.size.height, context: context, ); }, ); }, ); } } /////////////////////////////////////////////////////////////////////////////// class _WebToonZoomReaderState extends _ImageReaderContentState { var _controllerTime = DateTime.now().millisecondsSinceEpoch + 400; late final List _trueSizes = []; late final zoomable.ItemScrollController _itemScrollController; late final zoomable.ScrollOffsetController _scrollOffsetController; late final zoomable.ItemPositionsListener _itemPositionsListener; late final zoomable.ScrollOffsetListener _scrollOffsetListener; StreamSubscription? _scrollOffsetSubscription; @override void initState() { for (var e in widget.struct.images) { if (e.pkzFile != null && e.width != null && e.height != null && e.width! > 0 && e.height! > 0) { _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); } else if (e.downloadLocalPath != null) { _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); } else { _trueSizes.add(null); } } _itemScrollController = zoomable.ItemScrollController(); _scrollOffsetController = zoomable.ScrollOffsetController(); _itemPositionsListener = zoomable.ItemPositionsListener.create(); _itemPositionsListener.itemPositions.addListener(_onListCurrentChange); _scrollOffsetListener = zoomable.ScrollOffsetListener.create(); _scrollOffsetSubscription = _scrollOffsetListener.changes.listen(_onScroll); super.initState(); } void _onScroll(double delta) { if (currentAutoFullScreenOnForward() && !widget.struct.fullScreen) { if (delta > 0) { widget.struct.onFullScreenChange(true); } } } @override void dispose() { _itemPositionsListener.itemPositions.removeListener(_onListCurrentChange); _scrollOffsetSubscription?.cancel(); super.dispose(); } void _onListCurrentChange() { var positions = _itemPositionsListener.itemPositions.value; if (positions.isNotEmpty) { super._onPositionsChange(positions .map((e) => ImageReaderItemPosition( e.index, e.itemLeadingEdge, e.itemTrailingEdge)) .toList()); } } @override void _needJumpTo(int index, bool animation) { if (noAnimation() || animation == false) { _itemScrollController.jumpTo( index: index, ); } else { if (DateTime.now().millisecondsSinceEpoch < _controllerTime) { return; } _controllerTime = DateTime.now().millisecondsSinceEpoch + 400; _itemScrollController.scrollTo( index: index, duration: const Duration(milliseconds: 400), ); } } @override void _needScrollForward() { _commonWebToonScrollForward( (offset) { _scrollOffsetController.animateScroll( offset: offset, duration: noAnimation() ? Duration.zero : const Duration(milliseconds: 200), curve: Curves.easeOut, ); }, (index) { _needJumpTo(index, true); }, ); } @override void _needScrollBackward() { _commonWebToonScrollBackward( (offset) { _scrollOffsetController.animateScroll( offset: offset, duration: noAnimation() ? Duration.zero : const Duration(milliseconds: 200), curve: Curves.easeOut, ); }, (index) { _needJumpTo(index, true); }, ); } @override double? get _remainingScrollHeight { try { return _scrollOffsetController.maxScrollExtent - _scrollOffsetController.offset; } catch (e) { return null; } } @override Widget _buildViewer() { return Container( decoration: BoxDecoration( color: readerBackgroundColorObj, ), child: _buildList(), ); } Widget _buildList() { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { List _images = []; for (var index = 0; index < widget.struct.images.length; index++) { late Size renderSize; if (_trueSizes[index] != null) { if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) { renderSize = Size( constraints.maxWidth, constraints.maxWidth * _trueSizes[index]!.height / _trueSizes[index]!.width, ); } else { var maxHeight = constraints.maxHeight - super._topBarHeight() - super._bottomBarHeight() - MediaQuery.of(context).padding.bottom; renderSize = Size( maxHeight * _trueSizes[index]!.width / _trueSizes[index]!.height, maxHeight, ); } } else { if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) { renderSize = Size(constraints.maxWidth, constraints.maxWidth / 2); } else { renderSize = Size(constraints.maxWidth / 2, constraints.maxHeight); } } var currentIndex = index; onTrueSize(Size size) { setState(() { _trueSizes[currentIndex] = size; }); } var e = widget.struct.images[index]; if (e.pkzFile != null) { _images.add(_WebToonPkzImage( width: e.width!, height: e.height!, format: e.format!, size: renderSize, onTrueSize: onTrueSize, pkzFile: e.pkzFile!, )); } else if (e.downloadLocalPath != null) { _images.add(_WebToonDownloadImage( fileServer: e.fileServer, path: e.path, localPath: e.downloadLocalPath!, fileSize: e.fileSize!, width: e.width!, height: e.height!, format: e.format!, size: renderSize, onTrueSize: onTrueSize, )); } else { _images.add(_WebToonRemoteImage( e.fileServer, e.path, renderSize, onTrueSize, )); } } return zoomable.ZoomablePositionedList.builder( gestureSpeed: currentGestureSpeed(), dragRegionLock: dragRegionLock(), scrollOffsetListener: _scrollOffsetListener, minScale: readerZoomMinScale, maxScale: readerZoomMaxScale, doubleTapScale: readerZoomDoubleTapScale, doubleTapAnimationDuration: noAnimation() ? Duration.zero : const Duration(milliseconds: 200), enableDoubleTapZoom: widget.fullScreenAction != FullScreenAction.TOUCH_DOUBLE && widget.fullScreenAction != FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT, initialScrollIndex: super._startIndex, scrollDirection: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? Axis.vertical : Axis.horizontal, reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT, padding: EdgeInsets.only( top: super._topBarHeight(), bottom: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? 130 : (super._bottomBarHeight() + MediaQuery.of(context).padding.bottom), ), itemScrollController: _itemScrollController, scrollOffsetController: _scrollOffsetController, itemPositionsListener: _itemPositionsListener, itemCount: widget.struct.images.length + 1, itemBuilder: (BuildContext context, int index) { if (index >= widget.struct.images.length) { return _buildNextEp(); } return _images[index]; }, ); }, ); } Widget _buildNextEp() { if (super._fullscreenController()) { return Container(); } return Container( color: Colors.transparent, padding: const EdgeInsets.all(20), child: MaterialButton( onPressed: () { if (super._hasNextEp()) { super._onNextAction(); } else { Navigator.of(context).pop(); } }, textColor: invertColor(readerBackgroundColorObj), child: Container( padding: const EdgeInsets.only(top: 40, bottom: 40), child: Text(super._hasNextEp() ? tr('components.image_reader.next_chapter') : tr('components.image_reader.end_reading')), ), ), ); } } /////////////////////////////////////////////////////////////////////////////// class _ListViewReaderState extends _ImageReaderContentState with SingleTickerProviderStateMixin { final List _trueSizes = []; final _transformationController = TransformationController(); late TapDownDetails _doubleTapDetails; late final _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 100), ); late final _scrollController = ScrollController(); double _lastScrollOffset = 0; @override void initState() { _scrollController.addListener(_onScroll); for (var e in widget.struct.images) { if (e.pkzFile != null && e.width != null && e.height != null && e.width! > 0 && e.height! > 0) { _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); } else if (e.downloadLocalPath != null) { _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); } else { _trueSizes.add(null); } } super.initState(); } void _onScroll() { var offset = _scrollController.offset; if (currentAutoFullScreenOnForward() && !widget.struct.fullScreen) { if (offset > _lastScrollOffset) { widget.struct.onFullScreenChange(true); } } _lastScrollOffset = offset; } @override void dispose() { _transformationController.dispose(); _animationController.dispose(); _scrollController.dispose(); super.dispose(); } @override void _needJumpTo(int index, bool animation) {} int _controllerTime = 0; @override void _needScrollForward() { var first = _scrollController.offset; double s; if (gReaderDirection == ReaderDirection.TOP_TO_BOTTOM) { s = MediaQuery.of(context).size.height; } else { s = MediaQuery.of(context).size.width; } var scrollSize = s * readerScrollByScreenPercentage; var pos = first + scrollSize; if (pos > _scrollController.position.maxScrollExtent) { pos = _scrollController.position.maxScrollExtent; } if (noAnimation()) { _scrollController.jumpTo(pos); } else { if (DateTime.now().millisecondsSinceEpoch < _controllerTime) { return; } _controllerTime = DateTime.now().millisecondsSinceEpoch + 400; _scrollController.animateTo( pos, duration: const Duration(milliseconds: 400), curve: Curves.ease, ); } } @override void _needScrollBackward() {} @override double? get _remainingScrollHeight { if (_scrollController.hasClients) { return _scrollController.position.maxScrollExtent - _scrollController.offset; } return null; } @override Widget _buildViewer() { return Container( decoration: BoxDecoration( color: readerBackgroundColorObj, ), child: _buildList(), ); } Widget _buildList() { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { // reload _images size List _images = []; for (var index = 0; index < widget.struct.images.length; index++) { late Size renderSize; if (_trueSizes[index] != null) { if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) { renderSize = Size( constraints.maxWidth, constraints.maxWidth * _trueSizes[index]!.height / _trueSizes[index]!.width, ); } else { var maxHeight = constraints.maxHeight - super._topBarHeight() - super._bottomBarHeight() - MediaQuery.of(context).padding.bottom; renderSize = Size( maxHeight * _trueSizes[index]!.width / _trueSizes[index]!.height, maxHeight, ); } } else { if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) { renderSize = Size(constraints.maxWidth, constraints.maxWidth / 2); } else { // ReaderDirection.LEFT_TO_RIGHT // ReaderDirection.RIGHT_TO_LEFT renderSize = Size(constraints.maxWidth / 2, constraints.maxHeight); } } var currentIndex = index; onTrueSize(Size size) { setState(() { _trueSizes[currentIndex] = size; }); } var e = widget.struct.images[index]; if (e.pkzFile != null) { _images.add(_WebToonPkzImage( width: e.width!, height: e.height!, format: e.format!, size: renderSize, onTrueSize: onTrueSize, pkzFile: e.pkzFile!, )); } else if (e.downloadLocalPath != null) { _images.add(_WebToonDownloadImage( fileServer: e.fileServer, path: e.path, localPath: e.downloadLocalPath!, fileSize: e.fileSize!, width: e.width!, height: e.height!, format: e.format!, size: renderSize, onTrueSize: onTrueSize, )); } else { _images.add(_WebToonRemoteImage( e.fileServer, e.path, renderSize, onTrueSize, )); } } var list = ListView.builder( controller: _scrollController, scrollDirection: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? Axis.vertical : Axis.horizontal, reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT, padding: EdgeInsets.only( // 不管全屏与否, 滚动方向如何, 顶部永远保持间距 top: super._topBarHeight(), bottom: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? 130 // 纵向滚动 底部永远都是130的空白 : (super._bottomBarHeight() + MediaQuery.of(context).padding.bottom) // 非全屏时, 顶部去掉顶部BAR的高度, 底部去掉底部BAR的高度, 形成看似填充的效果 , ), itemCount: widget.struct.images.length + 1, itemBuilder: (BuildContext context, int index) { if (widget.struct.images.length == index) { return _buildNextEp(); } return _images[index]; }, ); var viewer = InteractiveViewer( transformationController: _transformationController, minScale: readerZoomMinScale, maxScale: readerZoomMaxScale, child: list, ); if (FullScreenAction.TOUCH_DOUBLE == currentFullScreenAction() || FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT == currentFullScreenAction()) { return viewer; } return GestureDetector( onDoubleTap: _handleDoubleTap, onDoubleTapDown: _handleDoubleTapDown, child: viewer, ); }, ); } Widget _buildNextEp() { if (super._fullscreenController()) { return Container(); } return Container( padding: const EdgeInsets.all(20), child: MaterialButton( onPressed: () { if (super._hasNextEp()) { super._onNextAction(); } else { Navigator.of(context).pop(); } }, textColor: invertColor(readerBackgroundColorObj), child: Container( padding: const EdgeInsets.only(top: 40, bottom: 40), child: Text(super._hasNextEp() ? tr('components.image_reader.next_chapter') : tr('components.image_reader.end_reading')), ), ), ); } void _handleDoubleTapDown(TapDownDetails details) { _doubleTapDetails = details; } void _handleDoubleTap() { if (_animationController.isAnimating) { return; } if (_transformationController.value != Matrix4.identity()) { _transformationController.value = Matrix4.identity(); } else { var position = _doubleTapDetails.localPosition; final targetScale = readerZoomDoubleTapScale.clamp(1.0, readerZoomMaxScale); var animation = Tween(begin: 0.0, end: 1.0).animate(_animationController); animation.addListener(() { final scale = 1.0 + (targetScale - 1.0) * animation.value; _transformationController.value = Matrix4.identity() ..translate( -position.dx * (scale - 1.0), -position.dy * (scale - 1.0)) ..scale(scale); }); _animationController.forward(from: 0); } } } /////////////////////////////////////////////////////////////////////////////// class _GalleryReaderState extends _ImageReaderContentState { late PageController _pageController; List ips = []; List options = []; late Widget gallery; @override void initState() { super.initState(); // 需要先初始化 super._startIndex 才能使用, 所以在上面 _pageController = PageController(initialPage: super._startIndex); for (var index = 0; index < widget.struct.images.length; index++) { var item = widget.struct.images[index]; late ImageProvider ip; if (item.pkzFile != null) { ip = PkzImageProvider(item.pkzFile!.pkzPath, item.pkzFile!.path); } else if (item.downloadLocalPath != null) { ip = ResourceDownloadFileImageProvider(item.downloadLocalPath!); } else { ip = ResourceRemoteImageProvider(item.fileServer, item.path); } ips.add(ip); } for (var ip in ips) { options.add(PhotoViewGalleryPageOptions( disableGestures: FullScreenAction.TOUCH_DOUBLE == currentFullScreenAction() || FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT == currentFullScreenAction(), imageProvider: ip, initialScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained * readerZoomMinScale, maxScale: PhotoViewComputedScale.contained * readerZoomMaxScale, tightMode: dragRegionLock(), errorBuilder: (b, e, s) { print("$e,$s"); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return buildError(constraints.maxWidth, constraints.maxHeight); }, ); }, filterQuality: FilterQuality.high, )); } _preloadJump(super._startIndex, init: true); } @override void dispose() { _pageController.dispose(); super.dispose(); } @override void _needJumpTo(int index, bool animation) { if (noAnimation() || animation == false) { _pageController.jumpToPage( index, ); } else { _pageController.animateToPage( index, duration: const Duration(milliseconds: 400), curve: Curves.ease, ); } _preloadJump(index); } @override void _needScrollForward() {} @override void _needScrollBackward() {} _preloadJump(int index, {bool init = false}) { fn() { for (var i = index - 1; i < index + 3; i++) { if (i < 0 || i >= ips.length) continue; final ip = ips[i]; precacheImage(ip, context); } } if (init) { WidgetsBinding.instance?.addPostFrameCallback((_) => fn()); } else { fn(); } } Future _onLongPress() async { if (_current >= 0 && _current < widget.struct.images.length) { var item = widget.struct.images[_current]; if (item.pkzFile != null) { return; } Future load() async { var item = widget.struct.images[_current]; if (item.downloadLocalPath != null) { return method.downloadImagePath(item.downloadLocalPath!); } var data = await method.remoteImageData(item.fileServer, item.path); return data.finalPath; } String? choose = await chooseListDialog(context, tr('app.please_choose'), [tr('app.preview_image'), tr('app.save_image')]); if (choose == null) { return; } if (choose == tr('app.preview_image')) { try { var file = await load(); Navigator.of(context).push(mixRoute( builder: (context) => FilePhotoViewScreen(file), )); } catch (e) { defaultToast(context, tr('components.image_reader.image_load_failed')); } } else if (choose == tr('app.save_image')) { try { var file = await load(); saveImage(file, context); } catch (e) { defaultToast(context, tr('components.image_reader.image_load_failed')); } } } } void _onGalleryPageChange(int to) { if (to > super._current && currentAutoFullScreenOnForward() && !widget.struct.fullScreen) { widget.struct.onFullScreenChange(true); } for (var i = to; i < to + 3 && i < ips.length; i++) { final ip = ips[i]; precacheImage(ip, context); } // 包含一个下一章, 假设5张图片 0,1,2,3,4 length=5, 下一章=5 if (to >= 0 && to < widget.struct.images.length) { super._onCurrentChange(to); } } @override Widget _buildViewer() { gallery = PhotoViewGallery.builder( scrollDirection: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? Axis.vertical : Axis.horizontal, reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT, backgroundDecoration: BoxDecoration(color: readerBackgroundColorObj), loadingBuilder: (context, event) => LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return buildLoading(constraints.maxWidth, constraints.maxHeight); }, ), pageController: _pageController, onPageChanged: _onGalleryPageChange, itemCount: widget.struct.images.length, builder: (BuildContext context, int index) { return options[index]; }, allowImplicitScrolling: true, ); gallery = GestureDetector( child: gallery, onLongPress: _onLongPress, ); gallery = Container( padding: EdgeInsets.only( top: widget.struct.fullScreen ? 0 : super._topBarHeight(), bottom: widget.struct.fullScreen ? 0 : super._bottomBarHeight(), ), child: gallery, ); gallery = Stack( children: [ gallery, _buildNextEpController(), ], ); return gallery; } Widget _buildNextEpController() { if (super._fullscreenController() || _current < widget.struct.images.length - 1) return Container(); return Align( alignment: Alignment.bottomRight, child: Material( color: Colors.transparent, child: SafeArea( top: false, child: Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4), decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(10), bottomLeft: Radius.circular(10), ), color: Color(0x88000000), ), child: GestureDetector( onTap: () { if (_hasNextEp()) { _onNextAction(); } else { Navigator.of(context).pop(); } }, child: Text( _hasNextEp() ? tr('components.image_reader.next_chapter') : tr('components.image_reader.end_reading'), style: const TextStyle(color: Colors.white), ), ), ), ), ), ); } } /////////////////////////////////////////////////////////////////////////////// class _TwoPageGalleryReaderState extends _ImageReaderContentState { late PageController _pageController; var _controllerTime = DateTime.now().millisecondsSinceEpoch + 400; late final List _trueSizes = []; List ips = []; List options = []; late PhotoViewGallery _view; @override void initState() { // 需要先初始化 super._startIndex 才能使用, 所以在上面 for (var e in widget.struct.images) { if (e.pkzFile != null && e.width != null && e.height != null && e.width! > 0 && e.height! > 0) { _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); } else if (e.downloadLocalPath != null) { _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); } else { _trueSizes.add(null); } } super.initState(); _pageController = PageController(initialPage: super._startIndex ~/ 2); for (var index = 0; index < widget.struct.images.length; index++) { var item = widget.struct.images[index]; late ImageProvider ip; if (item.pkzFile != null) { ip = PkzImageProvider(item.pkzFile!.pkzPath, item.pkzFile!.path); } else if (item.downloadLocalPath != null) { ip = ResourceDownloadFileImageProvider(item.downloadLocalPath!); } else { ip = ResourceRemoteImageProvider(item.fileServer, item.path); } ips.add(ip); } for (var index = 0; index < ips.length; index += 2) { // 两页 late ImageProvider leftIp = ips[index]; late ImageProvider rightIp = ips[index + 1]; if (index + 1 < ips.length) { leftIp = ips[index]; rightIp = ips[index + 1]; } else { leftIp = ips[index]; // ImageProvider by color black rightIp = const AssetImage('lib/assets/0.png'); } if (twoPageDirection == TwoPageDirection.RIGHT_TO_LEFT) { final temp = leftIp; leftIp = rightIp; rightIp = temp; } late Alignment leftAlignment, rightAlignment; switch (gReaderTwoPageDirection) { case ReaderTwoPageDirection.CLOSE_TO: leftAlignment = Alignment.centerRight; rightAlignment = Alignment.centerLeft; break; case ReaderTwoPageDirection.PULL_AWAY: leftAlignment = Alignment.centerLeft; rightAlignment = Alignment.centerRight; break; case ReaderTwoPageDirection.EACH_CENTERED: leftAlignment = Alignment.center; rightAlignment = Alignment.center; break; } options.add( PhotoViewGalleryPageOptions.customChild( disableGestures: FullScreenAction.TOUCH_DOUBLE == currentFullScreenAction() || FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT == currentFullScreenAction(), initialScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained * readerZoomMinScale, maxScale: PhotoViewComputedScale.contained * readerZoomMaxScale, tightMode: dragRegionLock(), child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Row( children: [ Expanded( child: Align( alignment: leftAlignment, child: Image( image: leftIp, fit: BoxFit.contain, // loadingBuilder: (context, child, event) => buildLoading(constraints.maxWidth, constraints.maxHeight), errorBuilder: (b, e, s) { print("$e,$s"); return buildError( constraints.maxWidth / 2, constraints.maxHeight / 2, ); }, ), ), ), Expanded( child: Align( alignment: rightAlignment, child: Image( image: rightIp, fit: BoxFit.contain, // loadingBuilder: (context, child, event) => buildLoading(constraints.maxWidth, constraints.maxHeight), errorBuilder: (b, e, s) { print("$e,$s"); return buildError( constraints.maxWidth / 2, constraints.maxHeight / 2, ); }, ), ), ), ], ); }, ), ), ); } _view = PhotoViewGallery( pageController: _pageController, pageOptions: options, scrollDirection: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? Axis.vertical : Axis.horizontal, reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT, onPageChanged: _onGalleryPageChange, backgroundDecoration: BoxDecoration(color: readerBackgroundColorObj), ); _preloadJump(super._startIndex, init: true); } @override void dispose() { _pageController.dispose(); super.dispose(); } @override void _needJumpTo(int index, bool animation) { if (noAnimation() || animation == false) { _pageController.jumpToPage( index ~/ 2, ); } else { _pageController.animateToPage( index ~/ 2, duration: const Duration(milliseconds: 400), curve: Curves.ease, ); } _preloadJump(index); } @override void _needScrollBackward() { // TODO: implement _needScrollBackward } @override void _needScrollForward() { // TODO: implement _needScrollForward } _preloadJump(int index, {bool init = false}) { fn() { for (var i = index - 2; i < index + 5; i++) { if (i < 0 || i >= ips.length) continue; final ip = ips[i]; precacheImage(ip, context); } } if (init) { WidgetsBinding.instance?.addPostFrameCallback((_) => fn()); } else { fn(); } } @override Widget _buildViewer() { return Stack( children: [ GestureDetector( onLongPress: _onLongPress, child: _view, ), _buildNextEpController(), ], ); } void _onGalleryPageChange(int to) { var toIndex = to * 2; if (toIndex > super._current && currentAutoFullScreenOnForward() && !widget.struct.fullScreen) { widget.struct.onFullScreenChange(true); } // 提前加载 for (var i = toIndex + 2; i < toIndex + 5 && i < ips.length; i++) { final ip = ips[i]; precacheImage(ip, context); } // 包含一个下一章, 假设5张图片 0,1,2,3,4 length=5, 下一章=5 if (to >= 0 && to < widget.struct.images.length) { super._onCurrentChange(toIndex); } } Widget _buildNextEpController() { if (super._fullscreenController() || _current < widget.struct.images.length - 2) return Container(); return Align( alignment: Alignment.bottomRight, child: Material( color: Colors.transparent, child: SafeArea( top: false, child: Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4), decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(10), bottomLeft: Radius.circular(10), ), color: Color(0x88000000), ), child: GestureDetector( onTap: () { if (_hasNextEp()) { _onNextAction(); } else { Navigator.of(context).pop(); } }, child: Text( _hasNextEp() ? tr('components.image_reader.next_chapter') : tr('components.image_reader.end_reading'), style: const TextStyle(color: Colors.white), ), ), ), ), ), ); } Future _onLongPress() async { List matchImages = []; if (_current >= 0 && _current < widget.struct.images.length) { var item = widget.struct.images[_current]; if (item.pkzFile != null) { return; } matchImages.add(item); } if (_current + 1 >= 0 && _current + 1 < widget.struct.images.length) { var item = widget.struct.images[_current + 1]; if (item.pkzFile != null) { return; } matchImages.add(item); } if (matchImages.isEmpty) { return; } String? choose = await chooseListDialog(context, tr('app.please_choose'), [tr('components.image_reader.save_image_in_this_page')]); if (choose == null) { return; } if (choose == tr('components.image_reader.save_image_in_this_page')) { for (var item in matchImages) { if (item.downloadLocalPath != null) { var file = await method.downloadImagePath(item.downloadLocalPath!); saveImage(file, context); } else { var data = await method.remoteImageData(item.fileServer, item.path); saveImage(data.finalPath, context); } } } } } /////////////////////////////////////////////////////////////////////////////// Color invertColor(Color color) { return Color.fromRGBO( 255 - color.red, 255 - color.green, 255 - color.blue, 1.0, ); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// ================================================ FILE: lib/screens/components/Images.dart ================================================ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Cross.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:flutter_svg/svg.dart'; import 'package:pikapika/basic/config/ImageAddress.dart'; import 'dart:io'; import 'dart:ui' as ui show Codec; import '../../basic/config/IconLoading.dart'; import '../FilePhotoViewScreen.dart'; // 从本地加载图片 class ResourceFileImageProvider extends ImageProvider { final String path; final double scale; ResourceFileImageProvider(this.path, {this.scale = 1.0}); @override ImageStreamCompleter load( ResourceFileImageProvider key, DecoderCallback decode, ) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key), scale: key.scale, ); } @override Future obtainKey( ImageConfiguration configuration) { return SynchronousFuture(this); } Future _loadAsync(ResourceFileImageProvider key) async { assert(key == this); return PaintingBinding.instance!.instantiateImageCodec( await File(path).readAsBytes(), ); } @override bool operator ==(dynamic other) { if (other.runtimeType != runtimeType) return false; final ResourceFileImageProvider typedOther = other; return path == typedOther.path && scale == typedOther.scale; } @override int get hashCode => hashValues(path, scale); @override String toString() => '$runtimeType(' 'path: ${describeIdentity(path)},' ' scale: $scale' ')'; } // 从本地加载图片 class ResourceDownloadFileImageProvider extends ImageProvider { final String path; final double scale; ResourceDownloadFileImageProvider(this.path, {this.scale = 1.0}); @override ImageStreamCompleter load( ResourceDownloadFileImageProvider key, DecoderCallback decode, ) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key), scale: key.scale, ); } @override Future obtainKey( ImageConfiguration configuration) { return SynchronousFuture(this); } Future _loadAsync(ResourceDownloadFileImageProvider key) async { assert(key == this); return PaintingBinding.instance!.instantiateImageCodec( await File(await method.downloadImagePath(path)).readAsBytes()); } @override bool operator ==(dynamic other) { if (other.runtimeType != runtimeType) return false; final ResourceDownloadFileImageProvider typedOther = other; return path == typedOther.path && scale == typedOther.scale; } @override int get hashCode => hashValues(path, scale); @override String toString() => '$runtimeType(' 'path: ${describeIdentity(path)},' ' scale: $scale' ')'; } // 从远端加载图片 class ResourceRemoteImageProvider extends ImageProvider { final String fileServer; final String path; final double scale; ResourceRemoteImageProvider(this.fileServer, this.path, {this.scale = 1.0}); @override ImageStreamCompleter load( ResourceRemoteImageProvider key, DecoderCallback decode, ) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key), scale: key.scale, ); } @override Future obtainKey( ImageConfiguration configuration) { return SynchronousFuture(this); } Future _loadAsync(ResourceRemoteImageProvider key) async { assert(key == this); var downloadTo = await method.remoteImageData(fileServer, path); return PaintingBinding.instance!.instantiateImageCodec( await File(downloadTo.finalPath).readAsBytes(), ); } @override bool operator ==(dynamic other) { if (other.runtimeType != runtimeType) return false; final ResourceRemoteImageProvider typedOther = other; return fileServer == typedOther.fileServer && path == typedOther.path && scale == typedOther.scale; } @override int get hashCode => hashValues(fileServer, path, scale); @override String toString() => '$runtimeType(' 'fileServer: ${describeIdentity(fileServer)},' ' path: ${describeIdentity(path)},' ' scale: $scale' ')'; } // 下载的图片 class DownloadImage extends StatefulWidget { final String path; final double? width; final double? height; const DownloadImage({ Key? key, required this.path, this.width, this.height, }) : super(key: key); @override State createState() => _DownloadImageState(); } class _DownloadImageState extends State { late final Future _future = method.downloadImagePath(widget.path); @override Widget build(BuildContext context) { return pathFutureImage( _future, widget.width, widget.height, context: context, ); } } // 远端图片 class RemoteImage extends StatefulWidget { final String fileServer; final String path; final double? width; final double? height; final BoxFit fit; const RemoteImage({ Key? key, required this.fileServer, required this.path, this.width, this.height, this.fit = BoxFit.cover, }) : super(key: key); @override State createState() => _RemoteImageState(); } class _RemoteImageState extends State { late bool _mock; late Future _future; @override void initState() { _mock = widget.fileServer == "" || (widget.fileServer.contains(".xyz/") && currentImageAddress() < 0); if (!_mock) { _future = method .remoteImageData(widget.fileServer, widget.path) .then((value) => value.finalPath); } super.initState(); } @override Widget build(BuildContext context) { if (_mock) { return buildMock(widget.width, widget.height); } return pathFutureImage( _future, widget.width, widget.height, fit: widget.fit, context: context, ); } } Widget pathFutureImage(Future future, double? width, double? height, {BoxFit fit = BoxFit.cover, BuildContext? context}) { return FutureBuilder( future: future, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { print("${snapshot.error}"); print("${snapshot.stackTrace}"); return buildError(width, height); } if (snapshot.connectionState != ConnectionState.done) { return buildLoading(width, height); } return buildFile( snapshot.data!, width, height, fit: fit, context: context, ); }); } // 通用方法 Widget buildSvg(String source, double? width, double? height, {Color? color, double? margin}) { var widget = Container( width: width, height: height, padding: margin != null ? const EdgeInsets.all(10) : null, child: Center( child: SvgPicture.asset( source, width: width, height: height, color: color, ), ), ); return GestureDetector(onLongPress: () {}, child: widget); } Widget buildMock(double? width, double? height) { var widget = Container( width: width, height: height, padding: const EdgeInsets.all(10), child: Center( child: SvgPicture.asset( 'lib/assets/unknown.svg', width: width, height: height, color: Colors.grey.shade600, ), ), ); return GestureDetector(onLongPress: () {}, child: widget); } Widget buildError(double? width, double? height) { double? size; if (width != null && height != null) { size = width < height ? width : height; } return SizedBox( width: width, height: height, child: Center( child: Icon( Icons.error_outline, size: size, color: Colors.grey, ), ), ); } Widget buildLoading(double? width, double? height) { double? size; if (width != null && height != null) { size = width < height ? width : height; } return SizedBox( width: width, height: height, child: Center( child: Icon( Icons.downloading, size: size, color: Colors.grey, ), ), ); } Widget buildFile(String file, double? width, double? height, {BoxFit fit = BoxFit.cover, BuildContext? context}) { var image = Image( image: ResourceFileImageProvider(file), width: width, height: height, errorBuilder: (a, b, c) { print("$b"); print("$c"); return buildError(width, height); }, fit: fit, ); if (context == null) return image; return GestureDetector( onLongPress: () async { String? choose = await chooseListDialog(context, '请选择', ['预览图片', '保存图片']); switch (choose) { case '预览图片': Navigator.of(context).push(mixRoute( builder: (context) => FilePhotoViewScreen(file), )); break; case '保存图片': saveImage(file, context); break; } }, child: image, ); } ================================================ FILE: lib/screens/components/ItemBuilder.dart ================================================ import 'package:flutter/material.dart'; // 非全屏FutureBuilder封装 class ItemBuilder extends StatelessWidget { final Future future; final AsyncWidgetBuilder successBuilder; final Future Function() onRefresh; final double? loadingHeight; final double? height; const ItemBuilder({ Key? key, required this.future, required this.successBuilder, required this.onRefresh, this.height, this.loadingHeight, }) : super(key: key); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var _maxWidth = constraints.maxWidth; var _loadingHeight = height ?? loadingHeight ?? _maxWidth / 2; return FutureBuilder( future: future, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { print("${snapshot.error}"); print("${snapshot.stackTrace}"); return InkWell( onTap: onRefresh, child: SizedBox( width: _maxWidth, height: _loadingHeight, child: Center( child: Icon(Icons.sync_problem, size: _loadingHeight / 1.5), ), ), ); } if (snapshot.connectionState != ConnectionState.done) { return SizedBox( width: _maxWidth, height: _loadingHeight, child: Center( child: Icon(Icons.sync, size: _loadingHeight / 1.5), ), ); } return SizedBox( width: _maxWidth, height: height, child: successBuilder(context, snapshot), ); }); }, ); } } ================================================ FILE: lib/screens/components/LinkToComicInfo.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/basic/Navigator.dart'; import '../ComicInfoScreen.dart'; class LinkToComicInfo extends StatelessWidget { final String comicId; final Widget child; const LinkToComicInfo({ required this.comicId, required this.child, Key? key, }):super(key: key); @override Widget build(BuildContext context) => InkWell( onTap: () { navPushOrReplace( context, (context) => ComicInfoScreen(comicId: comicId), ); }, child: child, ); } ================================================ FILE: lib/screens/components/ListView.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/basic/config/EBookScrolling.dart'; import '../../basic/config/EBookScrollingRange.dart'; import '../../basic/config/EBookScrollingTrigger.dart'; class PikaListView extends StatefulWidget { final EdgeInsets? padding; final ScrollController? controller; final List children; final ScrollPhysics? physics; const PikaListView({ Key? key, required this.children, this.controller, this.padding, this.physics, }) : super(key: key); @override State createState() => _PikaListViewState(); } class _PikaListViewState extends State { late ScrollController _privateController; @override void initState() { if (widget.controller == null) { _privateController = ScrollController(); } super.initState(); } @override void dispose() { if (widget.controller == null) { _privateController.dispose(); } super.dispose(); } ScrollController get _controller => widget.controller ?? _privateController; double _y = 0; @override Widget build(BuildContext context) { if (!eBookScrolling) { return ListView( children: widget.children, controller: _controller, padding: widget.padding, physics: widget.physics, ); } return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return GestureDetector( onPanDown: (details) { _y = 0; }, onPanUpdate: (details) { _y += details.delta.dy; }, onPanEnd: (details) { final lmPoints = (MediaQuery.of(context).devicePixelRatio * (160 / 2.54)); final double centimeters = _y / lmPoints; late double off; if (centimeters < -eBookScrollingTrigger) { off = _controller.offset + eBookScrollingRange * constraints.maxHeight; off = off.clamp(0, _controller.position.maxScrollExtent); _controller.jumpTo(off); _controller.notifyListeners(); } else if (centimeters > eBookScrollingTrigger) { off = _controller.offset - eBookScrollingRange * constraints.maxHeight; off = off.clamp(0, _controller.position.maxScrollExtent); _controller.jumpTo(off); _controller.notifyListeners(); } }, child: ListView( physics: const NeverScrollableScrollPhysics(), children: widget.children, controller: _controller, padding: widget.padding, ), ); }, ); } } ================================================ FILE: lib/screens/components/MouseAndTouchScrollBehavior.dart ================================================ import 'dart:ui'; import 'package:flutter/material.dart'; final mouseAndTouchScrollBehavior = MouseAndTouchScrollBehavior(); class MouseAndTouchScrollBehavior extends MaterialScrollBehavior { @override Set get dragDevices => { PointerDeviceKind.touch, PointerDeviceKind.mouse, }; } ================================================ FILE: lib/screens/components/NetworkSetting.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/basic/config/Address.dart'; import 'package:pikapika/basic/config/ImageAddress.dart'; import 'package:pikapika/basic/config/Proxy.dart'; import 'package:pikapika/basic/config/UseApiLoadImage.dart'; // 网络设置 class NetworkSetting extends StatelessWidget { const NetworkSetting({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Column( children: [ switchAddressSetting(), imageSwitchAddressSetting(), useApiLoadImageSetting(), proxySetting(), reloadSwitchAddressSetting(), ], ); } } ================================================ FILE: lib/screens/components/PkzComicInfoCard.dart ================================================ import 'package:pikapika/i18.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Cross.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/SearchScreen.dart'; import 'package:pikapika/basic/Navigator.dart'; import '../../basic/config/CopyFullName.dart'; import '../../basic/config/CopyFullNameTemplate.dart'; import 'ComicInfoCard.dart'; import 'PkzImages.dart'; // 漫画卡片 class PkzComicInfoCard extends StatefulWidget { final String pkzPath; final PkzComic info; final bool linkItem; final PkzComicViewLog? displayViewLog; const PkzComicInfoCard({ required this.info, required this.pkzPath, this.linkItem = false, Key? key, this.displayViewLog, }) : super(key: key); @override State createState() => _ComicInfoCard(); } class _ComicInfoCard extends State { @override Widget build(BuildContext context) { var info = widget.info; final theme = Theme.of(context); return Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: theme.dividerColor, ), ), ), child: Row( children: [ Container( padding: const EdgeInsets.only(right: 10), child: PkzImage( pkzPath: widget.pkzPath, path: info.coverPath, width: imageWidth, height: imageHeight, ), ), Expanded( child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ widget.linkItem ? GestureDetector( onLongPress: () { var fin = copyFullNameTemplate() .replaceAll("{title}", info.title) .replaceAll("{author}", info.author); if (fin.isEmpty) { fin = info.title; } if (copyFullName()) { confirmCopy( context, fin); } else { confirmCopy(context, info.title); } }, child: Text(info.title, style: titleStyle), ) : Text(info.title, style: titleStyle), Container(height: 5), widget.linkItem ? InkWell( onTap: () { // todo }, onLongPress: () { confirmCopy(context, info.author); }, child: Text(info.author, style: authorStyle), ) : Text(info.author, style: authorStyle), Container(height: 5), Text.rich( widget.linkItem ? TextSpan( children: [ TextSpan(text: tr('app.categories') + ' :'), ...info.categories.map( (e) => TextSpan( children: [ const TextSpan(text: ' '), TextSpan( text: e, recognizer: TapGestureRecognizer() ..onTap = () { // todo }), ], ), ), ], ) : TextSpan( text: "${tr('app.categories')} : ${info.categories.join(' ')}"), style: TextStyle( fontSize: 13, color: Theme.of(context) .textTheme .bodyText1! .color! .withAlpha(0xCC), ), ), Container(height: 5), widget.displayViewLog != null && widget.displayViewLog!.lastViewEpId.isNotEmpty ? Container( padding: EdgeInsets.only(bottom: 5), child: Text( "${tr('app.last_viewed')} ${widget.displayViewLog!.lastViewEpName}", maxLines: 1, overflow: TextOverflow.ellipsis, style: authorStyleX, ), ) : Container(), ], ), ), ], ), ), ], ), ); } } ================================================ FILE: lib/screens/components/PkzImages.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Method.dart'; import 'dart:ui' as ui show Codec; import 'Images.dart'; import 'dart:typed_data'; // 从本地加载图片 class PkzImageProvider extends ImageProvider { final String pkzPath; final String path; final double scale; PkzImageProvider(this.pkzPath, this.path, {this.scale = 1.0}); @override ImageStreamCompleter load( PkzImageProvider key, DecoderCallback decode, ) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key), scale: key.scale, ); } @override Future obtainKey(ImageConfiguration configuration) { return SynchronousFuture(this); } Future _loadAsync(PkzImageProvider key) async { assert(key == this); return PaintingBinding.instance!.instantiateImageCodec( await method.loadPkzFile(pkzPath, path), ); } @override bool operator ==(dynamic other) { if (other.runtimeType != runtimeType) return false; final PkzImageProvider typedOther = other; return pkzPath == typedOther.pkzPath && path == typedOther.path && scale == typedOther.scale; } @override int get hashCode => hashValues(path, scale); @override String toString() => '$runtimeType(' ' pkzPath: ${describeIdentity(pkzPath)},' ' path: ${describeIdentity(path)},' ' scale: $scale' ')'; } // 远端图片 class PkzImage extends StatefulWidget { final String pkzPath; final String path; final double? width; final double? height; final BoxFit fit; const PkzImage({ Key? key, required this.pkzPath, required this.path, this.width, this.height, this.fit = BoxFit.cover, }) : super(key: key); @override State createState() => _PkzImageState(); } class _PkzImageState extends State { late bool _mock; @override void initState() { _mock = widget.path == ""; super.initState(); } @override Widget build(BuildContext context) { if (_mock) { return buildMock(widget.width, widget.height); } return Image( image: PkzImageProvider(widget.pkzPath, widget.path), width: widget.width, height: widget.height, errorBuilder: (a, b, c) { print("$b"); print("$c"); return buildError(widget.width, widget.height); }, fit: widget.fit, ); } } // 远端图片 class PkzLoadingImage extends StatefulWidget { final String pkzPath; final String path; final double? width; final double? height; final BoxFit fit; final Function(Size)? onTrueSize; const PkzLoadingImage({ Key? key, required this.pkzPath, required this.path, this.width, this.height, this.fit = BoxFit.cover, this.onTrueSize, }) : super(key: key); @override State createState() => _PkzLoadingImageState(); } class _PkzLoadingImageState extends State { late bool _mock; late Future data; @override void initState() { _mock = widget.path == ""; if (!_mock) { data = () async { final data = await method.loadPkzFile(widget.pkzPath, widget.path); if (widget.onTrueSize != null) { var decodedImage = await decodeImageFromList(data); widget.onTrueSize!( Size( decodedImage.width.toDouble(), decodedImage.height.toDouble(), ), ); } return data; }(); } super.initState(); } @override Widget build(BuildContext context) { if (_mock) { return buildMock(widget.width, widget.height); } return Image( image: PkzImageProvider(widget.pkzPath, widget.path), width: widget.width, height: widget.height, errorBuilder: (a, b, c) { print("$b"); print("$c"); return buildError(widget.width, widget.height); }, fit: widget.fit, ); } } ================================================ FILE: lib/screens/components/RecommendLinksPanel.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/basic/Cross.dart'; import 'package:pikapika/basic/config/RecommendLinks.dart'; class RecommendLinksPanel extends StatefulWidget { final EdgeInsetsGeometry padding; const RecommendLinksPanel({ Key? key, this.padding = const EdgeInsets.fromLTRB(0, 0, 0, 0), }) : super(key: key); @override State createState() => _RecommendLinksPanelState(); } class _RecommendLinksPanelState extends State { @override void initState() { recommendLinksEvent.subscribe(_setState); super.initState(); } @override void dispose() { recommendLinksEvent.unsubscribe(_setState); super.dispose(); } void _setState(_) { setState(() {}); } @override Widget build(BuildContext context) { final links = currentRecommendLinks(); if (links.isEmpty) { return const SizedBox.shrink(); } return Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Divider(), ...links.entries.map((entry) { return ListTile( title: Text(entry.key), onTap: () => openUrl(entry.value), ); }).toList(), ], ), ); } } ================================================ FILE: lib/screens/components/Recommendation.dart ================================================ import 'package:flutter/material.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/screens/ComicInfoScreen.dart'; import 'package:pikapika/basic/Method.dart'; import '../../basic/config/IconLoading.dart'; import 'ItemBuilder.dart'; import 'Images.dart'; // 看过此本子的也在看 class Recommendation extends StatefulWidget { final String comicId; const Recommendation({Key? key, required this.comicId}) : super(key: key); @override State createState() => _RecommendationState(); } class _RecommendationState extends State { late Future> _future = method.recommendation(widget.comicId); @override Widget build(BuildContext context) { return ItemBuilder( future: _future, successBuilder: (BuildContext context, AsyncSnapshot> snapshot) { var _comicList = snapshot.data!; var size = MediaQuery.of(context).size; var min = size.width < size.height ? size.width : size.height; var width = (min - 45) / 4; return Wrap( alignment: WrapAlignment.spaceAround, children: _comicList .map((e) => InkWell( onTap: () { var i = 0; Navigator.pushAndRemoveUntil( context, mixRoute( builder: (context) => ComicInfoScreen(comicId: e.id)), (route) => i++ < 10); }, child: Card( child: SizedBox( width: width, child: Column( children: [ LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { return RemoteImage( width: width, fileServer: e.thumb.fileServer, path: e.thumb.path, ); }), Text( e.title + '\n', maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(height: 1.4), strutStyle: const StrutStyle(height: 1.4), ), ], ), ), ), )) .toList(), ); }, onRefresh: () async => setState(() => _future = method.recommendation(widget.comicId)), ); } } ================================================ FILE: lib/screens/components/RightClickPop.dart ================================================ import 'package:flutter/material.dart'; import '../../basic/config/UsingRightClickPop.dart'; Widget rightClickPop({ required Widget child, required BuildContext context, bool canPop = true, }) => currentUsingRightClickPop() ? GestureDetector( onSecondaryTap: () { if (canPop) { Navigator.of(context).pop(); } }, child: child, ) : child; ================================================ FILE: lib/screens/components/TimeoutLock.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import '../../basic/config/Authentication.dart'; import '../../basic/config/TimeoutLock.dart'; class TimeoutLock extends StatefulWidget { final Widget child; const TimeoutLock({required this.child, Key? key}) : super(key: key); @override State createState() => _TimeoutLockState(); } class _TimeoutLockState extends State with WidgetsBindingObserver { DateTime? _appLostFocusTimestamp; bool _locked = false; @override void initState() { super.initState(); WidgetsBinding.instance?.addObserver(this); } @override void dispose() { WidgetsBinding.instance?.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); if (!currentAuthentication() || timeoutLock == 0) return; print("_locked: $_locked"); if (_locked) { return; } if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) { if (_appLostFocusTimestamp != null) { return; } _appLostFocusTimestamp = DateTime.now(); } else if (state == AppLifecycleState.resumed) { if (_appLostFocusTimestamp == null) { return; } final currentTimeStamp = DateTime.now(); final difference = currentTimeStamp.difference(_appLostFocusTimestamp!); _appLostFocusTimestamp = null; if (difference.inSeconds > timeoutLock) { _locked = true; Navigator.of(context) .push(MaterialPageRoute( builder: (context) => const TimeoutScreen(), )) .then((value) { _locked = false; }); } } } @override Widget build(BuildContext context) { return widget.child; } } class TimeoutScreen extends StatelessWidget { const TimeoutScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { return false; }, child: Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ MaterialButton( onPressed: () async { if (true == await verifyAuthentication(context)) { Navigator.of(context).pop(); } }, child: const Text('您离开APP很久了,请验点击证身份'), ), ], ), ), ), ); } } ================================================ FILE: lib/screens/components/UserProfileCard.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'dart:ui'; import 'package:pikapika/i18.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:image_cropper/image_cropper.dart'; import 'package:image_picker/image_picker.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/screens/components/Avatar.dart'; import 'package:pikapika/screens/components/Images.dart'; import 'package:pikapika/screens/components/ItemBuilder.dart'; import '../../basic/config/IconLoading.dart'; import 'DesktopCropper.dart'; const double _cardHeight = 180; // 用户信息卡 class UserProfileCard extends StatefulWidget { const UserProfileCard({Key? key}) : super(key: key); @override State createState() => _UserProfileCardState(); } class _UserProfileCardState extends State { late Future _future = _load(); Future _load() async { var profile = await method.userProfile(); if (!profile.isPunched) { await method.punchIn(); profile.isPunched = true; defaultToast(context, tr('app.auto_punch')); } return profile; } @override void initState() { super.initState(); } @override void dispose() { super.dispose(); } @override Widget build(BuildContext context) { var theme = Theme.of(context); var nameStyle = const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, ); var nameStrutStyle = const StrutStyle( fontSize: 14, forceStrutHeight: true, fontWeight: FontWeight.bold, ); var levelStyle = TextStyle( fontSize: 12, color: theme.colorScheme.secondary.withOpacity(.9), fontWeight: FontWeight.bold, ); var levelStrutStyle = const StrutStyle( fontSize: 12, forceStrutHeight: true, fontWeight: FontWeight.bold, ); var sloganStyle = TextStyle( fontSize: 10, color: theme.textTheme.bodyText1?.color?.withOpacity(.5), ); var sloganStrutStyle = const StrutStyle( fontSize: 10, forceStrutHeight: true, ); return ItemBuilder( future: _future, onRefresh: () async { setState(() => _future = method.userProfile()); }, height: _cardHeight, successBuilder: (BuildContext context, AsyncSnapshot snapshot) { UserProfile profile = snapshot.data!; return Stack( children: [ Stack( children: [ Opacity( opacity: .25, // child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return RemoteImage( path: profile.avatar.path, fileServer: profile.avatar.fileServer, width: constraints.maxWidth, height: _cardHeight, ); }, ), ), Positioned.fromRect( rect: Rect.largest, child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), child: Container(), ), ), ], ), SizedBox( height: _cardHeight, child: Column( children: [ Expanded(child: Container()), GestureDetector( onTap: () async { if (Platform.isAndroid || Platform.isIOS) { await _updateAvatarPhone(); } else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { await _updateAvatarDesktop(); } }, child: Avatar(profile.avatar, size: 65), ), Container(height: 5), Text( profile.name, style: nameStyle, strutStyle: nameStrutStyle, ), Text( "(Lv. ${profile.level}) (${profile.title})", style: levelStyle, strutStyle: levelStrutStyle, ), Container(height: 8), GestureDetector( onTap: () async { var input = await inputString( context, "更新签名", defaultValue: profile.slogan ?? "", ); if (input != null) { await method.updateSlogan(input); _reload(); } }, child: Text( profile.slogan == null || profile.slogan!.isEmpty ? "这个人很懒, 什么也没留下" : profile.slogan!, style: sloganStyle, strutStyle: sloganStrutStyle, ), ), Expanded(child: Container()), ], ), ) ], ); }, ); } Future _updateAvatarPhone() async { final ImagePicker _picker = ImagePicker(); final XFile? image = await _picker.pickImage(source: ImageSource.gallery); if (image != null) { final theme = Theme.of(context); final cropper = ImageCropper(); File? croppedFile = await cropper.cropImage( sourcePath: image.path, aspectRatioPresets: [ CropAspectRatioPreset.square, ], aspectRatio: const CropAspectRatio(ratioX: 200, ratioY: 200), maxWidth: 200, maxHeight: 200, androidUiSettings: AndroidUiSettings( toolbarTitle: "修改头像", toolbarColor: theme.appBarTheme.backgroundColor, toolbarWidgetColor: Colors.white, initAspectRatio: CropAspectRatioPreset.original, lockAspectRatio: true, ), iosUiSettings: const IOSUiSettings( resetAspectRatioEnabled: true, aspectRatioLockEnabled: true, title: "修改头像", ), ); if (croppedFile != null) { var buff = await croppedFile.readAsBytes(); var data = base64Encode(buff); await method.updateAvatar(data); _reload(); } } } Future _updateAvatarDesktop() async { FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: false, ); if (result != null) { List? buff = await Navigator.of(context).push( mixRoute(builder: (BuildContext context) { return DesktopCropper( file: result.files.first.path!, aspectRatio: 1, title: "裁剪头像", ); }), ); if (buff != null) { var data = base64Encode(buff); await method.updateAvatar(data); _reload(); } } } void _reload() { setState(() { _future = _load(); }); } } ================================================ FILE: lib/screens/components/flutter_search_bar.dart ================================================ // Copyright (c) 2017, Spencer. All rights reserved. Use of this source code // is governed by a BSD-style license that can be found in the LICENSE file. import 'package:flutter/material.dart'; typedef Widget AppBarCallback(BuildContext context); typedef void TextFieldSubmitCallback(String value); typedef void TextFieldChangeCallback(String value); typedef void SetStateCallback(void fn()); class SearchBar { /// Whether the search should take place "in the existing search bar", meaning whether it has the same background or a flipped one. Defaults to true. final bool inBar; /// Whether or not the search bar should close on submit. Defaults to true. final bool closeOnSubmit; /// Whether the text field should be cleared when it is submitted final bool clearOnSubmit; /// A callback which should return an AppBar that is displayed until search is started. One of the actions in this AppBar should be a search button which you obtain from SearchBar.getSearchAction(). This will be called every time search is ended, etc. (like a build method on a widget) final AppBarCallback buildDefaultAppBar; /// A void callback which takes a string as an argument, this is fired every time the search is submitted. Do what you want with the result. final TextFieldSubmitCallback? onSubmitted; /// A void callback which gets fired on close button press. final VoidCallback? onClosed; /// A callback which is fired when clear button is pressed. final VoidCallback? onCleared; /// Since this should be inside of a State class, just pass setState to this. final SetStateCallback setState; /// Whether or not the search bar should add a clear input button, defaults to true. final bool showClearButton; /// What the hintText on the search bar should be. Defaults to 'Search'. final String hintText; /// Whether search is currently active. final ValueNotifier isSearching = ValueNotifier(false); /// A callback which is invoked each time the text field's value changes final TextFieldChangeCallback? onChanged; /// The type of keyboard to use for editing the search bar text. Defaults to 'TextInputType.text'. final TextInputType keyboardType; /// The controller to be used in the textField. late TextEditingController controller; /// Whether the clear button should be active (fully colored) or inactive (greyed out) bool _clearActive = false; SearchBar({ required this.setState, required this.buildDefaultAppBar, this.onSubmitted, TextEditingController? controller, this.hintText = 'Search', this.inBar = true, this.closeOnSubmit = true, this.clearOnSubmit = true, this.showClearButton = true, this.onChanged, this.onClosed, this.onCleared, this.keyboardType = TextInputType.text, }) { this.controller = controller ?? new TextEditingController(); // Don't waste resources on listeners for the text controller if the dev // doesn't want a clear button anyways in the search bar if (!this.showClearButton) { return; } this.controller.addListener(() { if (this.controller.text.isEmpty) { // If clear is already disabled, don't disable it if (_clearActive) { setState(() { _clearActive = false; }); } return; } // If clear is already enabled, don't enable it if (!_clearActive) { setState(() { _clearActive = true; }); } }); } /// Initializes the search bar. /// /// This adds a route that listens for onRemove (and stops the search when that happens), and then calls [setState] to rebuild and start the search. void beginSearch(context) { ModalRoute.of(context)!.addLocalHistoryEntry(LocalHistoryEntry(onRemove: () { setState(() { isSearching.value = false; }); })); setState(() { isSearching.value = true; }); } /// Builds, saves and returns the default app bar. /// /// This calls the [buildDefaultAppBar] provided in the constructor. AppBar buildAppBar(BuildContext context) { return buildDefaultAppBar(context) as AppBar; } /// Builds the search bar! /// /// The leading will always be a back button. /// backgroundColor is determined by the value of inBar /// title is always a [TextField] with the key 'SearchBarTextField', and various text stylings based on [inBar]. This is also where [onSubmitted] has its listener registered. /// AppBar buildSearchBar(BuildContext context) { ThemeData theme = Theme.of(context); Color? buttonColor = inBar ? null : theme.iconTheme.color; return AppBar( leading: IconButton( icon: const BackButtonIcon(), color: buttonColor, tooltip: MaterialLocalizations.of(context).backButtonTooltip, onPressed: () { onClosed?.call(); controller.clear(); Navigator.maybePop(context); }), backgroundColor: inBar ? null : theme.canvasColor, title: Directionality( textDirection: Directionality.of(context), child: TextField( key: Key('SearchBarTextField'), keyboardType: keyboardType, decoration: InputDecoration( hintText: hintText, hintStyle: inBar ? null : TextStyle( color: theme.textTheme.headline4!.color, ), enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, border: InputBorder.none), onChanged: this.onChanged, onSubmitted: (String val) async { if (closeOnSubmit) { await Navigator.maybePop(context); } if (clearOnSubmit) { controller.clear(); } onSubmitted?.call(val); }, autofocus: true, controller: controller, ), ), actions: !showClearButton ? null : [ // Show an icon if clear is not active, so there's no ripple on tap IconButton( icon: Icon(Icons.clear, semanticLabel: "Clear"), color: inBar ? null : buttonColor, disabledColor: inBar ? null : theme.disabledColor, onPressed: !_clearActive ? null : () { onCleared?.call(); controller.clear(); }), ], ); } /// Returns an [IconButton] suitable for an Action /// /// Put this inside your [buildDefaultAppBar] method! IconButton getSearchAction(BuildContext context) { return IconButton( icon: Icon(Icons.search, semanticLabel: "Search"), onPressed: () { beginSearch(context); }); } /// Returns an AppBar based on the value of [isSearching] AppBar build(BuildContext context) { return isSearching.value ? buildSearchBar(context) : buildAppBar(context); } } ================================================ FILE: lib/screens/components/gesture_zoom_box.dart ================================================ import 'package:flutter/material.dart'; import 'dart:math'; class GestureZoomBox extends StatefulWidget { final double minScale; final double maxScale; final double doubleTapScale; final Widget child; final Duration duration; const GestureZoomBox({ Key? key, this.minScale = 1.0, this.maxScale = 2.0, this.doubleTapScale = 2.0, required this.child, this.duration = const Duration(milliseconds: 200), }) : assert(maxScale >= 1.0), assert(minScale > 0), assert(maxScale >= minScale), assert(doubleTapScale >= 1.0 && doubleTapScale <= maxScale), super(key: key); @override State createState() { return _GestureZoomBoxState(); } } class _GestureZoomBoxState extends State with TickerProviderStateMixin { AnimationController? _scaleAnimController; // 缩放动画控制器 AnimationController? _offsetAnimController; // 偏移动画控制器 ScaleUpdateDetails? _latestScaleUpdateDetails; // 上次缩放变化数据 double _scale = 1.0; // 当前缩放值 Offset _offset = Offset.zero; // 当前偏移值 Offset? _doubleTapPosition; // 双击缩放的点击位置 bool _isScaling = false; bool _isDragging = false; final double _maxDragOver = 100; // 拖动超出边界的最大值 @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return Transform( alignment: Alignment.center, transform: Matrix4.identity() ..translate(_offset.dx, _offset.dy) ..scale(_scale, _scale), child: Listener( onPointerUp: _onPointerUp, child: GestureDetector( onDoubleTap: _onDoubleTap, onScaleStart: _onScaleStart, onScaleUpdate: _onScaleUpdate, onScaleEnd: _onScaleEnd, child: AbsorbPointer( absorbing: _scale != 1, child: widget.child, ), ), ), ); } @override void dispose() { _scaleAnimController?.dispose(); _offsetAnimController?.dispose(); super.dispose(); } /// 处理手指抬起事件 [event] _onPointerUp(PointerUpEvent event) { _doubleTapPosition = event.localPosition; } /// 处理双击 _onDoubleTap() { double targetScale = _scale == 1.0 ? widget.doubleTapScale : 1.0; _animationScale(targetScale); if (targetScale == 1.0) { _animationOffset(Offset.zero); } } _onScaleStart(ScaleStartDetails details) { _scaleAnimController?.stop(); _offsetAnimController?.stop(); _isScaling = false; _isDragging = false; _latestScaleUpdateDetails = null; } /// 处理缩放变化 [details] _onScaleUpdate(ScaleUpdateDetails details) { setState(() { if (details.scale != 1.0) { _scaling(details); } else { _dragging(details); } }); } /// 执行缩放 _scaling(ScaleUpdateDetails details) { if (_isDragging) { return; } final latestScaleUpdateDetails = _latestScaleUpdateDetails, size = context.size; _isScaling = true; if (latestScaleUpdateDetails == null || size == null) { _latestScaleUpdateDetails = details; return; } // 计算缩放比例 double scaleIncrement = details.scale - latestScaleUpdateDetails.scale; if (details.scale < 1.0 && _scale > 1.0) { scaleIncrement *= _scale; } if (_scale < 1.0 && scaleIncrement < 0) { scaleIncrement *= (_scale - 0.5); } else if (_scale > widget.maxScale && scaleIncrement > 0) { scaleIncrement *= (2.0 - (_scale - widget.maxScale)); } _scale = max(_scale + scaleIncrement, 0.0); // 计算缩放后偏移前(缩放前后的内容中心对齐)的左上角坐标变化 double scaleOffsetX = size.width * (_scale - 1.0) / 2; double scaleOffsetY = size.height * (_scale - 1.0) / 2; // 将缩放前的触摸点映射到缩放后的内容上 double scalePointDX = (details.localFocalPoint.dx + scaleOffsetX - _offset.dx) / _scale; double scalePointDY = (details.localFocalPoint.dy + scaleOffsetY - _offset.dy) / _scale; // 计算偏移,使缩放中心在屏幕上的位置保持不变 _offset += Offset( (size.width / 2 - scalePointDX) * scaleIncrement, (size.height / 2 - scalePointDY) * scaleIncrement, ); _latestScaleUpdateDetails = details; } /// 执行拖动 _dragging(ScaleUpdateDetails details) { if (_isScaling) { return; } final latestScaleUpdateDetails = _latestScaleUpdateDetails, size = context.size; _isDragging = true; if (latestScaleUpdateDetails == null || size == null) { _latestScaleUpdateDetails = details; return; } // 计算本次拖动增量 double offsetXIncrement = (details.localFocalPoint.dx - latestScaleUpdateDetails.localFocalPoint.dx) * _scale; double offsetYIncrement = (details.localFocalPoint.dy - latestScaleUpdateDetails.localFocalPoint.dy) * _scale; // 处理 X 轴边界 double scaleOffsetX = size.width * (_scale - 1.0) / 2; if (scaleOffsetX <= 0) { offsetXIncrement = 0; } else if (_offset.dx > scaleOffsetX) { offsetXIncrement *= (_maxDragOver - (_offset.dx - scaleOffsetX)) / _maxDragOver; } else if (_offset.dx < -scaleOffsetX) { offsetXIncrement *= (_maxDragOver - (-scaleOffsetX - _offset.dx)) / _maxDragOver; } // 处理 Y 轴边界 double scaleOffsetY = (size.height * _scale - MediaQuery.of(context).size.height) / 2; if (scaleOffsetY <= 0) { offsetYIncrement = 0; } else if (_offset.dy > scaleOffsetY) { offsetYIncrement *= (_maxDragOver - (_offset.dy - scaleOffsetY)) / _maxDragOver; } else if (_offset.dy < -scaleOffsetY) { offsetYIncrement *= (_maxDragOver - (-scaleOffsetY - _offset.dy)) / _maxDragOver; } _offset += Offset(offsetXIncrement, offsetYIncrement); _latestScaleUpdateDetails = details; } /// 缩放/拖动结束 _onScaleEnd(ScaleEndDetails details) { final size = context.size; if (size == null) { return; } if (_scale < widget.minScale) { // 缩放值过小,恢复到最小值 _animationScale(widget.minScale); } else if (_scale > widget.maxScale) { // 缩放值过大,恢复到最大值 _animationScale(widget.maxScale); } if (_scale <= widget.minScale) { // 缩放值过小,修改偏移值,使内容居中 _animationOffset(Offset.zero); } else if (_isDragging) { // 处理拖动超过边界的情况(自动回弹到边界) double realScale = _scale > widget.maxScale ? widget.maxScale : _scale; double targetOffsetX = _offset.dx, targetOffsetY = _offset.dy; // 处理 X 轴边界 double scaleOffsetX = size.width * (realScale - 1.0) / 2; if (scaleOffsetX <= 0) { targetOffsetX = 0; } else if (_offset.dx > scaleOffsetX) { targetOffsetX = scaleOffsetX; } else if (_offset.dx < -scaleOffsetX) { targetOffsetX = -scaleOffsetX; } // 处理 Y 轴边界 double scaleOffsetY = (size.height * realScale - MediaQuery.of(context).size.height) / 2; if (scaleOffsetY < 0) { targetOffsetY = 0; } else if (_offset.dy > scaleOffsetY) { targetOffsetY = scaleOffsetY; } else if (_offset.dy < -scaleOffsetY) { targetOffsetY = -scaleOffsetY; } if (_offset.dx != targetOffsetX || _offset.dy != targetOffsetY) { // 启动越界回弹 _animationOffset(Offset(targetOffsetX, targetOffsetY)); } else { // 处理 X 轴边界 double duration = (widget.duration.inSeconds + widget.duration.inMilliseconds / 1000); Offset targetOffset = _offset + details.velocity.pixelsPerSecond * duration; targetOffsetX = targetOffset.dx; if (targetOffsetX > scaleOffsetX) { targetOffsetX = scaleOffsetX; } else if (targetOffsetX < -scaleOffsetX) { targetOffsetX = -scaleOffsetX; } // 处理 X 轴边界 targetOffsetY = targetOffset.dy; if (targetOffsetY > scaleOffsetY) { targetOffsetY = scaleOffsetY; } else if (targetOffsetY < -scaleOffsetY) { targetOffsetY = -scaleOffsetY; } // 启动惯性滚动 _animationOffset(Offset(targetOffsetX, targetOffsetY)); } } _isScaling = false; _isDragging = false; _latestScaleUpdateDetails = null; } /// 执行动画缩放内容到 [targetScale] _animationScale(double targetScale) { _scaleAnimController?.dispose(); final scaleAnimController = _scaleAnimController = AnimationController(vsync: this, duration: widget.duration); Animation anim = Tween(begin: _scale, end: targetScale) .animate(scaleAnimController); anim.addListener(() { setState(() { _scaling(ScaleUpdateDetails( focalPoint: _doubleTapPosition!, localFocalPoint: _doubleTapPosition!, scale: anim.value, horizontalScale: anim.value, verticalScale: anim.value, )); }); }); anim.addStatusListener((status) { if (status == AnimationStatus.completed) { _onScaleEnd(ScaleEndDetails()); } }); scaleAnimController.forward(); } /// 执行动画偏移内容到 [targetOffset] _animationOffset(Offset targetOffset) { _offsetAnimController?.dispose(); final offsetAnimController = _offsetAnimController = AnimationController(vsync: this, duration: widget.duration); Animation anim = offsetAnimController .drive(Tween(begin: _offset, end: targetOffset)); anim.addListener(() { setState(() { _offset = anim.value; }); }); offsetAnimController.fling(); } } ================================================ FILE: linux/.gitignore ================================================ flutter/ephemeral ================================================ FILE: linux/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.10) project(runner LANGUAGES CXX) set(BINARY_NAME "pikapika") set(APPLICATION_ID "opensource.pikapika") cmake_policy(SET CMP0063 NEW) set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") # Root filesystem for cross-building. if(FLUTTER_TARGET_PLATFORM_SYSROOT) set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) endif() # Configure build options. if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() # Compilation settings that should be applied to most targets. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_14) target_compile_options(${TARGET} PRIVATE -Wall -Werror) target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") endfunction() set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") # Flutter library and tool build rules. add_subdirectory(${FLUTTER_MANAGED_DIR}) # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") # Application build add_executable(${BINARY_NAME} "main.cc" "my_application.cc" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" ) apply_standard_settings(${BINARY_NAME}) target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) add_dependencies(${BINARY_NAME} flutter_assemble) # Only the install-generated bundle's copy of the executable will launch # correctly, since the resources must in the right relative locations. To avoid # people trying to run the unbundled copy, put it in a subdirectory instead of # the default top-level location. set_target_properties(${BINARY_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" ) # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # By default, "installing" just makes a relocatable bundle in the build # directory. set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() # Start with a clean build bundle directory every time. install(CODE " file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") " COMPONENT Runtime) set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() ================================================ FILE: linux/flutter/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.10) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. # Serves the same purpose as list(TRANSFORM ... PREPEND ...), # which isn't available in 3.10. function(list_prepend LIST_NAME PREFIX) set(NEW_LIST "") foreach(element ${${LIST_NAME}}) list(APPEND NEW_LIST "${PREFIX}${element}") endforeach(element) set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) endfunction() # === Flutter Library === # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "fl_basic_message_channel.h" "fl_binary_codec.h" "fl_binary_messenger.h" "fl_dart_project.h" "fl_engine.h" "fl_json_message_codec.h" "fl_json_method_codec.h" "fl_message_codec.h" "fl_method_call.h" "fl_method_channel.h" "fl_method_codec.h" "fl_method_response.h" "fl_plugin_registrar.h" "fl_plugin_registry.h" "fl_standard_message_codec.h" "fl_standard_method_codec.h" "fl_string_codec.h" "fl_value.h" "fl_view.h" "flutter_linux.h" ) list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") target_link_libraries(flutter INTERFACE PkgConfig::GTK PkgConfig::GLIB PkgConfig::GIO ) add_dependencies(flutter 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 from the # flutter tool. add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/_phony_ COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ) ================================================ FILE: linux/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" #include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } ================================================ FILE: linux/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void fl_register_plugins(FlPluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: linux/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: linux/main.cc ================================================ #include "my_application.h" int main(int argc, char** argv) { g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } ================================================ FILE: linux/my_application.cc ================================================ #include "my_application.h" #include #ifdef GDK_WINDOWING_X11 #include #endif #include "flutter/generated_plugin_registrant.h" struct _MyApplication { GtkApplication parent_instance; char** dart_entrypoint_arguments; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); // Use a header bar when running in GNOME as this is the common style used // by applications and is the setup most users will be using (e.g. Ubuntu // desktop). // If running on X and not using GNOME then just use a traditional title bar // in case the window manager does more exotic layout, e.g. tiling. // If running on Wayland assume the header bar will work (may need changing // if future cases occur). gboolean use_header_bar = TRUE; #ifdef GDK_WINDOWING_X11 GdkScreen *screen = gtk_window_get_screen(window); if (GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); if (g_strcmp0(wm_name, "GNOME Shell") != 0) { use_header_bar = FALSE; } } #endif if (use_header_bar) { GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); gtk_header_bar_set_title(header_bar, "pikapi"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { gtk_window_set_title(window, "pikapi"); } gtk_window_set_default_size(window, 1280, 720); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); } // Implements GApplication::local_command_line. static gboolean my_application_local_command_line(GApplication* application, gchar ***arguments, int *exit_status) { MyApplication* self = MY_APPLICATION(application); // Strip out the first argument as it is the binary name. self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; if (!g_application_register(application, nullptr, &error)) { g_warning("Failed to register: %s", error->message); *exit_status = 1; return TRUE; } g_application_activate(application); *exit_status = 0; return TRUE; } // Implements GObject::dispose. static void my_application_dispose(GObject *object) { MyApplication* self = MY_APPLICATION(object); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } static void my_application_class_init(MyApplicationClass* klass) { G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, "flags", G_APPLICATION_NON_UNIQUE, nullptr)); } ================================================ FILE: linux/my_application.h ================================================ #ifndef FLUTTER_MY_APPLICATION_H_ #define FLUTTER_MY_APPLICATION_H_ #include G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, GtkApplication) /** * my_application_new: * * Creates a new Flutter-based application. * * Returns: a new #MyApplication. */ MyApplication* my_application_new(); #endif // FLUTTER_MY_APPLICATION_H_ ================================================ FILE: macos/.gitignore ================================================ # Flutter-related **/Flutter/ephemeral/ **/Pods/ # Xcode-related **/xcuserdata/ ================================================ FILE: macos/Flutter/Flutter-Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: macos/Flutter/Flutter-Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: macos/Flutter/GeneratedPluginRegistrant.swift ================================================ // // Generated file. Do not edit. // import FlutterMacOS import Foundation import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } ================================================ FILE: macos/Podfile ================================================ platform :osx, '10.14' # 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__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) end end ================================================ FILE: macos/Runner/AppDelegate.swift ================================================ import Cocoa import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } } ================================================ FILE: macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "16x16", "idiom" : "mac", "filename" : "app_icon_16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", "filename" : "app_icon_32.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "app_icon_32.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "app_icon_64.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "app_icon_128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "app_icon_256.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "app_icon_256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "app_icon_512.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "app_icon_512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "app_icon_1024.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: macos/Runner/Base.lproj/MainMenu.xib ================================================ ================================================ FILE: macos/Runner/Configs/AppInfo.xcconfig ================================================ // Application-level settings for the Runner target. // // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the // future. If not, the values below would default to using the project name when this becomes a // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. PRODUCT_NAME = pikapika // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = opensource // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved. ================================================ FILE: macos/Runner/Configs/Debug.xcconfig ================================================ #include "../../Flutter/Flutter-Debug.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: macos/Runner/Configs/Release.xcconfig ================================================ #include "../../Flutter/Flutter-Release.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: macos/Runner/Configs/Warnings.xcconfig ================================================ WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings GCC_WARN_UNDECLARED_SELECTOR = YES CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE CLANG_WARN__DUPLICATE_METHOD_MATCH = YES CLANG_WARN_PRAGMA_PACK = YES CLANG_WARN_STRICT_PROTOTYPES = YES CLANG_WARN_COMMA = YES GCC_WARN_STRICT_SELECTOR_MATCH = YES CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES GCC_WARN_SHADOW = YES CLANG_WARN_UNREACHABLE_CODE = YES ================================================ FILE: macos/Runner/DebugProfile.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.cs.allow-jit com.apple.security.network.server ================================================ FILE: macos/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu NSPrincipalClass NSApplication ================================================ FILE: macos/Runner/MainFlutterWindow.swift ================================================ import Cocoa import FlutterMacOS class MainFlutterWindow: NSWindow { override func awakeFromNib() { let flutterViewController = FlutterViewController.init() let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) RegisterGeneratedPlugins(registry: flutterViewController) super.awakeFromNib() } } ================================================ FILE: macos/Runner/Release.entitlements ================================================ com.apple.security.app-sandbox ================================================ FILE: macos/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { isa = PBXAggregateTarget; buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; buildPhases = ( 33CC111E2044C6BF0003C045 /* ShellScript */, ); dependencies = ( ); name = "Flutter Assemble"; productName = FLX; }; /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ 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 */; }; B7746CD3B58046AB2A30373A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F51BCF4C17559FFBAB4E95D7 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 33CC110E2044A8840003C045 /* Bundle Framework */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 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 /* pikapika.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = pikapika.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 = ""; }; 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; 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 = ""; }; 6DDC9F2D722240B8A73326EB /* 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 = ""; }; 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 = ""; }; B0D4B875C41B50DACC24CB89 /* 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 = ""; }; F2AF3FFEFCDFF4E0D5A2FFB1 /* 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 = ""; }; F51BCF4C17559FFBAB4E95D7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( B7746CD3B58046AB2A30373A /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( 33E5194F232828860026EE4D /* AppInfo.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, ); path = Configs; sourceTree = ""; }; 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, 99812F7FCCD1CE46B3B8E505 /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* pikapika.app */, ); name = Products; sourceTree = ""; }; 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, ); name = Resources; path = ..; sourceTree = ""; }; 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, ); path = Flutter; sourceTree = ""; }; 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, 33CC11242044D66E0003C045 /* Resources */, 33BA886A226E78AF003329D5 /* Configs */, ); path = Runner; sourceTree = ""; }; 99812F7FCCD1CE46B3B8E505 /* Pods */ = { isa = PBXGroup; children = ( F2AF3FFEFCDFF4E0D5A2FFB1 /* Pods-Runner.debug.xcconfig */, B0D4B875C41B50DACC24CB89 /* Pods-Runner.release.xcconfig */, 6DDC9F2D722240B8A73326EB /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( F51BCF4C17559FFBAB4E95D7 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 33CC10EC2044A3C60003C045 /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 410398265F6EBE1BF9230F69 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, B17A392332FC2746FE84EE4D /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* pikapika.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; }; }; }; 33CC111A2044C6BA0003C045 = { CreatedOnToolsVersion = 9.2; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 33CC10E42044A3C60003C045; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( Flutter/ephemeral/FlutterInputs.xcfilelist, ); inputPaths = ( Flutter/ephemeral/tripwire, ); outputFileListPaths = ( Flutter/ephemeral/FlutterOutputs.xcfilelist, ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; 410398265F6EBE1BF9230F69 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); 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"; showEnvVarsInLog = 0; }; B17A392332FC2746FE84EE4D /* [CP] Embed Pods Frameworks */ = { 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"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 33CC10E92044A3C60003C045 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( 33CC10F52044A3C60003C045 /* Base */, ); name = MainMenu.xib; path = Runner; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 338D0CE9231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Profile; }; 338D0CEA231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Profile; }; 338D0CEB231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Profile; }; 33CC10F92044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 33CC10FA2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 33CC10FC2044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 33CC10FD2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; 33CC111C2044C6BA0003C045 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 33CC111D2044C6BA0003C045 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10F92044A3C60003C045 /* Debug */, 33CC10FA2044A3C60003C045 /* Release */, 338D0CE9231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10FC2044A3C60003C045 /* Debug */, 33CC10FD2044A3C60003C045 /* Release */, 338D0CEA231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC111C2044C6BA0003C045 /* Debug */, 33CC111D2044C6BA0003C045 /* Release */, 338D0CEB231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } ================================================ FILE: macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: macos/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: pubspec.yaml ================================================ name: pikapika description: A cross platform comic client. publish_to: 'none' version: 1.8.19+43 environment: sdk: ">=2.12.0 <3.0.0" dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 isolate: ^2.1.1 event: ^2.0.5 flutter_svg: ^1.0.3 flutter_styled_toast: 2.0.0 another_xlider: ^1.0.1+2 scrollable_positioned_list: ^0.2.0-nullsafety.0 permission_handler: ^10.1.0 url_launcher: ^6.0.9 clipboard: ^0.1.3 photo_view: 0.14.0 multi_select_flutter: ^4.0.0 # flutter_datetime_picker: 1.5.1 modal_bottom_sheet: ^3.0.0-pre image_cropper: 1.5.0 image_picker: ^0.8.6 file_picker: 5.2.5 crop_image: 1.0.2 image: ^3.1.3 path: ^1.8.0 uri_to_file: ^0.2.0 uni_links: ^0.5.1 filesystem_picker: ^3.0.0-beta.1 easy_localization: ^3.0.7+1 zoomable_positioned_list: 1.0.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^1.0.0 flutter: uses-material-design: true assets: - lib/assets/ - lib/assets/translations/ ================================================ FILE: scripts/README.md ================================================ 用于记录作者构建时使用的脚本 ================================================ FILE: scripts/bind-android-arm64.sh ================================================ cd "$( cd "$( dirname "$0" )" && pwd )/.." cd go/mobile gomobile init gomobile bind -androidapi 21 -target=android/arm64 -o lib/Mobile.aar ./ ================================================ FILE: scripts/bind-android-debug.sh ================================================ # 编译所有架构的依赖 cd "$( cd "$( dirname "$0" )" && pwd )/.." cd go/mobile gomobile init gomobile bind -androidapi 21 -target=android/arm,android/arm64,android/386,android/amd64 -o lib/Mobile.aar ./ ================================================ FILE: scripts/bind-ios-arm64.sh ================================================ cd "$( cd "$( dirname "$0" )" && pwd )/.." cd go/mobile gomobile init gomobile bind -iosversion 11.0 -target=ios -o lib/Mobile.xcframework ./ ================================================ FILE: scripts/bind-ios.sh ================================================ # 编译所有架构的依赖 cd "$( cd "$( dirname "$0" )" && pwd )/.." cd go/mobile gomobile init gomobile bind -iosversion 11.0 -target=ios -o lib/Mobile.xcframework ./ ================================================ FILE: scripts/build-apk-arm.sh ================================================ # 仅构建arm的APK cd "$( cd "$( dirname "$0" )" && pwd )/.." cd go/mobile gomobile init gomobile bind -androidapi 21 -target=android/arm -o lib/Mobile.aar ./ cd ../.. flutter build apk --target-platform android-arm ================================================ FILE: scripts/build-apk-arm64.sh ================================================ # 仅构建arm64的APK cd "$( cd "$( dirname "$0" )" && pwd )/.." cd go/mobile gomobile init gomobile bind -androidapi 21 -target=android/arm64 -o lib/Mobile.aar ./ cd ../.. flutter build apk --target-platform android-arm64 ================================================ FILE: scripts/build-apk-x64.sh ================================================ # 仅构建x86_64的APK cd "$( cd "$( dirname "$0" )" && pwd )/.." cd go/mobile gomobile init gomobile bind -androidapi 21 -target=android/amd64 -o lib/Mobile.aar ./ cd ../.. flutter build apk --target-platform android-x64 ================================================ FILE: scripts/build-apk-x86.sh ================================================ # 仅构建x86的APK cd "$( cd "$( dirname "$0" )" && pwd )/.." cd go/mobile gomobile init gomobile bind -androidapi 21 -target=android/386 -o lib/Mobile.aar ./ cd ../.. flutter build apk --target-platform android-x86 ================================================ FILE: scripts/build-ipa.sh ================================================ # 构建未签名的IPA cd "$( cd "$( dirname "$0" )" && pwd )/.." cd go/mobile gomobile init gomobile bind -iosversion 11.0 -target=ios -o lib/Mobile.xcframework ./ cd ../.. flutter build ios --release --no-codesign cd build mkdir -p Payload mv ios/iphoneos/Runner.app Payload sh ../scripts/thin-payload.sh zip -9 nosign.ipa -r Payload ================================================ FILE: scripts/build-linux.sh ================================================ #!/usr/bin/env bash curl -JOL https://github.com/junmer/source-han-serif-ttf/raw/master/SubsetTTF/CN/SourceHanSerifCN-Regular.ttf mkdir -p fonts mv SourceHanSerifCN-Regular.ttf fonts/Roboto.ttf cat ci/linux_font.yaml >> pubspec.yaml hover build linux-appimage mv go/build/outputs/linux-appimage-release/*.AppImage build/build.AppImage ================================================ FILE: scripts/build-macos-dmg.sh ================================================ # 构建macos cd "$( cd "$( dirname "$0" )" && pwd )/.." hover build darwin-dmg ================================================ FILE: scripts/json_compairer.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import json import os import sys import argparse from pathlib import Path from typing import Dict, List, Set, Any, Union from datetime import datetime def flatten_json(data: Dict[str, Any], parent_key: str = '', separator: str = '.') -> Dict[str, Any]: """ 将嵌套的JSON对象扁平化为点分隔的键值对 Args: data: 要扁平化的JSON数据 parent_key: 父级键名 separator: 键名分隔符 Returns: 扁平化后的字典 """ items = [] for key, value in data.items(): new_key = f"{parent_key}{separator}{key}" if parent_key else key if isinstance(value, dict): items.extend(flatten_json(value, new_key, separator).items()) else: items.append((new_key, value)) return dict(items) def unflatten_json(data: Dict[str, Any], separator: str = '.') -> Dict[str, Any]: """ 将扁平化的JSON对象还原为嵌套结构 Args: data: 扁平化的JSON数据 separator: 键名分隔符 Returns: 嵌套的JSON对象 """ result = {} for key, value in data.items(): parts = key.split(separator) current = result for part in parts[:-1]: if part not in current: current[part] = {} current = current[part] current[parts[-1]] = value return result def load_json_file(file_path: str) -> Dict[str, Any]: """ 加载JSON文件 Args: file_path: JSON文件路径 Returns: JSON数据字典 """ try: with open(file_path, 'r', encoding='utf-8') as f: return json.load(f) except FileNotFoundError: print(f"❌ 文件不存在: {file_path}") return {} except json.JSONDecodeError as e: print(f"❌ JSON解析错误 {file_path}: {e}") return {} except Exception as e: print(f"❌ 读取文件错误 {file_path}: {e}") return {} def save_json_file(file_path: str, data: Dict[str, Any]) -> bool: """ 保存JSON文件 Args: file_path: JSON文件路径 data: 要保存的数据 Returns: 是否保存成功 """ try: with open(file_path, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) return True except Exception as e: print(f"❌ 保存文件错误 {file_path}: {e}") return False def get_all_json_files(translations_dir: str) -> List[str]: """ 获取翻译目录下的所有JSON文件 Args: translations_dir: 翻译文件目录 Returns: JSON文件路径列表 """ json_files = [] if os.path.exists(translations_dir): for file in os.listdir(translations_dir): if file.endswith('.json'): json_files.append(os.path.join(translations_dir, file)) return sorted(json_files) def find_reference_value(key: str, all_data: Dict[str, Dict[str, Any]]) -> str: """ 为缺失的键找到参考值 Args: key: 缺失的键 all_data: 所有翻译数据 Returns: 参考值或占位符 """ # 首先尝试从其他文件中找到这个键的值 for filename, data in all_data.items(): flattened = flatten_json(data) if key in flattened: return flattened[key] # 如果没找到,生成一个占位符 key_parts = key.split('.') last_part = key_parts[-1] # 根据键名生成合理的占位符 placeholders = { 'title': 'Title', 'name': 'Name', 'description': 'Description', 'hint': 'Hint', 'label': 'Label', 'placeholder': 'Placeholder', 'button': 'Button', 'confirm': 'Confirm', 'cancel': 'Cancel', 'yes': 'Yes', 'no': 'No', 'loading': 'Loading...', 'error': 'Error', 'success': 'Success', 'failed': 'Failed', 'warning': 'Warning', } for placeholder_key, placeholder_value in placeholders.items(): if placeholder_key in last_part.lower(): return placeholder_value # 默认占位符 return f"TODO: {last_part.replace('_', ' ').title()}" def generate_missing_keys_report(translations_dir: str, output_file: Union[str, None] = None): """ 生成缺失键的报告 Args: translations_dir: 翻译文件目录 output_file: 输出文件路径 """ json_files = get_all_json_files(translations_dir) if not json_files: print(f"❌ 在目录 {translations_dir} 中没有找到JSON文件") return # 加载所有JSON文件 all_data = {} all_keys = {} for file_path in json_files: filename = os.path.basename(file_path) data = load_json_file(file_path) if data: all_data[filename] = data all_keys[filename] = set(flatten_json(data).keys()) if not all_data: print("❌ 没有成功加载任何JSON文件") return # 找出所有键的并集 all_possible_keys = set() for keys in all_keys.values(): all_possible_keys.update(keys) # 生成报告 report = [] report.append(f"# 翻译文件缺失键报告") report.append(f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") report.append(f"翻译文件目录: {translations_dir}") report.append(f"总键数: {len(all_possible_keys)}") report.append("") # 统计信息 report.append("## 统计信息") report.append("| 文件名 | 键数量 | 缺失键数量 |") report.append("|--------|--------|------------|") for filename, keys in all_keys.items(): missing_count = len(all_possible_keys - keys) report.append(f"| {filename} | {len(keys)} | {missing_count} |") report.append("") # 详细缺失信息 report.append("## 缺失键详情") for filename, keys in all_keys.items(): missing_keys = all_possible_keys - keys if missing_keys: report.append(f"### {filename}") report.append(f"缺失 {len(missing_keys)} 个键:") for key in sorted(missing_keys): report.append(f"- `{key}`") report.append("") # 输出报告 report_content = "\n".join(report) if output_file: try: with open(output_file, 'w', encoding='utf-8') as f: f.write(report_content) print(f"✅ 报告已保存到: {output_file}") except Exception as e: print(f"❌ 保存报告失败: {e}") else: print(report_content) def fix_missing_keys(translations_dir: str, dry_run: bool = True): """ 修复缺失的键 Args: translations_dir: 翻译文件目录 dry_run: 是否为试运行模式 """ json_files = get_all_json_files(translations_dir) if not json_files: print(f"❌ 在目录 {translations_dir} 中没有找到JSON文件") return # 加载所有JSON文件 all_data = {} all_keys = {} for file_path in json_files: filename = os.path.basename(file_path) data = load_json_file(file_path) if data: all_data[filename] = data all_keys[filename] = set(flatten_json(data).keys()) if not all_data: print("❌ 没有成功加载任何JSON文件") return # 找出所有键的并集 all_possible_keys = set() for keys in all_keys.values(): all_possible_keys.update(keys) # 修复每个文件的缺失键 for file_path in json_files: filename = os.path.basename(file_path) if filename not in all_data: continue missing_keys = all_possible_keys - all_keys[filename] if not missing_keys: print(f"✅ {filename} 无需修复") continue print(f"🔧 {'[试运行] ' if dry_run else ''}修复 {filename} 的 {len(missing_keys)} 个缺失键:") # 获取当前文件的扁平化数据 flattened_data = flatten_json(all_data[filename]) # 添加缺失的键 for key in sorted(missing_keys): reference_value = find_reference_value(key, all_data) flattened_data[key] = reference_value print(f" + {key} = \"{reference_value}\"") # 如果不是试运行,保存文件 if not dry_run: # 将扁平化数据还原为嵌套结构 nested_data = unflatten_json(flattened_data) # 保存文件 if save_json_file(file_path, nested_data): print(f" ✅ 已保存 {filename}") else: print(f" ❌ 保存 {filename} 失败") print() def compare_translation_files(translations_dir: str): """ 比较翻译文件,找出缺失的键 Args: translations_dir: 翻译文件目录 """ json_files = get_all_json_files(translations_dir) if not json_files: print(f"❌ 在目录 {translations_dir} 中没有找到JSON文件") return print(f"🔍 找到 {len(json_files)} 个翻译文件:") for file in json_files: print(f" - {os.path.basename(file)}") print() # 加载所有JSON文件 all_data = {} all_keys = {} for file_path in json_files: filename = os.path.basename(file_path) data = load_json_file(file_path) if data: all_data[filename] = data all_keys[filename] = set(flatten_json(data).keys()) else: print(f"⚠️ 跳过空文件: {filename}") if not all_data: print("❌ 没有成功加载任何JSON文件") return # 找出所有键的并集 all_possible_keys = set() for keys in all_keys.values(): all_possible_keys.update(keys) print(f"📊 总共发现 {len(all_possible_keys)} 个唯一键") print() # 检查每个文件缺失的键 has_missing_keys = False for filename, keys in all_keys.items(): missing_keys = all_possible_keys - keys if missing_keys: has_missing_keys = True print(f"❌ {filename} 缺失 {len(missing_keys)} 个键:") for key in sorted(missing_keys): print(f" - {key}") print() else: print(f"✅ {filename} 包含所有键") if not has_missing_keys: print("🎉 所有翻译文件都包含相同的键!") return # 显示键统计信息 print("\n📈 键统计信息:") print(f"{'文件名':<20} {'键数量':<10} {'缺失键数量':<12}") print("-" * 45) for filename, keys in all_keys.items(): missing_count = len(all_possible_keys - keys) print(f"{filename:<20} {len(keys):<10} {missing_count:<12}") # 找出只在某些文件中存在的键 print("\n🔍 键分布分析:") key_distribution = {} for key in all_possible_keys: files_with_key = [filename for filename, keys in all_keys.items() if key in keys] key_distribution[key] = files_with_key # 找出不在所有文件中的键 incomplete_keys = {key: files for key, files in key_distribution.items() if len(files) < len(all_keys)} if incomplete_keys: print(f"发现 {len(incomplete_keys)} 个键不在所有文件中:") for key, files in sorted(incomplete_keys.items()): missing_files = [f for f in all_keys.keys() if f not in files] print(f" {key}") print(f" 存在于: {', '.join(files)}") print(f" 缺失于: {', '.join(missing_files)}") print() else: print("所有键都在所有文件中存在!") def main(): """主函数""" parser = argparse.ArgumentParser(description='比较翻译文件中的键,找出缺失的键') parser.add_argument('translations_dir', nargs='?', help='翻译文件目录路径 (默认: ../lib/assets/translations)') parser.add_argument('--report', '-r', metavar='FILE', help='生成报告并保存到指定文件') parser.add_argument('--fix', '-f', action='store_true', help='修复缺失的键') parser.add_argument('--dry-run', '-d', action='store_true', help='试运行模式,不实际修改文件') args = parser.parse_args() # 获取脚本所在目录 script_dir = os.path.dirname(os.path.abspath(__file__)) # 翻译文件目录路径 if args.translations_dir: translations_dir = args.translations_dir else: translations_dir = os.path.join(script_dir, '..', 'lib', 'assets', 'translations') translations_dir = os.path.normpath(translations_dir) print(f"🚀 开始比较翻译文件...") print(f"📁 翻译文件目录: {translations_dir}") print() if not os.path.exists(translations_dir): print(f"❌ 翻译文件目录不存在: {translations_dir}") sys.exit(1) if args.report: generate_missing_keys_report(translations_dir, args.report) elif args.fix: fix_missing_keys(translations_dir, dry_run=args.dry_run) else: compare_translation_files(translations_dir) if __name__ == "__main__": main() ================================================ FILE: scripts/sign-apk-github-actions.sh ================================================ cd "$( cd "$( dirname "$0" )" && pwd )/.." echo $KEY_FILE_BASE64 > key.jks.base64 base64 -d key.jks.base64 > key.jks echo $KEY_PASSWORD | $ANDROID_HOME/build-tools/30.0.2/apksigner sign --ks key.jks build/app/outputs/flutter-apk/app-release.apk ================================================ FILE: scripts/thin-payload.sh ================================================ # 精简Payload文件夹 (上传到AppStore会自动区分平台, 此代码仅用于构建非签名ipa) foreachThin(){ for file in $1/* do if test -f $file then mime=$(file --mime-type -b $file) if [ "$mime" == 'application/x-mach-binary' ] || [ "${file##*.}"x = "dylib"x ] then echo thin $file xcrun -sdk iphoneos lipo "$file" -thin arm64 -output "$file" xcrun -sdk iphoneos bitcode_strip "$file" -r -o "$file" strip -S -x "$file" -o "$file" fi fi if test -d $file then foreachThin $file fi done } foreachThin ./Payload ================================================ FILE: scripts/version.sh ================================================ # 设置版本号 cd "$( cd "$( dirname "$0" )" && pwd )/.." if [ "$1" == "set" ] ; then if [ "$2" != "" ] ; then echo $2 > lib/assets/version.txt fi elif [ "$1" == "unset" ]; then rm -f lib/assets/version.txt fi ================================================ FILE: test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility that Flutter provides. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pikapika/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(const PikapikaApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Tap the '+' icon and trigger a frame. await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify that our counter has incremented. expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); } ================================================ FILE: windows/.gitignore ================================================ flutter/ephemeral/ # Visual Studio user-specific files. *.suo *.user *.userosscache *.sln.docstates # Visual Studio build-related files. x64/ x86/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ ================================================ FILE: windows/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.15) project(pikapika LANGUAGES CXX) set(BINARY_NAME "pikapika") cmake_policy(SET CMP0063 NEW) set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") # Configure build options. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" CACHE STRING "" FORCE) else() if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() endif() set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") # Use Unicode for all projects. add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") # Flutter library and tool build rules. add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build add_subdirectory("runner") # Generated plugin build rules, which manage building the plugins and adding # them to the application. 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 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) 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}") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) ================================================ FILE: windows/flutter/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.15) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "flutter_export.h" "flutter_windows.h" "flutter_messenger.h" "flutter_plugin_registrar.h" "flutter_texture_registrar.h" ) list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") add_dependencies(flutter flutter_assemble) # === Wrapper === list(APPEND CPP_WRAPPER_SOURCES_CORE "core_implementations.cc" "standard_codec.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_PLUGIN "plugin_registrar.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_APP "flutter_engine.cc" "flutter_view_controller.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") # Wrapper sources needed for a plugin. add_library(flutter_wrapper_plugin STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ) apply_standard_settings(flutter_wrapper_plugin) set_target_properties(flutter_wrapper_plugin PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(flutter_wrapper_plugin PROPERTIES CXX_VISIBILITY_PRESET hidden) target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) target_include_directories(flutter_wrapper_plugin PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_plugin flutter_assemble) # Wrapper sources needed for the runner. add_library(flutter_wrapper_app STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_APP} ) apply_standard_settings(flutter_wrapper_app) target_link_libraries(flutter_wrapper_app PUBLIC flutter) target_include_directories(flutter_wrapper_app PUBLIC "${WRAPPER_ROOT}/include" ) 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 from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ${PHONY_OUTPUT} COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" windows-x64 $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ) ================================================ FILE: windows/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } ================================================ FILE: windows/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void RegisterPlugins(flutter::PluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: windows/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST permission_handler_windows url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: windows/runner/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.15) project(runner LANGUAGES CXX) add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" "run_loop.cpp" "utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) apply_standard_settings(${BINARY_NAME}) target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") add_dependencies(${BINARY_NAME} flutter_assemble) ================================================ FILE: windows/runner/Runner.rc ================================================ // Microsoft Visual C++ generated resource script. // #pragma code_page(65001) #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (United States) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APP_ICON ICON "resources\\app_icon.ico" ///////////////////////////////////////////////////////////////////////////// // // Version // #ifdef FLUTTER_BUILD_NUMBER #define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER #else #define VERSION_AS_NUMBER 1,0,0 #endif #ifdef FLUTTER_BUILD_NAME #define VERSION_AS_STRING #FLUTTER_BUILD_NAME #else #define VERSION_AS_STRING "1.0.0" #endif VS_VERSION_INFO VERSIONINFO FILEVERSION VERSION_AS_NUMBER PRODUCTVERSION VERSION_AS_NUMBER FILEFLAGSMASK VS_FFI_FILEFLAGSMASK #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG #else FILEFLAGS 0x0L #endif FILEOS VOS__WINDOWS32 FILETYPE VFT_APP FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" VALUE "FileDescription", "A new Flutter project." "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "pikapika" "\0" VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0" VALUE "OriginalFilename", "pikapika.exe" "\0" VALUE "ProductName", "pikapika" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1252 END END #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED ================================================ FILE: windows/runner/flutter_window.cpp ================================================ #include "flutter_window.h" #include #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(RunLoop* run_loop, const flutter::DartProject& project) : run_loop_(run_loop), project_(project) {} FlutterWindow::~FlutterWindow() {} bool FlutterWindow::OnCreate() { if (!Win32Window::OnCreate()) { return false; } RECT frame = GetClientArea(); // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); flutter_controller_ = nullptr; } Win32Window::OnDestroy(); } LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); if (result) { return *result; } } switch (message) { case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); } ================================================ FILE: windows/runner/flutter_window.h ================================================ #ifndef RUNNER_FLUTTER_WINDOW_H_ #define RUNNER_FLUTTER_WINDOW_H_ #include #include #include #include "run_loop.h" #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: // Creates a new FlutterWindow driven by the |run_loop|, hosting a // Flutter view running |project|. explicit FlutterWindow(RunLoop* run_loop, const flutter::DartProject& project); virtual ~FlutterWindow(); protected: // Win32Window: bool OnCreate() override; void OnDestroy() override; LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; private: // The run loop driving events for this window. RunLoop* run_loop_; // The project to run. flutter::DartProject project_; // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; }; #endif // RUNNER_FLUTTER_WINDOW_H_ ================================================ FILE: windows/runner/main.cpp ================================================ #include #include #include #include "flutter_window.h" #include "run_loop.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { CreateAndAttachConsole(); } // Initialize COM, so that it is available for use in the library and/or // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); RunLoop run_loop; flutter::DartProject project(L"data"); std::vector command_line_arguments = GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(&run_loop, project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); if (!window.CreateAndShow(L"pikapika", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); run_loop.Run(); ::CoUninitialize(); return EXIT_SUCCESS; } ================================================ FILE: windows/runner/resource.h ================================================ //{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by Runner.rc // #define IDI_APP_ICON 101 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif ================================================ FILE: windows/runner/run_loop.cpp ================================================ #include "run_loop.h" #include #include RunLoop::RunLoop() {} RunLoop::~RunLoop() {} void RunLoop::Run() { bool keep_running = true; TimePoint next_flutter_event_time = TimePoint::clock::now(); while (keep_running) { std::chrono::nanoseconds wait_duration = std::max(std::chrono::nanoseconds(0), next_flutter_event_time - TimePoint::clock::now()); ::MsgWaitForMultipleObjects( 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), QS_ALLINPUT); bool processed_events = false; MSG message; // All pending Windows messages must be processed; MsgWaitForMultipleObjects // won't return again for items left in the queue after PeekMessage. while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { processed_events = true; if (message.message == WM_QUIT) { keep_running = false; break; } ::TranslateMessage(&message); ::DispatchMessage(&message); // Allow Flutter to process messages each time a Windows message is // processed, to prevent starvation. next_flutter_event_time = std::min(next_flutter_event_time, ProcessFlutterMessages()); } // If the PeekMessage loop didn't run, process Flutter messages. if (!processed_events) { next_flutter_event_time = std::min(next_flutter_event_time, ProcessFlutterMessages()); } } } void RunLoop::RegisterFlutterInstance( flutter::FlutterEngine* flutter_instance) { flutter_instances_.insert(flutter_instance); } void RunLoop::UnregisterFlutterInstance( flutter::FlutterEngine* flutter_instance) { flutter_instances_.erase(flutter_instance); } RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { TimePoint next_event_time = TimePoint::max(); for (auto instance : flutter_instances_) { std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); if (wait_duration != std::chrono::nanoseconds::max()) { next_event_time = std::min(next_event_time, TimePoint::clock::now() + wait_duration); } } return next_event_time; } ================================================ FILE: windows/runner/run_loop.h ================================================ #ifndef RUNNER_RUN_LOOP_H_ #define RUNNER_RUN_LOOP_H_ #include #include #include // A runloop that will service events for Flutter instances as well // as native messages. class RunLoop { public: RunLoop(); ~RunLoop(); // Prevent copying RunLoop(RunLoop const&) = delete; RunLoop& operator=(RunLoop const&) = delete; // Runs the run loop until the application quits. void Run(); // Registers the given Flutter instance for event servicing. void RegisterFlutterInstance( flutter::FlutterEngine* flutter_instance); // Unregisters the given Flutter instance from event servicing. void UnregisterFlutterInstance( flutter::FlutterEngine* flutter_instance); private: using TimePoint = std::chrono::steady_clock::time_point; // Processes all currently pending messages for registered Flutter instances. TimePoint ProcessFlutterMessages(); std::set flutter_instances_; }; #endif // RUNNER_RUN_LOOP_H_ ================================================ FILE: windows/runner/runner.exe.manifest ================================================ PerMonitorV2 ================================================ FILE: windows/runner/utils.cpp ================================================ #include "utils.h" #include #include #include #include #include void CreateAndAttachConsole() { if (::AllocConsole()) { FILE *unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } if (freopen_s(&unused, "CONOUT$", "w", stderr)) { _dup2(_fileno(stdout), 2); } std::ios::sync_with_stdio(); FlutterDesktopResyncOutputStreams(); } } std::vector GetCommandLineArguments() { // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. int argc; wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); if (argv == nullptr) { return std::vector(); } std::vector command_line_arguments; // Skip the first argument as it's the binary name. for (int i = 1; i < argc; i++) { command_line_arguments.push_back(Utf8FromUtf16(argv[i])); } ::LocalFree(argv); return command_line_arguments; } std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr); if (target_length == 0) { return std::string(); } std::string utf8_string; utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } return utf8_string; } ================================================ FILE: windows/runner/utils.h ================================================ #ifndef RUNNER_UTILS_H_ #define RUNNER_UTILS_H_ #include #include // Creates a console for the process, and redirects stdout and stderr to // it for both the runner and the Flutter library. void CreateAndAttachConsole(); // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string // encoded in UTF-8. Returns an empty std::string on failure. std::string Utf8FromUtf16(const wchar_t* utf16_string); // Gets the command line arguments passed in as a std::vector, // encoded in UTF-8. Returns an empty std::vector on failure. std::vector GetCommandLineArguments(); #endif // RUNNER_UTILS_H_ ================================================ FILE: windows/runner/win32_window.cpp ================================================ #include "win32_window.h" #include #include "resource.h" namespace { constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); // Scale helper to convert logical scaler values to physical using passed in // scale factor int Scale(int source, double scale_factor) { return static_cast(source * scale_factor); } // 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"); if (!user32_module) { return; } auto enable_non_client_dpi_scaling = reinterpret_cast( GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); FreeLibrary(user32_module); } } } // namespace // Manages the Win32Window's window class registration. class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; // Returns the singleton registar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); } return instance_; } // Returns the name of the window class, registering the class if it hasn't // previously been registered. const wchar_t* GetWindowClass(); // Unregisters the window class. Should only be called if there are no // instances of the window. void UnregisterWindowClass(); private: WindowClassRegistrar() = default; static WindowClassRegistrar* instance_; bool class_registered_ = false; }; WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; const wchar_t* WindowClassRegistrar::GetWindowClass() { if (!class_registered_) { WNDCLASS window_class{}; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; window_class.style = CS_HREDRAW | CS_VREDRAW; window_class.cbClsExtra = 0; window_class.cbWndExtra = 0; window_class.hInstance = GetModuleHandle(nullptr); window_class.hIcon = LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); window_class.hbrBackground = 0; window_class.lpszMenuName = nullptr; window_class.lpfnWndProc = Win32Window::WndProc; RegisterClass(&window_class); class_registered_ = true; } return kWindowClassName; } void WindowClassRegistrar::UnregisterWindowClass() { UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; } Win32Window::Win32Window() { ++g_active_window_count; } Win32Window::~Win32Window() { --g_active_window_count; Destroy(); } bool Win32Window::CreateAndShow(const std::wstring& title, const Point& origin, const Size& size) { Destroy(); const wchar_t* window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), static_cast(origin.y)}; HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); if (!window) { return false; } return OnCreate(); } // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { if (message == WM_NCCREATE) { auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; } else if (Win32Window* that = GetThisFromHandle(window)) { return that->MessageHandler(window, message, wparam, lparam); } return DefWindowProc(window, message, wparam, lparam); } LRESULT Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { switch (message) { case WM_DESTROY: window_handle_ = nullptr; Destroy(); if (quit_on_close_) { PostQuitMessage(0); } return 0; case WM_DPICHANGED: { auto newRectSize = reinterpret_cast(lparam); LONG newWidth = newRectSize->right - newRectSize->left; LONG newHeight = newRectSize->bottom - newRectSize->top; SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, newHeight, SWP_NOZORDER | SWP_NOACTIVATE); return 0; } case WM_SIZE: { RECT rect = GetClientArea(); if (child_content_ != nullptr) { // Size and position the child window. MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE); } return 0; } case WM_ACTIVATE: if (child_content_ != nullptr) { SetFocus(child_content_); } return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); } void Win32Window::Destroy() { OnDestroy(); if (window_handle_) { DestroyWindow(window_handle_); window_handle_ = nullptr; } if (g_active_window_count == 0) { WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); } } Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } void Win32Window::SetChildContent(HWND content) { child_content_ = content; SetParent(content, window_handle_); RECT frame = GetClientArea(); MoveWindow(content, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, true); SetFocus(child_content_); } RECT Win32Window::GetClientArea() { RECT frame; GetClientRect(window_handle_, &frame); return frame; } HWND Win32Window::GetHandle() { return window_handle_; } void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; } bool Win32Window::OnCreate() { // No-op; provided for subclasses. return true; } void Win32Window::OnDestroy() { // No-op; provided for subclasses. } ================================================ FILE: windows/runner/win32_window.h ================================================ #ifndef RUNNER_WIN32_WINDOW_H_ #define RUNNER_WIN32_WINDOW_H_ #include #include #include #include // A class abstraction for a high DPI-aware Win32 Window. Intended to be // inherited from by classes that wish to specialize with custom // rendering and input handling class Win32Window { public: struct Point { unsigned int x; unsigned int y; Point(unsigned int x, unsigned int y) : x(x), y(y) {} }; struct Size { unsigned int width; unsigned int height; Size(unsigned int width, unsigned int height) : width(width), height(height) {} }; Win32Window(); virtual ~Win32Window(); // Creates and shows a win32 window with |title| and position and size using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a // consistent size to will treat the width height passed in to this function // as logical pixels and scale to appropriate for the default monitor. Returns // true if the window was created successfully. bool CreateAndShow(const std::wstring& title, const Point& origin, const Size& size); // Release OS resources associated with window. void Destroy(); // Inserts |content| into the window tree. void SetChildContent(HWND content); // Returns the backing Window handle to enable clients to set icon and other // window properties. Returns nullptr if the window has been destroyed. HWND GetHandle(); // If true, closing this window will quit the application. void SetQuitOnClose(bool quit_on_close); // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. virtual LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Called when CreateAndShow is called, allowing subclass window-related // setup. Subclasses should return false if setup fails. virtual bool OnCreate(); // Called when Destroy is called. virtual void OnDestroy(); private: friend class WindowClassRegistrar; // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically // responsponds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Retrieves a class instance pointer for |window| static Win32Window* GetThisFromHandle(HWND const window) noexcept; bool quit_on_close_ = false; // window handle for top level window. HWND window_handle_ = nullptr; // window handle for hosted content. HWND child_content_ = nullptr; }; #endif // RUNNER_WIN32_WINDOW_H_