├── .github └── workflows │ └── build.yml ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle.kts │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── github │ │ │ │ └── raoxwup │ │ │ │ └── haka_comic │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ ├── values │ │ │ └── styles.xml │ │ │ └── xml │ │ │ └── network_security_config.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle.kts ├── assets ├── fonts │ ├── LXGWWenKaiLite-Medium.ttf │ └── LXGWWenKaiLite-Regular.ttf ├── icons │ ├── android │ │ └── icon.png │ ├── ios │ │ ├── Dark.png │ │ ├── Light.png │ │ └── Tinted.png │ ├── macos │ │ ├── Icon-1024.png │ │ ├── Icon-128.png │ │ ├── Icon-256.png │ │ ├── Icon-32.png │ │ ├── Icon-512.png │ │ └── Icon-64.png │ ├── pc │ │ ├── macos_icon.png │ │ ├── windows_icon.ico │ │ └── windows_icon.png │ └── splash │ │ ├── splash-icon-dark.png │ │ └── splash-icon-light.png └── images │ ├── forum.jpg │ ├── icon_empty.png │ ├── icon_exclamation_error.png │ ├── icon_leave.png │ ├── icon_no_comment.png │ ├── icon_success.png │ ├── icon_unknown_error.png │ ├── latest.jpg │ ├── leaderboard.jpg │ ├── loading-zip.gif │ ├── loading.gif │ ├── login.png │ ├── profile_bg.png │ ├── random.jpg │ └── user.png ├── devtools_options.yaml ├── flutter_launcher_icons.yaml ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── 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 ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-38x38@2x.png │ │ │ ├── Icon-App-38x38@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-64x64@2x.png │ │ │ ├── Icon-App-64x64@3x.png │ │ │ ├── Icon-App-68x68@2x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ ├── Icon-App-Dark-1024x1024@1x.png │ │ │ ├── Icon-App-Dark-20x20@2x.png │ │ │ ├── Icon-App-Dark-20x20@3x.png │ │ │ ├── Icon-App-Dark-29x29@2x.png │ │ │ ├── Icon-App-Dark-29x29@3x.png │ │ │ ├── Icon-App-Dark-38x38@2x.png │ │ │ ├── Icon-App-Dark-38x38@3x.png │ │ │ ├── Icon-App-Dark-40x40@2x.png │ │ │ ├── Icon-App-Dark-40x40@3x.png │ │ │ ├── Icon-App-Dark-60x60@2x.png │ │ │ ├── Icon-App-Dark-60x60@3x.png │ │ │ ├── Icon-App-Dark-64x64@2x.png │ │ │ ├── Icon-App-Dark-64x64@3x.png │ │ │ ├── Icon-App-Dark-68x68@2x.png │ │ │ ├── Icon-App-Dark-76x76@2x.png │ │ │ ├── Icon-App-Dark-83.5x83.5@2x.png │ │ │ ├── Icon-App-Tinted-1024x1024@1x.png │ │ │ ├── Icon-App-Tinted-20x20@2x.png │ │ │ ├── Icon-App-Tinted-20x20@3x.png │ │ │ ├── Icon-App-Tinted-29x29@2x.png │ │ │ ├── Icon-App-Tinted-29x29@3x.png │ │ │ ├── Icon-App-Tinted-38x38@2x.png │ │ │ ├── Icon-App-Tinted-38x38@3x.png │ │ │ ├── Icon-App-Tinted-40x40@2x.png │ │ │ ├── Icon-App-Tinted-40x40@3x.png │ │ │ ├── Icon-App-Tinted-60x60@2x.png │ │ │ ├── Icon-App-Tinted-60x60@3x.png │ │ │ ├── Icon-App-Tinted-64x64@2x.png │ │ │ ├── Icon-App-Tinted-64x64@3x.png │ │ │ ├── Icon-App-Tinted-68x68@2x.png │ │ │ ├── Icon-App-Tinted-76x76@2x.png │ │ │ └── Icon-App-Tinted-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h └── RunnerTests │ └── RunnerTests.swift ├── lib ├── config │ ├── app_config.dart │ └── setup_config.dart ├── database │ ├── download_task_helper.dart │ ├── history_helper.dart │ ├── images_helper.dart │ ├── read_record_helper.dart │ ├── tag_block_helper.dart │ └── word_block_helper.dart ├── main.dart ├── mixin │ ├── auto_register_handler.dart │ ├── blocked_words.dart │ └── pagination_handler.dart ├── model │ ├── reader_provider.dart │ ├── search_provider.dart │ ├── theme_provider.dart │ └── user_provider.dart ├── network │ ├── cache.dart │ ├── client.dart │ ├── http.dart │ ├── http_client.dart │ ├── models.dart │ ├── models.g.dart │ └── utils.dart ├── router │ ├── app_router.dart │ └── aware_page_wrapper.dart ├── startup_prepare.dart ├── utils │ ├── common.dart │ ├── download_manager.dart │ ├── extension.dart │ ├── loader.dart │ ├── log.dart │ ├── shared_preferences_util.dart │ ├── ui.dart │ └── version.dart ├── views │ ├── about │ │ └── about.dart │ ├── categories │ │ └── categories.dart │ ├── comic_details │ │ ├── chapters_list.dart │ │ ├── collect_action.dart │ │ ├── comic_details.dart │ │ ├── comic_share_id.dart │ │ ├── creator.dart │ │ ├── downloader.dart │ │ ├── icon_text.dart │ │ ├── liked_action.dart │ │ └── recommendation.dart │ ├── comics │ │ ├── comics.dart │ │ ├── common_tmi_list.dart │ │ ├── list_item.dart │ │ ├── page_selector.dart │ │ ├── simple_list_item.dart │ │ ├── sort_type_selector.dart │ │ └── tmi_list.dart │ ├── comments │ │ ├── comment_input.dart │ │ ├── comments.dart │ │ ├── sub_comments.dart │ │ └── thumb_up.dart │ ├── home │ │ ├── home.dart │ │ ├── navigation.dart │ │ └── share_dialog.dart │ ├── login │ │ ├── login.dart │ │ └── register.dart │ ├── mine │ │ ├── comments │ │ │ ├── comments.dart │ │ │ └── sub_comments.dart │ │ ├── downloads.dart │ │ ├── editor.dart │ │ ├── favorites.dart │ │ ├── history.dart │ │ └── mine.dart │ ├── notifications │ │ └── notifications.dart │ ├── random │ │ └── random.dart │ ├── rank │ │ └── rank.dart │ ├── reader │ │ ├── app_bar.dart │ │ ├── bottom.dart │ │ ├── comic_list_mixin.dart │ │ ├── next_chapter.dart │ │ ├── page_no_tag.dart │ │ ├── reader.dart │ │ └── widget │ │ │ ├── comic_image.dart │ │ │ ├── horizontal_list │ │ │ └── horizontal_list.dart │ │ │ └── vertical_list │ │ │ ├── gesture.dart │ │ │ └── vertical_list.dart │ ├── search │ │ ├── hot_search_words.dart │ │ ├── item.dart │ │ ├── search.dart │ │ ├── search_comics.dart │ │ ├── search_history.dart │ │ ├── search_list_item.dart │ │ └── simple_search_list_item.dart │ └── settings │ │ ├── blacklist.dart │ │ ├── browse_mode.dart │ │ ├── change_image_quality.dart │ │ ├── change_password.dart │ │ ├── change_server.dart │ │ ├── clear_cache.dart │ │ ├── comic_block_scale.dart │ │ ├── logout.dart │ │ ├── network.dart │ │ ├── pager.dart │ │ ├── read_mode.dart │ │ ├── settings.dart │ │ ├── tag_block.dart │ │ ├── theme.dart │ │ ├── theme_color.dart │ │ ├── theme_icon.dart │ │ ├── theme_switch.dart │ │ ├── visible_categories.dart │ │ ├── webdav.dart │ │ ├── widgets │ │ ├── block.dart │ │ └── menu_list_tile.dart │ │ └── word_block.dart └── widgets │ ├── base_image.dart │ ├── base_page.dart │ ├── button.dart │ ├── empty.dart │ ├── error_page.dart │ ├── shadow_text.dart │ ├── slide_transition_x.dart │ ├── tag.dart │ ├── toast.dart │ └── with_blur.dart ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── app_icon_1024.png │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ └── app_icon_64.png │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements └── RunnerTests │ └── RunnerTests.swift ├── package_rename_config.yaml ├── pubspec.lock ├── pubspec.yaml ├── screenshots ├── pc-分类.png ├── pc-漫画列表.png ├── pc-漫画详情.png ├── pc-阅读.png ├── 分类.png ├── 漫画列表.png ├── 漫画详情.png └── 阅读.png ├── 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 ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | .vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | 47 | *.jks 48 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 17 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 18 | - platform: android 19 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 20 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 21 | - platform: ios 22 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 23 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 24 | - platform: linux 25 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 26 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 27 | - platform: macos 28 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 29 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 30 | - platform: web 31 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 32 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 33 | - platform: windows 34 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 35 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HaKa Comic 2 | 3 | [![License: GPL-3.0](https://img.shields.io/badge/License-GPL%203.0-blue.svg)](https://opensource.org/licenses/GPL-3.0) 4 | 5 | ## 📖 项目简介 6 | 7 | 第三方哗咔漫画跨平台客户端。**练习项目**,支持 Android、IOS、Mac 和 Windows 四个平台,目前仍在持续完善中。欢迎给个 star⭐️ 支持一下。IOS 的 ipa 安装需要自签,有很多工具支持,具体需要自行搜索一下。 8 | 9 | --- 10 | 11 | ## ✨ 核心功能 12 | 13 | - 🆔 **漫画 ID 分享** 14 | 支持官方最新分享漫画 ID 功能,详情页可直接复制漫画 ID(使用 ID 见截图红圈标注) 15 | - 🌐 **多平台适配** 16 | 兼容移动端与桌面端设备(Android/IOS/Mac/Windows) 17 | 18 | --- 19 | 20 | ## 🖼️ 项目截图 21 | 22 | | 分类浏览 | 漫画列表 | 漫画详情 | 阅读界面 | 23 | | ------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------- | 24 | | | | | | 25 | | | | | | 26 | 27 | **截图可能过时,以实际项目界面为主** 28 | 29 | ## ⚠️ 免责声明 30 | 31 | 1. 本项目为**非官方第三方应用**,与哔咔漫画官方无任何关联 32 | 2. 仅用于**技术交流与学习**目的,禁止用于商业用途 33 | 3. 使用本软件产生的一切后果由使用者自行承担 34 | 4. 资源内容版权归原作者及平台所有,请于下载后 24 小时内删除 35 | 36 | --- 37 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | prefer_const_constructors: true 27 | use_null_aware_elements: true 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | .cxx/ 9 | 10 | # Remember to never publicly share your keystore. 11 | # See https://flutter.dev/to/reference-keystore 12 | key.properties 13 | **/*.keystore 14 | **/*.jks 15 | -------------------------------------------------------------------------------- /android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.util.Properties 2 | import java.io.FileInputStream 3 | 4 | plugins { 5 | id("com.android.application") 6 | id("kotlin-android") 7 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 8 | id("dev.flutter.flutter-gradle-plugin") 9 | } 10 | 11 | val keystoreProperties = Properties() 12 | val keystorePropertiesFile = rootProject.file("key.properties") 13 | if (keystorePropertiesFile.exists()) { 14 | keystoreProperties.load(FileInputStream(keystorePropertiesFile)) 15 | } 16 | 17 | android { 18 | namespace = "com.github.raoxwup.haka_comic" 19 | compileSdk = flutter.compileSdkVersion 20 | ndkVersion = "27.0.12077973" 21 | 22 | compileOptions { 23 | sourceCompatibility = JavaVersion.VERSION_11 24 | targetCompatibility = JavaVersion.VERSION_11 25 | } 26 | 27 | kotlinOptions { 28 | jvmTarget = JavaVersion.VERSION_11.toString() 29 | } 30 | 31 | defaultConfig { 32 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 33 | applicationId = "com.github.raoxwup.haka_comic" 34 | // You can update the following values to match your application needs. 35 | // For more information, see: https://flutter.dev/to/review-gradle-config. 36 | minSdk = flutter.minSdkVersion 37 | targetSdk = flutter.targetSdkVersion 38 | versionCode = flutter.versionCode 39 | versionName = flutter.versionName 40 | } 41 | 42 | signingConfigs { 43 | create("release") { 44 | keyAlias = keystoreProperties["keyAlias"] as String 45 | keyPassword = keystoreProperties["keyPassword"] as String 46 | storeFile = keystoreProperties["storeFile"]?.let { file(it) } 47 | storePassword = keystoreProperties["storePassword"] as String 48 | } 49 | } 50 | 51 | buildTypes { 52 | release { 53 | // TODO: Add your own signing config for the release build. 54 | // Signing with the debug keys for now, so `flutter run --release` works. 55 | signingConfig = signingConfigs.getByName("release") 56 | } 57 | } 58 | } 59 | 60 | flutter { 61 | source = "../.." 62 | } 63 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 21 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 39 | 41 | 42 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/raoxwup/haka_comic/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.raoxwup.haka_comic 2 | 3 | import android.os.Bundle 4 | import io.flutter.embedding.android.FlutterActivity 5 | import io.flutter.embedding.engine.FlutterEngine 6 | import io.flutter.plugin.common.MethodChannel 7 | 8 | class MainActivity : FlutterActivity() { 9 | private val CHANNEL = "back_to_home" 10 | 11 | override fun configureFlutterEngine(flutterEngine: FlutterEngine) { 12 | super.configureFlutterEngine(flutterEngine) 13 | 14 | // 设置 MethodChannel 15 | MethodChannel( 16 | flutterEngine.dartExecutor.binaryMessenger, 17 | CHANNEL 18 | ).setMethodCallHandler { call, result -> 19 | when (call.method) { 20 | "moveToBackground" -> { 21 | // 将应用退至后台 22 | moveTaskToBack(true) 23 | result.success(null) 24 | } 25 | else -> result.notImplemented() 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 20 | 21 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() 9 | rootProject.layout.buildDirectory.value(newBuildDir) 10 | 11 | subprojects { 12 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) 13 | project.layout.buildDirectory.value(newSubprojectBuildDir) 14 | } 15 | subprojects { 16 | project.evaluationDependsOn(":app") 17 | } 18 | 19 | tasks.register("clean") { 20 | delete(rootProject.layout.buildDirectory) 21 | } 22 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | val flutterSdkPath = run { 3 | val properties = java.util.Properties() 4 | file("local.properties").inputStream().use { properties.load(it) } 5 | val flutterSdkPath = properties.getProperty("flutter.sdk") 6 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } 7 | flutterSdkPath 8 | } 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id("dev.flutter.flutter-plugin-loader") version "1.0.0" 21 | id("com.android.application") version "8.7.0" apply false 22 | id("org.jetbrains.kotlin.android") version "1.8.22" apply false 23 | } 24 | 25 | include(":app") 26 | -------------------------------------------------------------------------------- /assets/fonts/LXGWWenKaiLite-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/fonts/LXGWWenKaiLite-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/LXGWWenKaiLite-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/fonts/LXGWWenKaiLite-Regular.ttf -------------------------------------------------------------------------------- /assets/icons/android/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/android/icon.png -------------------------------------------------------------------------------- /assets/icons/ios/Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/ios/Dark.png -------------------------------------------------------------------------------- /assets/icons/ios/Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/ios/Light.png -------------------------------------------------------------------------------- /assets/icons/ios/Tinted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/ios/Tinted.png -------------------------------------------------------------------------------- /assets/icons/macos/Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/macos/Icon-1024.png -------------------------------------------------------------------------------- /assets/icons/macos/Icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/macos/Icon-128.png -------------------------------------------------------------------------------- /assets/icons/macos/Icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/macos/Icon-256.png -------------------------------------------------------------------------------- /assets/icons/macos/Icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/macos/Icon-32.png -------------------------------------------------------------------------------- /assets/icons/macos/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/macos/Icon-512.png -------------------------------------------------------------------------------- /assets/icons/macos/Icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/macos/Icon-64.png -------------------------------------------------------------------------------- /assets/icons/pc/macos_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/pc/macos_icon.png -------------------------------------------------------------------------------- /assets/icons/pc/windows_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/pc/windows_icon.ico -------------------------------------------------------------------------------- /assets/icons/pc/windows_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/pc/windows_icon.png -------------------------------------------------------------------------------- /assets/icons/splash/splash-icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/splash/splash-icon-dark.png -------------------------------------------------------------------------------- /assets/icons/splash/splash-icon-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/icons/splash/splash-icon-light.png -------------------------------------------------------------------------------- /assets/images/forum.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/forum.jpg -------------------------------------------------------------------------------- /assets/images/icon_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/icon_empty.png -------------------------------------------------------------------------------- /assets/images/icon_exclamation_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/icon_exclamation_error.png -------------------------------------------------------------------------------- /assets/images/icon_leave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/icon_leave.png -------------------------------------------------------------------------------- /assets/images/icon_no_comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/icon_no_comment.png -------------------------------------------------------------------------------- /assets/images/icon_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/icon_success.png -------------------------------------------------------------------------------- /assets/images/icon_unknown_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/icon_unknown_error.png -------------------------------------------------------------------------------- /assets/images/latest.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/latest.jpg -------------------------------------------------------------------------------- /assets/images/leaderboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/leaderboard.jpg -------------------------------------------------------------------------------- /assets/images/loading-zip.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/loading-zip.gif -------------------------------------------------------------------------------- /assets/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/loading.gif -------------------------------------------------------------------------------- /assets/images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/login.png -------------------------------------------------------------------------------- /assets/images/profile_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/profile_bg.png -------------------------------------------------------------------------------- /assets/images/random.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/random.jpg -------------------------------------------------------------------------------- /assets/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/assets/images/user.png -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | - shared_preferences: true -------------------------------------------------------------------------------- /flutter_launcher_icons.yaml: -------------------------------------------------------------------------------- 1 | # flutter pub run flutter_launcher_icons 2 | flutter_launcher_icons: 3 | image_path: "assets/icons/android/icon.png" 4 | 5 | android: true 6 | image_path_android: "assets/icons/android/icon.png" 7 | min_sdk_android: 21 # android min sdk min:16, default 21 8 | # adaptive_icon_background: "assets/icon/background.png" 9 | # adaptive_icon_foreground: "assets/icon/foreground.png" 10 | # adaptive_icon_monochrome: "assets/icon/monochrome.png" 11 | 12 | ios: true 13 | image_path_ios: "assets/icons/ios/light.png" 14 | # remove_alpha_channel_ios: true 15 | image_path_ios_dark_transparent: "assets/icons/ios/dark.png" 16 | image_path_ios_tinted_grayscale: "assets/icons/ios/tinted.png" 17 | # desaturate_tinted_to_grayscale_ios: true 18 | 19 | windows: 20 | generate: true 21 | image_path: "assets/icons/pc/windows_icon.png" 22 | icon_size: 96 # min:48, max:256, default: 48 23 | 24 | macos: 25 | generate: true 26 | image_path: "assets/icons/pc/macos_icon.png" 27 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 13.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '13.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | 33 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_ios_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-68x68@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-68x68@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-38x38@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-38x38@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-38x38@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-38x38@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-64x64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-64x64@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-64x64@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-64x64@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-68x68@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-68x68@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-38x38@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-38x38@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-38x38@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-38x38@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-64x64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-64x64@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-64x64@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-64x64@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-68x68@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-68x68@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoxwup/haka_comic/2a19dca470454eab8ea32a9dfce7ba58a88a86e3/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSApplicationQueriesSchemes 6 | 7 | https 8 | http 9 | 10 | CADisableMinimumFrameDurationOnPhone 11 | 12 | CFBundleDevelopmentRegion 13 | $(DEVELOPMENT_LANGUAGE) 14 | CFBundleDisplayName 15 | HaKa Comic 16 | CFBundleExecutable 17 | $(EXECUTABLE_NAME) 18 | CFBundleIdentifier 19 | $(PRODUCT_BUNDLE_IDENTIFIER) 20 | CFBundleInfoDictionaryVersion 21 | 6.0 22 | CFBundleName 23 | HaKaComic 24 | CFBundlePackageType 25 | APPL 26 | CFBundleShortVersionString 27 | $(FLUTTER_BUILD_NAME) 28 | CFBundleSignature 29 | ???? 30 | CFBundleVersion 31 | $(FLUTTER_BUILD_NUMBER) 32 | LSRequiresIPhoneOS 33 | 34 | NSAppTransportSecurity 35 | 36 | NSAllowsArbitraryLoads 37 | 38 | 39 | NSPhotoLibraryUsageDescription 40 | Allow $(PRODUCT_NAME) to access your photos. 41 | UIApplicationSupportsIndirectInputEvents 42 | 43 | UIBackgroundModes 44 | 45 | fetch 46 | processing 47 | 48 | UILaunchStoryboardName 49 | LaunchScreen 50 | UIMainStoryboardFile 51 | Main 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/config/setup_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:path_provider/path_provider.dart'; 3 | 4 | final scaffoldMessengerKey = GlobalKey(); 5 | // 全局导航键 6 | final GlobalKey navigatorKey = GlobalKey(); 7 | 8 | class SetupConf { 9 | static late String dataPath; 10 | static String appVersion = "1.0.1"; 11 | 12 | static Future initialize() async { 13 | await Future.wait([initPath()]); 14 | } 15 | 16 | static Future initPath() async { 17 | final dir = await getApplicationSupportDirectory(); 18 | dataPath = dir.path; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/database/tag_block_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:haka_comic/config/setup_config.dart'; 4 | import 'package:path_provider/path_provider.dart'; 5 | import 'package:sqlite_async/sqlite_async.dart'; 6 | import 'package:path/path.dart' as p; 7 | 8 | final migrations = 9 | SqliteMigrations()..add( 10 | SqliteMigration(1, (tx) async { 11 | await tx.execute(''' 12 | CREATE TABLE IF NOT EXISTS tag_block ( 13 | id INTEGER PRIMARY KEY, 14 | tag TEXT UNIQUE NOT NULL 15 | ); 16 | '''); 17 | }), 18 | ); 19 | 20 | class TagBlockHelper with ChangeNotifier { 21 | TagBlockHelper._internal(); 22 | 23 | static final _instance = TagBlockHelper._internal(); 24 | 25 | factory TagBlockHelper() => _instance; 26 | 27 | late SqliteDatabase _db; 28 | String get dbPath => '${SetupConf.dataPath}/tag_block.db'; 29 | 30 | Future initialize() async { 31 | _db = SqliteDatabase(path: dbPath); 32 | await migrations.migrate(_db); 33 | } 34 | 35 | Future insert(String tag) async { 36 | await _db.writeTransaction((tx) async { 37 | await tx.execute('INSERT OR IGNORE INTO tag_block (tag) VALUES (?)', [ 38 | tag, 39 | ]); 40 | }); 41 | notifyListeners(); 42 | } 43 | 44 | Future delete(String tag) async { 45 | await _db.writeTransaction((tx) async { 46 | await tx.execute('DELETE FROM tag_block WHERE tag = ?', [tag]); 47 | }); 48 | notifyListeners(); 49 | } 50 | 51 | Future> query() async { 52 | final result = await _db.getAll('SELECT tag FROM tag_block'); 53 | return result.map((row) => row['tag'] as String).toList(); 54 | } 55 | 56 | Future contains(String tag) async { 57 | final result = await _db.getOptional( 58 | 'SELECT 1 FROM tag_block WHERE tag = ?', 59 | [tag], 60 | ); 61 | return result != null; 62 | } 63 | 64 | Future clear() async { 65 | await _db.execute('DELETE FROM tag_block'); 66 | notifyListeners(); 67 | } 68 | 69 | Future backup() async { 70 | final tempDir = await getTemporaryDirectory(); 71 | final path = p.join(tempDir.path, 'tag_block.db'); 72 | final file = File(path); 73 | if (await file.exists()) { 74 | await file.delete(); 75 | } 76 | await _db.execute('VACUUM INTO ?', [path]); 77 | return File(path); 78 | } 79 | 80 | Future restore(File file) async { 81 | await _db.close(); 82 | final files = [File(dbPath), File('$dbPath-wal'), File('$dbPath-shm')]; 83 | for (var f in files) { 84 | if (await f.exists()) { 85 | await f.delete(); 86 | } 87 | } 88 | await file.copy(dbPath); 89 | await initialize(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/database/word_block_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:haka_comic/config/setup_config.dart'; 4 | import 'package:path_provider/path_provider.dart'; 5 | import 'package:sqlite_async/sqlite_async.dart'; 6 | import 'package:path/path.dart' as p; 7 | 8 | final migrations = 9 | SqliteMigrations()..add( 10 | SqliteMigration(1, (tx) async { 11 | await tx.execute(''' 12 | CREATE TABLE IF NOT EXISTS word_block ( 13 | id INTEGER PRIMARY KEY, 14 | word TEXT UNIQUE NOT NULL 15 | ); 16 | '''); 17 | }), 18 | ); 19 | 20 | class WordBlockHelper with ChangeNotifier { 21 | WordBlockHelper._internal(); 22 | 23 | static final _instance = WordBlockHelper._internal(); 24 | 25 | factory WordBlockHelper() => _instance; 26 | 27 | late SqliteDatabase _db; 28 | String get dbPath => '${SetupConf.dataPath}/word_block.db'; 29 | 30 | Future initialize() async { 31 | _db = SqliteDatabase(path: dbPath); 32 | await migrations.migrate(_db); 33 | } 34 | 35 | Future insert(String word) async { 36 | await _db.writeTransaction((tx) async { 37 | await tx.execute('INSERT OR IGNORE INTO word_block (word) VALUES (?)', [ 38 | word, 39 | ]); 40 | }); 41 | notifyListeners(); 42 | } 43 | 44 | Future delete(String word) async { 45 | await _db.writeTransaction((tx) async { 46 | await tx.execute('DELETE FROM word_block WHERE word = ?', [word]); 47 | }); 48 | notifyListeners(); 49 | } 50 | 51 | Future> query() async { 52 | final result = await _db.getAll('SELECT word FROM word_block'); 53 | return result.map((row) => row['word'] as String).toList(); 54 | } 55 | 56 | Future contains(String word) async { 57 | final result = await _db.getOptional( 58 | 'SELECT 1 FROM word_block WHERE word = ?', 59 | [word], 60 | ); 61 | return result != null; 62 | } 63 | 64 | Future clear() async { 65 | await _db.execute('DELETE FROM word_block'); 66 | notifyListeners(); 67 | } 68 | 69 | Future backup() async { 70 | final tempDir = await getTemporaryDirectory(); 71 | final path = p.join(tempDir.path, 'word_block.db'); 72 | final file = File(path); 73 | if (await file.exists()) { 74 | await file.delete(); 75 | } 76 | await _db.execute('VACUUM INTO ?', [path]); 77 | return File(path); 78 | } 79 | 80 | Future restore(File file) async { 81 | await _db.close(); 82 | final files = [File(dbPath), File('$dbPath-wal'), File('$dbPath-shm')]; 83 | for (var f in files) { 84 | if (await f.exists()) { 85 | await f.delete(); 86 | } 87 | } 88 | await file.copy(dbPath); 89 | await initialize(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/mixin/auto_register_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:haka_comic/utils/extension.dart'; 3 | 4 | mixin AutoRegisterHandlerMixin on State { 5 | /// 注册请求处理器 6 | late final List _handlers; 7 | 8 | /// 注册请求处理器方法 9 | List registerHandler(); 10 | 11 | void update() { 12 | if (mounted) { 13 | setState(() {}); 14 | } 15 | } 16 | 17 | @override 18 | void initState() { 19 | super.initState(); 20 | _handlers = registerHandler(); 21 | for (var handler in _handlers) { 22 | handler.addListener(update); 23 | } 24 | } 25 | 26 | @override 27 | void dispose() { 28 | for (var handler in _handlers) { 29 | handler.dispose(); 30 | } 31 | super.dispose(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/mixin/blocked_words.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:haka_comic/database/tag_block_helper.dart'; 3 | import 'package:haka_comic/database/word_block_helper.dart'; 4 | 5 | mixin BlockedWordsMixin on State { 6 | final _tagBlockHelper = TagBlockHelper(); 7 | List blockedTags = []; 8 | 9 | Future _getBlockedTags() async { 10 | final tags = await _tagBlockHelper.query(); 11 | setState(() { 12 | blockedTags = tags; 13 | }); 14 | } 15 | 16 | final _wordBlockHelper = WordBlockHelper(); 17 | List blockedWords = []; 18 | 19 | Future _getBlockedWords() async { 20 | final words = await _wordBlockHelper.query(); 21 | setState(() { 22 | blockedWords = words; 23 | }); 24 | } 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | _getBlockedTags(); 30 | _tagBlockHelper.addListener(_getBlockedTags); 31 | _getBlockedWords(); 32 | _wordBlockHelper.addListener(_getBlockedWords); 33 | } 34 | 35 | @override 36 | void dispose() { 37 | _tagBlockHelper.removeListener(_getBlockedTags); 38 | _wordBlockHelper.removeListener(_getBlockedWords); 39 | super.dispose(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/mixin/pagination_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/config/app_config.dart'; 3 | 4 | mixin PaginationHandlerMixin on State { 5 | final scrollController = ScrollController(); 6 | final pagination = AppConf().pagination; 7 | bool _loading = false; 8 | 9 | Future loadMore(); 10 | 11 | void onScroll() { 12 | final position = scrollController.position; 13 | if (position.maxScrollExtent <= 0) return; 14 | 15 | const threshold = 200.0; // 距离底部 200 像素内触发加载 16 | final distanceToBottom = position.maxScrollExtent - position.pixels; 17 | 18 | if (distanceToBottom <= threshold) { 19 | if (_loading) return; 20 | _loading = true; 21 | loadMore().whenComplete(() => _loading = false); 22 | } 23 | } 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | 29 | if (!pagination) { 30 | scrollController.addListener(onScroll); 31 | } 32 | } 33 | 34 | @override 35 | void dispose() { 36 | if (!pagination) { 37 | scrollController 38 | ..removeListener(onScroll) 39 | ..dispose(); 40 | } 41 | 42 | super.dispose(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/model/reader_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/network/models.dart'; 3 | 4 | enum ReadMode { 5 | /// 条漫模式 6 | vertical, 7 | 8 | /// 单页横向从左到右 9 | leftToRight, 10 | 11 | /// 单页横向从右到左 12 | rightToLeft, 13 | 14 | /// 双页横向从左到右 15 | doubleLeftToRight, 16 | 17 | /// 双页横向从右到左 18 | doubleRightToLeft, 19 | } 20 | 21 | ReadMode stringToReadMode(String mode) { 22 | return switch (mode) { 23 | 'vertical' => ReadMode.vertical, 24 | 'leftToRight' => ReadMode.leftToRight, 25 | 'rightToLeft' => ReadMode.rightToLeft, 26 | 'doubleLeftToRight' => ReadMode.doubleLeftToRight, 27 | 'doubleRightToLeft' => ReadMode.doubleRightToLeft, 28 | _ => ReadMode.vertical, 29 | }; 30 | } 31 | 32 | String readModeToString(ReadMode mode) { 33 | return switch (mode) { 34 | ReadMode.vertical => '连续从上到下', 35 | ReadMode.leftToRight => '单页从左到右', 36 | ReadMode.rightToLeft => '单页从右到左', 37 | ReadMode.doubleLeftToRight => '双页从左到右', 38 | ReadMode.doubleRightToLeft => '双页从右到左', 39 | }; 40 | } 41 | 42 | class ReaderProvider with ChangeNotifier { 43 | /// 漫画id 44 | late String cid; 45 | 46 | /// 漫画名称 47 | late String title; 48 | 49 | /// 漫画所有章节 50 | late List chapters; 51 | 52 | /// 漫画当前阅读章节 53 | late Chapter _currentChapter; 54 | Chapter get currentChapter => _currentChapter; 55 | set currentChapter(Chapter chapter) { 56 | _currentChapter = chapter; 57 | notifyListeners(); 58 | } 59 | 60 | /// 当前章节的索引 61 | int get currentChapterIndex => chapters.indexOf(currentChapter); 62 | 63 | /// 是否为第一章 64 | bool get isFirstChapter => currentChapter.uid == chapters.first.uid; 65 | 66 | /// 是否为最后一章 67 | bool get isLastChapter => currentChapter.uid == chapters.last.uid; 68 | 69 | /// 当前章节第几张图片 70 | int _pageNo = 0; 71 | int get pageNo => _pageNo; 72 | set pageNo(int index) { 73 | _pageNo = index; 74 | notifyListeners(); 75 | } 76 | 77 | /// [id] 漫画id 78 | /// [title] 漫画标题 79 | /// [chapters] 漫画所有章节 80 | /// [currentChapter] 从第几章开始 81 | /// [pageNo] 从第几张图片开始 82 | void initialize({ 83 | required String id, 84 | required String title, 85 | required List chapters, 86 | Chapter? currentChapter, 87 | int? pageNo, 88 | }) { 89 | cid = id; 90 | this.title = title; 91 | this.chapters = chapters; 92 | _currentChapter = currentChapter ?? chapters.first; 93 | _pageNo = pageNo ?? 0; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/model/search_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/utils/shared_preferences_util.dart'; 3 | 4 | class SearchProvider with ChangeNotifier { 5 | static const historyLength = 20; 6 | final List _history = []; 7 | 8 | List get history => _history; 9 | 10 | SearchProvider() { 11 | final history = SharedPreferencesUtil.prefs.getStringList('search_history'); 12 | if (history != null) { 13 | _history.addAll(history); 14 | notifyListeners(); 15 | } 16 | } 17 | 18 | void add(String keyword) { 19 | if (_history.contains(keyword)) { 20 | _history.remove(keyword); 21 | } 22 | if (_history.length >= historyLength) { 23 | _history.removeLast(); 24 | } 25 | _history.insert(0, keyword); 26 | notifyListeners(); 27 | } 28 | 29 | void remove(String keyword) { 30 | _history.remove(keyword); 31 | notifyListeners(); 32 | } 33 | 34 | void clear() { 35 | _history.clear(); 36 | notifyListeners(); 37 | } 38 | 39 | @override 40 | void notifyListeners() { 41 | super.notifyListeners(); 42 | useCache(); 43 | } 44 | 45 | void useCache() { 46 | SharedPreferencesUtil.prefs.setStringList('search_history', _history); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/model/theme_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/utils/shared_preferences_util.dart'; 3 | 4 | class ThemeProvider with ChangeNotifier { 5 | ThemeMode _themeMode = ThemeMode.system; 6 | String _primaryColor = 'System'; 7 | 8 | ThemeMode get themeMode => _themeMode; 9 | 10 | String get primaryColor => _primaryColor; 11 | 12 | static const Map stringToThemeMode = { 13 | 'System': ThemeMode.system, 14 | 'Light': ThemeMode.light, 15 | 'Dark': ThemeMode.dark, 16 | }; 17 | 18 | static const Map themeModeToString = { 19 | ThemeMode.system: 'System', 20 | ThemeMode.light: 'Light', 21 | ThemeMode.dark: 'Dark', 22 | }; 23 | 24 | static Color stringToColor(String color) { 25 | return switch (color) { 26 | 'Red' => Colors.red, 27 | 'Pink' => Colors.pink, 28 | 'Green' => Colors.green, 29 | 'Blue' => Colors.blue, 30 | 'Yellow' => Colors.yellow, 31 | 'Orange' => Colors.orange, 32 | 'Purple' => Colors.purple, 33 | _ => Colors.blue, 34 | }; 35 | } 36 | 37 | ThemeProvider() { 38 | final mode = 39 | SharedPreferencesUtil.prefs.getString('theme_mode') ?? 'System'; 40 | _themeMode = stringToThemeMode[mode]!; 41 | 42 | _primaryColor = 43 | SharedPreferencesUtil.prefs.getString('primary_color') ?? 'System'; 44 | } 45 | 46 | void setThemeMode(ThemeMode mode) { 47 | _themeMode = mode; 48 | SharedPreferencesUtil.prefs.setString( 49 | 'theme_mode', 50 | themeModeToString[mode]!, 51 | ); 52 | notifyListeners(); 53 | } 54 | 55 | void setPrimaryColor(String color) { 56 | _primaryColor = color; 57 | SharedPreferencesUtil.prefs.setString('primary_color', color); 58 | notifyListeners(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/model/user_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/network/models.dart'; 3 | 4 | class UserProvider with ChangeNotifier { 5 | User? _user; 6 | 7 | User? get user => _user; 8 | 9 | set user(User? user) { 10 | _user = user; 11 | notifyListeners(); 12 | } 13 | 14 | late VoidCallback refresh; 15 | } 16 | -------------------------------------------------------------------------------- /lib/network/cache.dart: -------------------------------------------------------------------------------- 1 | class Cache { 2 | static final Map> _cache = {}; 3 | 4 | static void add(String key, Map map) { 5 | _cache[key] = map; 6 | } 7 | 8 | static Map? get(String key) { 9 | return _cache[key]; 10 | } 11 | 12 | static void clear() { 13 | _cache.clear(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/router/aware_page_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | typedef RouteAwarePageBuilder = 4 | Widget Function(BuildContext context, bool completed); 5 | 6 | class RouteAwarePageWrapper extends StatefulWidget { 7 | final RouteAwarePageBuilder builder; 8 | final bool shouldRebuildOnCompleted; 9 | 10 | const RouteAwarePageWrapper({ 11 | super.key, 12 | required this.builder, 13 | this.shouldRebuildOnCompleted = true, 14 | }); 15 | 16 | @override 17 | State createState() => _RouteAwarePageWrapperState(); 18 | } 19 | 20 | class _RouteAwarePageWrapperState extends State { 21 | late bool _completed; 22 | Animation? _routeAnimation; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _completed = false; 28 | WidgetsBinding.instance.addPostFrameCallback((_) => _setupRouteListener()); 29 | } 30 | 31 | void _setupRouteListener() { 32 | final route = ModalRoute.of(context); 33 | if (route != null) { 34 | _routeAnimation = route.animation; 35 | // 检查初始状态 36 | if (_routeAnimation?.status == AnimationStatus.completed) { 37 | _markCompleted(); 38 | } else { 39 | _routeAnimation?.addStatusListener(_handleRouteAnimationStatus); 40 | } 41 | } 42 | } 43 | 44 | void _handleRouteAnimationStatus(AnimationStatus status) { 45 | if (status == AnimationStatus.completed) { 46 | _markCompleted(); 47 | _routeAnimation?.removeStatusListener(_handleRouteAnimationStatus); 48 | } 49 | } 50 | 51 | void _markCompleted() { 52 | if (!_completed && widget.shouldRebuildOnCompleted) { 53 | setState(() => _completed = true); 54 | } else { 55 | _completed = true; 56 | } 57 | } 58 | 59 | @override 60 | void dispose() { 61 | _routeAnimation?.removeStatusListener(_handleRouteAnimationStatus); 62 | super.dispose(); 63 | } 64 | 65 | @override 66 | Widget build(BuildContext context) { 67 | return widget.builder(context, _completed); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/startup_prepare.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:haka_comic/config/app_config.dart'; 4 | import 'package:haka_comic/config/setup_config.dart'; 5 | import 'package:haka_comic/database/history_helper.dart'; 6 | import 'package:haka_comic/database/images_helper.dart'; 7 | import 'package:haka_comic/database/read_record_helper.dart'; 8 | import 'package:haka_comic/database/tag_block_helper.dart'; 9 | import 'package:haka_comic/database/word_block_helper.dart'; 10 | import 'package:haka_comic/utils/common.dart'; 11 | import 'package:haka_comic/utils/download_manager.dart'; 12 | import 'package:window_manager/window_manager.dart'; 13 | 14 | class StartupPrepare { 15 | /// 初始化 16 | static Future> prepare() async { 17 | await Future.wait([AppConf.initialize(), SetupConf.initialize()]); 18 | return Future.wait([ 19 | HistoryHelper().initialize(), 20 | ImagesHelper.initialize(), 21 | ReadRecordHelper().initialize(), 22 | DownloadManager.initialize(), 23 | TagBlockHelper().initialize(), 24 | WordBlockHelper().initialize(), 25 | startDesktop(), 26 | ]); 27 | } 28 | } 29 | 30 | Future startDesktop() async { 31 | if (isDesktop && kReleaseMode) { 32 | await windowManager.ensureInitialized(); 33 | WindowOptions windowOptions = const WindowOptions( 34 | size: Size(900, 620), 35 | center: true, 36 | backgroundColor: Colors.transparent, 37 | skipTaskbar: false, 38 | titleBarStyle: TitleBarStyle.normal, 39 | ); 40 | windowManager.waitUntilReadyToShow(windowOptions, () async { 41 | await windowManager.setMinimumSize(const Size(750, 550)); 42 | await windowManager.setResizable(true); 43 | await windowManager.show(); 44 | await windowManager.focus(); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/utils/loader.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:haka_comic/utils/extension.dart'; 4 | 5 | class Loader { 6 | static bool isShowing = false; 7 | 8 | static void show(BuildContext context) { 9 | if (isShowing) return; 10 | isShowing = true; 11 | showDialog( 12 | context: context, 13 | barrierDismissible: false, 14 | builder: (context) { 15 | return Center( 16 | child: Container( 17 | width: 70, 18 | height: 70, 19 | decoration: BoxDecoration( 20 | color: context.colorScheme.surfaceContainerLowest, 21 | borderRadius: BorderRadius.circular(10), 22 | ), 23 | child: const Center(child: CircularProgressIndicator()), 24 | ), 25 | ); 26 | }, 27 | ); 28 | } 29 | 30 | static void hide(BuildContext context) { 31 | if (context.canPop() && isShowing) { 32 | isShowing = false; 33 | context.pop(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/utils/log.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | extension FirstWhereOrNull on List { 4 | T? firstWhereOrNull(bool Function(T) predicate) { 5 | for (final element in this) { 6 | if (predicate(element)) return element; 7 | } 8 | return null; 9 | } 10 | } 11 | 12 | enum LogLevel { error, warning, info } 13 | 14 | class LogItem { 15 | final LogLevel level; 16 | final String title; 17 | final String content; 18 | final DateTime time = DateTime.now(); 19 | 20 | LogItem(this.level, this.title, this.content); 21 | 22 | @override 23 | String toString() => "${level.name} $title $time \n$content\n\n"; 24 | } 25 | 26 | class Log { 27 | static final List _items = []; 28 | 29 | static List get items => _items; 30 | 31 | static const int maxItemLength = 3000; 32 | 33 | static const int maxItemsNumber = 500; 34 | 35 | static void printWarning(String text) { 36 | debugPrint('⚠️ WARNING: $text'); 37 | } 38 | 39 | static void printError(String text) { 40 | debugPrint('⛔ ERROR: $text'); 41 | } 42 | 43 | static void printInfo(String text) { 44 | debugPrint('ℹ️ INFO: $text'); 45 | } 46 | 47 | static void add(LogLevel level, String title, String content) { 48 | if (content.length > maxItemLength) { 49 | content = "${content.substring(0, maxItemLength)}..."; 50 | } 51 | 52 | switch (level) { 53 | case LogLevel.error: 54 | printError(content); 55 | break; 56 | case LogLevel.warning: 57 | printWarning(content); 58 | break; 59 | case LogLevel.info: 60 | printInfo(content); 61 | } 62 | 63 | var item = LogItem(level, title, content); 64 | 65 | if (item == _items.lastOrNull) { 66 | return; 67 | } 68 | 69 | _items.add(item); 70 | 71 | if (_items.length > maxItemsNumber) { 72 | var res = _items.remove( 73 | _items.firstWhereOrNull((element) => element.level == LogLevel.info), 74 | ); 75 | if (!res) { 76 | _items.removeAt(0); 77 | } 78 | } 79 | } 80 | 81 | static info(String title, String content) { 82 | add(LogLevel.info, title, content); 83 | } 84 | 85 | static warning(String title, String content) { 86 | add(LogLevel.warning, title, content); 87 | } 88 | 89 | static error(String title, Object content, [Object? stackTrace]) { 90 | var info = content.toString(); 91 | if (stackTrace != null) { 92 | info += "\n${stackTrace.toString()}"; 93 | } 94 | add(LogLevel.error, title, info); 95 | } 96 | 97 | static void clear() => _items.clear(); 98 | 99 | @override 100 | String toString() { 101 | var res = "Logs\n\n"; 102 | for (var log in _items) { 103 | res += log.toString(); 104 | } 105 | return res; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/utils/shared_preferences_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class SharedPreferencesUtil { 4 | static late SharedPreferences prefs; 5 | 6 | static Future init() async { 7 | prefs = await SharedPreferences.getInstance(); 8 | return prefs; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/utils/ui.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/utils/extension.dart'; 3 | 4 | enum UiModes { 5 | /// The screen have a short width. Usually the device is phone. 6 | m1, 7 | 8 | /// The screen's width is medium size. Usually the device is tablet. 9 | m2, 10 | 11 | /// The screen's width is long. Usually the device is PC. 12 | m3, 13 | } 14 | 15 | UiModes getMode(BuildContext context) { 16 | final width = context.width; 17 | if (width <= 600) { 18 | return UiModes.m1; 19 | } else if (width > 600 && width <= 950) { 20 | return UiModes.m2; 21 | } else { 22 | return UiModes.m3; 23 | } 24 | } 25 | 26 | class UiMode { 27 | static bool m1(BuildContext context) { 28 | return getMode(context) == UiModes.m1; 29 | } 30 | 31 | static bool m2(BuildContext context) { 32 | return getMode(context) == UiModes.m2; 33 | } 34 | 35 | static bool m3(BuildContext context) { 36 | return getMode(context) == UiModes.m3; 37 | } 38 | 39 | static bool notM1(BuildContext context) { 40 | return !m1(context); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/utils/version.dart: -------------------------------------------------------------------------------- 1 | class Version implements Comparable { 2 | final int major; 3 | final int minor; 4 | final int patch; 5 | final List preRelease; 6 | 7 | Version({ 8 | required this.major, 9 | required this.minor, 10 | required this.patch, 11 | this.preRelease = const [], 12 | }); 13 | 14 | factory Version.parse(String versionStr) { 15 | final regex = RegExp( 16 | r'^v?(\d+)\.(\d+)\.(\d+)(?:-([\w\.]+))?', 17 | caseSensitive: false, 18 | ); 19 | 20 | final match = regex.firstMatch(versionStr); 21 | if (match == null) throw FormatException('Invalid version: $versionStr'); 22 | 23 | final preRelease = 24 | (match.group(4) ?? '') 25 | .split('.') 26 | .where((s) => s.isNotEmpty) 27 | .map((s) => int.tryParse(s) ?? s) 28 | .toList(); 29 | 30 | return Version( 31 | major: int.parse(match.group(1)!), 32 | minor: int.parse(match.group(2)!), 33 | patch: int.parse(match.group(3)!), 34 | preRelease: preRelease, 35 | ); 36 | } 37 | 38 | @override 39 | int compareTo(Version other) { 40 | // 比较主版本号 41 | if (major != other.major) return major.compareTo(other.major); 42 | 43 | // 比较次版本号 44 | if (minor != other.minor) return minor.compareTo(other.minor); 45 | 46 | // 比较修订号 47 | if (patch != other.patch) return patch.compareTo(other.patch); 48 | 49 | // 处理预发布标签比较 50 | return _comparePreRelease(other.preRelease); 51 | } 52 | 53 | int _comparePreRelease(List other) { 54 | final a = preRelease; 55 | final b = other; 56 | 57 | // 正式版本 > 预发布版本 58 | if (a.isEmpty && b.isEmpty) return 0; 59 | if (a.isEmpty) return 1; 60 | if (b.isEmpty) return -1; 61 | 62 | for (var i = 0; i < a.length && i < b.length; i++) { 63 | final itemA = a[i]; 64 | final itemB = b[i]; 65 | 66 | // 类型不同时:数字 < 字符串 67 | if (itemA is num && itemB is! num) return -1; 68 | if (itemA is! num && itemB is num) return 1; 69 | 70 | // 同类型比较 71 | final comparison = _compareItems(itemA, itemB); 72 | if (comparison != 0) return comparison; 73 | } 74 | 75 | // 长度不同时:标识符更多的一方更大 76 | return a.length.compareTo(b.length); 77 | } 78 | 79 | int _compareItems(Object a, Object b) { 80 | if (a is num && b is num) { 81 | return a.compareTo(b); 82 | } else if (a is String && b is String) { 83 | return a.compareTo(b); 84 | } 85 | return 0; 86 | } 87 | 88 | @override 89 | String toString() { 90 | final version = '$major.$minor.$patch'; 91 | if (preRelease.isEmpty) return version; 92 | return '$version-${preRelease.join('.')}'; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/views/comic_details/collect_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/network/http.dart'; 3 | import 'package:haka_comic/utils/extension.dart'; 4 | import 'package:haka_comic/utils/log.dart'; 5 | import 'package:haka_comic/widgets/toast.dart'; 6 | 7 | class CollectAction extends StatefulWidget { 8 | const CollectAction({super.key, required this.id, required this.isFavorite}); 9 | 10 | final String id; 11 | 12 | final bool isFavorite; 13 | 14 | @override 15 | State createState() => _CollectActionState(); 16 | } 17 | 18 | class _CollectActionState extends State 19 | with SingleTickerProviderStateMixin { 20 | late bool _isFavorite; 21 | late AnimationController _controller; 22 | late Animation _animation; 23 | late final handler = favoriteComic.useRequest( 24 | onSuccess: (data, _) { 25 | Log.info('Favorite comic success', data.action); 26 | }, 27 | onError: (e, _) { 28 | Log.error('Favorite comic error', e); 29 | Toast.show(message: '收藏失败'); 30 | setState(() { 31 | _isFavorite = !_isFavorite; 32 | }); 33 | }, 34 | ); 35 | 36 | @override 37 | void initState() { 38 | super.initState(); 39 | 40 | _isFavorite = widget.isFavorite; 41 | 42 | _controller = AnimationController( 43 | duration: const Duration(milliseconds: 250), 44 | vsync: this, 45 | ); 46 | 47 | _animation = TweenSequence([ 48 | TweenSequenceItem(tween: Tween(begin: 1.0, end: 1.3), weight: 1), 49 | TweenSequenceItem(tween: Tween(begin: 1.3, end: 1.0), weight: 1), 50 | ]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); 51 | } 52 | 53 | @override 54 | void dispose() { 55 | _controller.dispose(); 56 | super.dispose(); 57 | } 58 | 59 | void _handlePress() { 60 | setState(() { 61 | _isFavorite = !_isFavorite; 62 | }); 63 | _controller.forward(from: 0); 64 | 65 | handler.run(widget.id); 66 | } 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | return ActionChip( 71 | avatar: ScaleTransition( 72 | scale: _animation, 73 | child: Icon(_isFavorite ? Icons.star : Icons.star_outline), 74 | ), 75 | shape: const StadiumBorder(), 76 | label: const Text('收藏'), 77 | onPressed: _handlePress, 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/views/comic_details/comic_share_id.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:haka_comic/mixin/auto_register_handler.dart'; 4 | import 'package:haka_comic/network/http.dart'; 5 | import 'package:haka_comic/utils/extension.dart'; 6 | import 'package:haka_comic/utils/log.dart'; 7 | import 'package:haka_comic/views/comic_details/comic_details.dart'; 8 | import 'package:haka_comic/widgets/toast.dart'; 9 | 10 | class ComicShareId extends StatefulWidget { 11 | const ComicShareId({super.key, required this.id}); 12 | 13 | /// 漫画id 14 | final String id; 15 | 16 | @override 17 | State createState() => _ComicShareIdState(); 18 | } 19 | 20 | class _ComicShareIdState extends State 21 | with AutoRegisterHandlerMixin { 22 | final _handler = fetchComicShareId.useRequest( 23 | onSuccess: (data, _) { 24 | Log.info('Fetch comic share id success', data.toString()); 25 | }, 26 | onError: (e, _) { 27 | Log.error('Fetch comic share id error', e); 28 | }, 29 | ); 30 | 31 | @override 32 | List registerHandler() => [_handler]; 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | _handler.run(widget.id); 38 | } 39 | 40 | void copy() async { 41 | await Clipboard.setData(ClipboardData(text: 'PICA${_handler.data}')); 42 | Toast.show(message: '已复制'); 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | final String id = _handler.data?.toString() ?? '加载中...'; 48 | return InfoRow( 49 | onTap: _handler.data == null ? null : copy, 50 | icon: Icons.share, 51 | data: id, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/views/comic_details/creator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/network/models.dart'; 3 | import 'package:haka_comic/utils/common.dart'; 4 | import 'package:haka_comic/utils/extension.dart'; 5 | import 'package:haka_comic/widgets/base_image.dart'; 6 | 7 | class ComicCreator extends StatelessWidget { 8 | const ComicCreator({ 9 | super.key, 10 | required this.creator, 11 | required this.updatedAt, 12 | }); 13 | 14 | final Creator? creator; 15 | 16 | final String? updatedAt; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | final String time = 21 | updatedAt == null 22 | ? '' 23 | : DateTime.parse(updatedAt!).toString().split(' ')[0]; 24 | return InkWell( 25 | onTap: () => showCreator(context, creator), 26 | borderRadius: const BorderRadius.all(Radius.circular(8)), 27 | child: Container( 28 | padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), 29 | decoration: BoxDecoration( 30 | color: context.colorScheme.surfaceContainer, 31 | borderRadius: const BorderRadius.all(Radius.circular(8)), 32 | ), 33 | child: Row( 34 | spacing: 8, 35 | children: [ 36 | BaseImage( 37 | shape: const CircleBorder(), 38 | url: creator?.avatar?.url ?? '', 39 | width: 40, 40 | height: 40, 41 | ), 42 | Column( 43 | crossAxisAlignment: CrossAxisAlignment.start, 44 | spacing: 5, 45 | children: [ 46 | Text( 47 | creator?.name ?? '', 48 | style: context.textTheme.titleMedium, 49 | maxLines: 1, 50 | overflow: TextOverflow.ellipsis, 51 | ), 52 | Text(time, style: context.textTheme.bodySmall), 53 | ], 54 | ), 55 | ], 56 | ), 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/views/comic_details/icon_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/utils/extension.dart'; 3 | 4 | class IconText extends StatelessWidget { 5 | const IconText({ 6 | super.key, 7 | required this.icon, 8 | required this.text, 9 | this.spacing = 4, 10 | }); 11 | 12 | final Icon icon; 13 | final String text; 14 | final double spacing; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Row( 19 | spacing: spacing, 20 | children: [icon, Text(text, style: context.textTheme.bodySmall)], 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/views/comic_details/liked_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/network/http.dart'; 3 | import 'package:haka_comic/utils/extension.dart'; 4 | import 'package:haka_comic/utils/log.dart'; 5 | import 'package:haka_comic/widgets/toast.dart'; 6 | 7 | class LikedAction extends StatefulWidget { 8 | const LikedAction({super.key, required this.id, required this.isLiked}); 9 | 10 | final String id; 11 | 12 | final bool isLiked; 13 | 14 | @override 15 | State createState() => _LikedActionState(); 16 | } 17 | 18 | class _LikedActionState extends State 19 | with SingleTickerProviderStateMixin { 20 | late bool _isLiked; 21 | late AnimationController _controller; 22 | late Animation _animation; 23 | late final handler = likeComic.useRequest( 24 | onSuccess: (data, _) { 25 | Log.info('Like comic success', data.action); 26 | }, 27 | onError: (e, _) { 28 | Log.error('Like comic error', e); 29 | Toast.show(message: '点赞失败'); 30 | setState(() { 31 | _isLiked = !_isLiked; 32 | }); 33 | }, 34 | ); 35 | 36 | @override 37 | void initState() { 38 | super.initState(); 39 | 40 | _isLiked = widget.isLiked; 41 | 42 | _controller = AnimationController( 43 | duration: const Duration(milliseconds: 250), 44 | vsync: this, 45 | ); 46 | 47 | _animation = TweenSequence([ 48 | TweenSequenceItem(tween: Tween(begin: 1.0, end: 1.3), weight: 1), 49 | TweenSequenceItem(tween: Tween(begin: 1.3, end: 1.0), weight: 1), 50 | ]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); 51 | } 52 | 53 | @override 54 | void dispose() { 55 | _controller.dispose(); 56 | super.dispose(); 57 | } 58 | 59 | void _handlePress() { 60 | setState(() { 61 | _isLiked = !_isLiked; 62 | }); 63 | _controller.forward(from: 0); 64 | 65 | handler.run(widget.id); 66 | } 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | return ActionChip( 71 | avatar: ScaleTransition( 72 | scale: _animation, 73 | child: Icon(_isLiked ? Icons.favorite : Icons.favorite_border), 74 | ), 75 | shape: const StadiumBorder(), 76 | label: const Text('点赞'), 77 | onPressed: _handlePress, 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/views/comics/sort_type_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:haka_comic/network/models.dart'; 4 | 5 | class SortTypeSelector extends StatefulWidget { 6 | const SortTypeSelector({ 7 | super.key, 8 | required this.sortType, 9 | required this.onSortTypeChange, 10 | }); 11 | 12 | final ComicSortType sortType; 13 | 14 | final ValueChanged onSortTypeChange; 15 | 16 | @override 17 | State createState() => _SortTypeSelectorState(); 18 | } 19 | 20 | class _SortTypeSelectorState extends State { 21 | late ComicSortType _sortType; 22 | 23 | final List> sorts = [ 24 | {"label": '新到旧', "value": ComicSortType.dd}, 25 | {"label": "旧到新", "value": ComicSortType.da}, 26 | {"label": "最多喜欢", "value": ComicSortType.ld}, 27 | {"label": "最多观看", "value": ComicSortType.vd}, 28 | ]; 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | _sortType = widget.sortType; 34 | } 35 | 36 | void handleChange(ComicSortType type) { 37 | if (type == _sortType) return; 38 | setState(() { 39 | _sortType = type; 40 | }); 41 | widget.onSortTypeChange(type); 42 | context.pop(); 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return AlertDialog( 48 | title: const Text('排序方式'), 49 | contentPadding: const EdgeInsets.all(20), 50 | content: SingleChildScrollView( 51 | child: RadioGroup( 52 | groupValue: _sortType, 53 | onChanged: (ComicSortType? value) { 54 | handleChange(value!); 55 | }, 56 | child: Column( 57 | mainAxisSize: MainAxisSize.min, 58 | children: 59 | sorts 60 | .map( 61 | (e) => ListTile( 62 | title: Text(e['label']), 63 | leading: Radio(value: e['value']), 64 | onTap: () => handleChange(e['value']), 65 | ), 66 | ) 67 | .toList(), 68 | ), 69 | ), 70 | ), 71 | actions: [ 72 | TextButton(onPressed: () => context.pop(), child: const Text('关闭')), 73 | ], 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/views/comics/tmi_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/config/app_config.dart'; 3 | import 'package:haka_comic/utils/extension.dart'; 4 | import 'package:haka_comic/utils/ui.dart'; 5 | import 'package:haka_comic/views/settings/browse_mode.dart'; 6 | 7 | class TMIList extends StatelessWidget { 8 | const TMIList({ 9 | super.key, 10 | this.controller, 11 | this.itemCount, 12 | required this.itemBuilder, 13 | this.pageSelectorBuilder, 14 | this.footerBuilder, 15 | }); 16 | 17 | /// 滑动控制器 18 | final ScrollController? controller; 19 | 20 | final int? itemCount; 21 | 22 | final NullableIndexedWidgetBuilder itemBuilder; 23 | 24 | final Widget Function(BuildContext)? pageSelectorBuilder; 25 | 26 | final Widget Function(BuildContext)? footerBuilder; 27 | 28 | /// 简洁模式? 29 | bool get isSimpleMode => AppConf().comicBlockMode == ComicBlockMode.simple; 30 | 31 | double get scale => AppConf().scale; 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | final width = context.width; 36 | final gridDelegate = 37 | isSimpleMode 38 | ? SliverGridDelegateWithMaxCrossAxisExtent( 39 | maxCrossAxisExtent: 40 | (UiMode.m1(context) 41 | ? 130 42 | : UiMode.m2(context) 43 | ? 135 44 | : 140) * 45 | scale, 46 | mainAxisSpacing: 2, 47 | crossAxisSpacing: 3, 48 | childAspectRatio: 1 / 1.66, 49 | ) 50 | : SliverGridDelegateWithMaxCrossAxisExtent( 51 | maxCrossAxisExtent: 52 | UiMode.m1(context) 53 | ? width 54 | : UiMode.m2(context) 55 | ? width / 2 56 | : width / 3, 57 | mainAxisSpacing: 5, 58 | crossAxisSpacing: 5, 59 | childAspectRatio: 2.5, 60 | ); 61 | 62 | return CustomScrollView( 63 | controller: controller, 64 | slivers: [ 65 | if (pageSelectorBuilder != null) pageSelectorBuilder!(context), 66 | SliverGrid.builder( 67 | gridDelegate: gridDelegate, 68 | itemBuilder: itemBuilder, 69 | itemCount: itemCount, 70 | ), 71 | if (pageSelectorBuilder != null) pageSelectorBuilder!(context), 72 | if (footerBuilder != null) footerBuilder!(context), 73 | ], 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/views/comments/comment_input.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:haka_comic/network/models.dart'; 4 | import 'package:haka_comic/utils/extension.dart'; 5 | import 'package:haka_comic/widgets/button.dart'; 6 | 7 | class CommentInput extends StatefulWidget { 8 | const CommentInput({super.key, required this.id, required this.handler}); 9 | 10 | final String id; 11 | 12 | final AsyncRequestHandler1 handler; 13 | 14 | @override 15 | State createState() => _CommentInputState(); 16 | } 17 | 18 | class _CommentInputState extends State { 19 | late final AsyncRequestHandler1 _handler; 20 | final _commentController = TextEditingController(); 21 | 22 | void _update() => setState(() {}); 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _handler = widget.handler; 28 | _handler.isLoading = false; 29 | 30 | _handler.addListener(_update); 31 | } 32 | 33 | @override 34 | void dispose() { 35 | _handler 36 | ..removeListener(_update) 37 | ..dispose(); 38 | 39 | _commentController.dispose(); 40 | 41 | super.dispose(); 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | final content = _commentController.text; 47 | final bottom = context.viewInsets.bottom; 48 | 49 | return Padding( 50 | padding: EdgeInsets.fromLTRB(15, 15, 15, 5 + bottom), 51 | child: Column( 52 | mainAxisSize: MainAxisSize.min, 53 | spacing: 5, 54 | children: [ 55 | TextField( 56 | minLines: 3, 57 | maxLines: 10, 58 | keyboardType: TextInputType.multiline, 59 | autofocus: true, 60 | decoration: InputDecoration( 61 | hintText: '评论', 62 | border: const OutlineInputBorder(borderSide: BorderSide.none), 63 | filled: true, 64 | fillColor: context.colorScheme.surfaceContainerHighest, 65 | ), 66 | controller: _commentController, 67 | onChanged: (_) => _update(), 68 | ), 69 | Row( 70 | spacing: 5, 71 | children: [ 72 | const Spacer(), 73 | TextButton( 74 | onPressed: () => context.pop(), 75 | child: Text('取消', style: context.textTheme.bodyMedium), 76 | ), 77 | Button.text( 78 | isLoading: _handler.isLoading, 79 | onPressed: 80 | content.isEmpty 81 | ? null 82 | : () { 83 | _handler.run( 84 | SendCommentPayload(id: widget.id, content: content), 85 | ); 86 | }, 87 | child: const Text('发送'), 88 | ), 89 | ], 90 | ), 91 | ], 92 | ), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/views/comments/thumb_up.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/mixin/auto_register_handler.dart'; 3 | import 'package:haka_comic/network/http.dart'; 4 | import 'package:haka_comic/utils/extension.dart'; 5 | import 'package:haka_comic/utils/log.dart'; 6 | import 'package:haka_comic/widgets/toast.dart'; 7 | 8 | class ThumbUp extends StatefulWidget { 9 | const ThumbUp({ 10 | super.key, 11 | required this.isLiked, 12 | required this.likesCount, 13 | required this.id, 14 | }); 15 | 16 | final bool isLiked; 17 | 18 | final int likesCount; 19 | 20 | final String id; 21 | 22 | @override 23 | State createState() => _ThumbUpState(); 24 | } 25 | 26 | class _ThumbUpState extends State with AutoRegisterHandlerMixin { 27 | late final _handler = likeComment.useRequest( 28 | onSuccess: (data, _) { 29 | Log.info('Like comment success', data.action); 30 | setState(() { 31 | _isLiked = !_isLiked; 32 | _likesCount += _isLiked ? 1 : -1; 33 | }); 34 | }, 35 | onError: (e, _) { 36 | Log.error('Like comment error', e); 37 | Toast.show(message: '点赞失败'); 38 | }, 39 | ); 40 | 41 | bool _isLiked = false; 42 | 43 | int _likesCount = 0; 44 | 45 | @override 46 | List registerHandler() => [_handler]; 47 | 48 | @override 49 | void initState() { 50 | super.initState(); 51 | _isLiked = widget.isLiked; 52 | _likesCount = widget.likesCount; 53 | 54 | _handler.isLoading = false; 55 | } 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | return InkWell( 60 | onTap: () { 61 | _handler.run(widget.id); 62 | }, 63 | borderRadius: const BorderRadius.all(Radius.circular(99)), 64 | child: Padding( 65 | padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), 66 | child: Row( 67 | spacing: 5, 68 | mainAxisSize: MainAxisSize.min, 69 | children: [ 70 | _handler.isLoading 71 | ? CircularProgressIndicator( 72 | constraints: BoxConstraints.tight(const Size(12, 12)), 73 | strokeWidth: 1, 74 | color: context.textTheme.bodySmall?.color, 75 | ) 76 | : Icon( 77 | _isLiked ? Icons.thumb_up : Icons.thumb_up_outlined, 78 | size: 16, 79 | ), 80 | Text(_likesCount.toString(), style: context.textTheme.bodySmall), 81 | ], 82 | ), 83 | ), 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/views/home/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/router/aware_page_wrapper.dart'; 3 | import 'package:haka_comic/utils/ui.dart'; 4 | import 'package:haka_comic/views/categories/categories.dart'; 5 | import 'package:haka_comic/views/home/navigation.dart'; 6 | import 'package:haka_comic/views/mine/mine.dart'; 7 | 8 | class Home extends StatefulWidget { 9 | const Home({super.key}); 10 | 11 | @override 12 | State createState() => _HomeState(); 13 | } 14 | 15 | class _HomeState extends State { 16 | int _selectedIndex = 0; 17 | 18 | Widget _buildAppNavigationBar() => AppNavigationBar( 19 | selectedIndex: _selectedIndex, 20 | onDestinationSelected: (int index) { 21 | setState(() { 22 | _selectedIndex = index; 23 | }); 24 | }, 25 | ); 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return RouteAwarePageWrapper( 30 | builder: (context, isRouteAnimationCompleted) { 31 | return Scaffold( 32 | extendBodyBehindAppBar: 33 | destinations[_selectedIndex]['extendBodyBehindAppBar'], 34 | appBar: 35 | UiMode.m1(context) 36 | ? destinations[_selectedIndex]['buildAppBar'](context) 37 | : null, 38 | body: Row( 39 | children: [ 40 | if (!UiMode.m1(context)) _buildAppNavigationBar(), 41 | Expanded( 42 | child: IndexedStack( 43 | index: _selectedIndex, 44 | children: [ 45 | Categories( 46 | isRouteAnimationCompleted: isRouteAnimationCompleted, 47 | ), 48 | const Mine(), 49 | ], 50 | ), 51 | ), 52 | ], 53 | ), 54 | bottomNavigationBar: 55 | UiMode.m1(context) ? _buildAppNavigationBar() : null, 56 | ); 57 | }, 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/views/home/share_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:go_router/go_router.dart'; 4 | import 'package:haka_comic/mixin/auto_register_handler.dart'; 5 | import 'package:haka_comic/network/http.dart'; 6 | import 'package:haka_comic/utils/extension.dart'; 7 | import 'package:haka_comic/utils/log.dart'; 8 | import 'package:haka_comic/widgets/button.dart'; 9 | import 'package:haka_comic/widgets/toast.dart'; 10 | 11 | class ShareDialog extends StatefulWidget { 12 | const ShareDialog({super.key}); 13 | 14 | @override 15 | State createState() => _ShareDialogState(); 16 | } 17 | 18 | class _ShareDialogState extends State 19 | with AutoRegisterHandlerMixin { 20 | final _controller = TextEditingController(); 21 | 22 | late final _handler = fetchComicIdByShareId.useRequest( 23 | onSuccess: (data, _) { 24 | Log.info('Fetch comic id by share id success', data.toString()); 25 | context.push('/details/$data'); 26 | context.pop(); 27 | }, 28 | onError: (e, _) { 29 | Log.error('Fetch comic id by share id error', e); 30 | Toast.show(message: '获取漫画失败'); 31 | }, 32 | ); 33 | 34 | @override 35 | List registerHandler() => [_handler]; 36 | 37 | Future _getClipboardData() async { 38 | try { 39 | final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); 40 | if (data != null && data.text!.startsWith('PICA')) { 41 | _controller.text = data.text!; 42 | } 43 | } catch (e) { 44 | Log.error('Get clipboard data error', e); 45 | } 46 | } 47 | 48 | @override 49 | void initState() { 50 | super.initState(); 51 | _handler.isLoading = false; 52 | _getClipboardData(); 53 | } 54 | 55 | @override 56 | void dispose() { 57 | _controller.dispose(); 58 | super.dispose(); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | return SimpleDialog( 64 | contentPadding: const EdgeInsets.all(20), 65 | title: const Text('漫画ID'), 66 | children: [ 67 | TextField( 68 | controller: _controller, 69 | decoration: const InputDecoration(border: OutlineInputBorder()), 70 | ), 71 | const SizedBox(height: 10), 72 | Button.filled( 73 | onPressed: () { 74 | final shareId = _controller.text; 75 | if (shareId.isEmpty || !shareId.contains('PICA')) { 76 | Toast.show(message: '请输入正确的分享ID'); 77 | return; 78 | } 79 | final id = shareId.split('PICA')[1]; 80 | _handler.run(id); 81 | }, 82 | isLoading: _handler.isLoading, 83 | child: const Text('查看'), 84 | ), 85 | ], 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/views/random/random.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/mixin/auto_register_handler.dart'; 3 | import 'package:haka_comic/mixin/blocked_words.dart'; 4 | import 'package:haka_comic/network/http.dart'; 5 | import 'package:haka_comic/router/aware_page_wrapper.dart'; 6 | import 'package:haka_comic/utils/extension.dart'; 7 | import 'package:haka_comic/utils/log.dart'; 8 | import 'package:haka_comic/views/comics/common_tmi_list.dart'; 9 | import 'package:haka_comic/widgets/base_page.dart'; 10 | 11 | class Random extends StatefulWidget { 12 | const Random({super.key}); 13 | 14 | @override 15 | State createState() => _RandomState(); 16 | } 17 | 18 | class _RandomState extends State 19 | with AutoRegisterHandlerMixin, BlockedWordsMixin { 20 | final _handler = fetchRandomComics.useRequest( 21 | onSuccess: (data, _) { 22 | Log.info('fetch random comics success', data.toString()); 23 | }, 24 | onError: (e, _) { 25 | Log.error('fetch random comics error', e); 26 | }, 27 | ); 28 | 29 | @override 30 | List registerHandler() => [_handler]; 31 | 32 | @override 33 | void initState() { 34 | super.initState(); 35 | _handler.run(); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | final comics = _handler.data?.comics ?? []; 41 | return RouteAwarePageWrapper( 42 | builder: (context, completed) { 43 | return Scaffold( 44 | appBar: AppBar(title: const Text('随机本子')), 45 | body: BasePage( 46 | isLoading: _handler.isLoading, 47 | onRetry: _handler.refresh, 48 | error: _handler.error, 49 | child: CommonTMIList( 50 | comics: comics, 51 | blockedTags: blockedTags, 52 | blockedWords: blockedWords, 53 | ), 54 | ), 55 | floatingActionButton: FloatingActionButton( 56 | onPressed: _handler.isLoading ? null : () => _handler.refresh(), 57 | child: const Icon(Icons.refresh), 58 | ), 59 | ); 60 | }, 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/views/reader/app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:haka_comic/model/reader_provider.dart'; 4 | import 'package:haka_comic/utils/common.dart'; 5 | import 'package:haka_comic/utils/extension.dart'; 6 | import 'package:haka_comic/views/reader/reader.dart'; 7 | import 'package:haka_comic/widgets/with_blur.dart'; 8 | 9 | /// 顶部工具栏 10 | class ReaderAppBar extends StatelessWidget { 11 | const ReaderAppBar({ 12 | super.key, 13 | required this.showToolbar, 14 | required this.readMode, 15 | required this.onReadModeChanged, 16 | }); 17 | 18 | final bool showToolbar; 19 | 20 | final ReadMode readMode; 21 | 22 | final ValueChanged onReadModeChanged; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | final top = context.top; 27 | return AnimatedPositioned( 28 | duration: const Duration(milliseconds: 250), 29 | top: showToolbar ? 0 : -(kToolbarHeight + top), 30 | left: 0, 31 | right: 0, 32 | height: kToolbarHeight + top, 33 | child: WithBlur( 34 | child: AppBar( 35 | leading: IconButton( 36 | icon: Icon( 37 | (isIos || isMacOS) ? Icons.arrow_back_ios_new : Icons.arrow_back, 38 | ), 39 | onPressed: () => context.pop(), 40 | ), 41 | actions: [ 42 | MenuAnchor( 43 | menuChildren: 44 | ReadMode.values.map((mode) { 45 | return MenuItemButton( 46 | onPressed: () => onReadModeChanged(mode), 47 | child: Row( 48 | spacing: 5, 49 | children: [ 50 | Text(readModeToString(mode)), 51 | if (mode == readMode) 52 | Icon( 53 | Icons.done, 54 | size: 16, 55 | color: context.colorScheme.primary, 56 | ), 57 | ], 58 | ), 59 | ); 60 | }).toList(), 61 | builder: (context, controller, child) { 62 | return IconButton( 63 | icon: const Icon(Icons.chrome_reader_mode_outlined), 64 | onPressed: () { 65 | if (controller.isOpen) { 66 | controller.close(); 67 | } else { 68 | controller.open(); 69 | } 70 | }, 71 | ); 72 | }, 73 | ), 74 | ], 75 | title: Text(context.reader.title), 76 | backgroundColor: context.colorScheme.surface.withValues(alpha: 0.92), 77 | ), 78 | ), 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/views/reader/comic_list_mixin.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:cached_network_image/cached_network_image.dart'; 3 | import 'package:flutter/widgets.dart'; 4 | import 'package:haka_comic/database/images_helper.dart'; 5 | import 'package:haka_comic/network/models.dart'; 6 | 7 | mixin ComicListMixin on State { 8 | @override 9 | dispose() { 10 | _preloadDebounceTimer?.cancel(); 11 | super.dispose(); 12 | } 13 | 14 | /// 已加载图片索引 - 用于避免重复预加载 15 | final Set _loadedImages = {}; 16 | 17 | // 最大预加载数量限制 18 | final int maxPreloadCount = 4; 19 | 20 | /// 防抖计时器,50ms内只处理最后一次预加载请求 21 | Timer? _preloadDebounceTimer; 22 | 23 | /// 预加载图片 24 | void preloadImages(int startIndex, int endIndex, List images) { 25 | // 取消之前的预加载计时器 26 | _preloadDebounceTimer?.cancel(); 27 | 28 | // 设置新的防抖计时器,50ms内只处理最后一次预加载请求 29 | _preloadDebounceTimer = Timer(const Duration(milliseconds: 50), () { 30 | // 确保方向正确 31 | final start = startIndex < endIndex ? startIndex : endIndex; 32 | final end = startIndex < endIndex ? endIndex : startIndex; 33 | 34 | for (int i = start; i <= end; i++) { 35 | // 检查索引是否有效 36 | if (i < 0 || i >= images.length) continue; 37 | // 避免重复加载 38 | if (_loadedImages.contains(i)) continue; 39 | 40 | final imageUrl = images[i].media.url; 41 | final imageProvider = CachedNetworkImageProvider(imageUrl); 42 | precacheImage(imageProvider, context); 43 | _loadedImages.add(i); 44 | } 45 | }); 46 | } 47 | 48 | /// 将图片尺寸信息插入数据库 49 | void insertImageSize(ImageSize imageSize) { 50 | ImagesHelper.insert(imageSize); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/views/reader/next_chapter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/utils/extension.dart'; 3 | 4 | class ReaderNextChapter extends StatelessWidget { 5 | const ReaderNextChapter({ 6 | super.key, 7 | required this.isShow, 8 | required this.onPressed, 9 | }); 10 | 11 | final bool isShow; 12 | 13 | final VoidCallback onPressed; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | // final isShow = 18 | // !loading && images.isNotEmpty && currentImageIndex >= images.length - 2; 19 | return Positioned( 20 | right: context.right + 16, 21 | bottom: context.bottom + 16, 22 | child: AnimatedOpacity( 23 | duration: const Duration(milliseconds: 200), 24 | opacity: isShow ? 1.0 : 0.0, 25 | child: AnimatedScale( 26 | duration: const Duration(milliseconds: 200), 27 | scale: isShow ? 1.0 : 0.0, 28 | child: IgnorePointer( 29 | ignoring: !isShow, 30 | child: FloatingActionButton( 31 | onPressed: onPressed, 32 | child: const Icon(Icons.skip_next), 33 | ), 34 | ), 35 | ), 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/views/reader/page_no_tag.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/utils/extension.dart'; 3 | import 'package:haka_comic/widgets/shadow_text.dart'; 4 | 5 | /// 页码 6 | class ReaderPageNoTag extends StatelessWidget { 7 | const ReaderPageNoTag({ 8 | super.key, 9 | required this.pageNo, 10 | required this.total, 11 | required this.title, 12 | }); 13 | 14 | /// 当前阅读的页码 15 | final int pageNo; 16 | 17 | /// 总页数 18 | final int total; 19 | 20 | /// 章节标题 21 | final String title; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Positioned( 26 | left: context.left + 12, 27 | bottom: context.bottom + 12, 28 | width: context.width / 2, 29 | child: Row( 30 | spacing: 5, 31 | children: [ 32 | Flexible(child: ShadowText(text: title)), 33 | ShadowText(text: '${pageNo + 1} / $total'), 34 | ], 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/views/search/hot_search_words.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:haka_comic/mixin/auto_register_handler.dart'; 4 | import 'package:haka_comic/model/search_provider.dart'; 5 | import 'package:haka_comic/network/http.dart'; 6 | import 'package:haka_comic/utils/extension.dart'; 7 | import 'package:haka_comic/utils/log.dart'; 8 | import 'package:haka_comic/views/search/item.dart'; 9 | import 'package:haka_comic/widgets/base_page.dart'; 10 | import 'package:provider/provider.dart'; 11 | 12 | class HotSearchWords extends StatefulWidget { 13 | const HotSearchWords({super.key, required this.isRouteAnimationCompleted}); 14 | 15 | final bool isRouteAnimationCompleted; 16 | 17 | @override 18 | State createState() => _HotSearchWordsState(); 19 | } 20 | 21 | class _HotSearchWordsState extends State 22 | with AutoRegisterHandlerMixin { 23 | final _handler = fetchHotSearchWords.useRequest( 24 | onSuccess: (data, _) { 25 | Log.info('Fetch hot search words success', data.toString()); 26 | }, 27 | onError: (e, _) { 28 | Log.error('Fetch hot search words error', e); 29 | }, 30 | ); 31 | 32 | @override 33 | List registerHandler() => [_handler]; 34 | 35 | @override 36 | void initState() { 37 | super.initState(); 38 | _handler.run(); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | final words = _handler.data?.keywords ?? []; 44 | return ConstrainedBox( 45 | constraints: const BoxConstraints(minHeight: 150), 46 | child: BasePage( 47 | error: _handler.error, 48 | isLoading: _handler.isLoading || !widget.isRouteAnimationCompleted, 49 | onRetry: _handler.refresh, 50 | errorBuilder: 51 | (context) => Center( 52 | child: IconButton( 53 | onPressed: _handler.refresh, 54 | icon: const Icon(Icons.refresh), 55 | ), 56 | ), 57 | child: Column( 58 | mainAxisSize: MainAxisSize.min, 59 | crossAxisAlignment: CrossAxisAlignment.start, 60 | spacing: 10, 61 | children: [ 62 | Row( 63 | spacing: 5, 64 | children: [ 65 | Text( 66 | '热门搜索', 67 | style: context.textTheme.titleMedium?.copyWith( 68 | fontWeight: FontWeight.bold, 69 | ), 70 | ), 71 | ], 72 | ), 73 | Wrap( 74 | spacing: 5, 75 | runSpacing: 5, 76 | children: 77 | words 78 | .map( 79 | (e) => Item( 80 | title: e, 81 | onTap: () { 82 | context.read().add(e); 83 | context.push('/search_comics?keyword=$e'); 84 | }, 85 | ), 86 | ) 87 | .toList(), 88 | ), 89 | ], 90 | ), 91 | ), 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/views/search/item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/utils/extension.dart'; 3 | 4 | class Item extends StatelessWidget { 5 | const Item({super.key, required this.title, this.onTap, this.color}); 6 | 7 | final String title; 8 | final VoidCallback? onTap; 9 | final Color? color; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Material( 14 | type: MaterialType.transparency, 15 | child: InkWell( 16 | onTap: onTap, 17 | borderRadius: BorderRadius.circular(8), 18 | child: Container( 19 | padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), 20 | decoration: BoxDecoration( 21 | color: 22 | color ?? context.colorScheme.surfaceDim.withValues(alpha: 0.45), 23 | borderRadius: BorderRadius.circular(8), 24 | ), 25 | child: Text(title, style: context.textTheme.bodyMedium), 26 | ), 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/views/search/search.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:haka_comic/model/search_provider.dart'; 4 | import 'package:haka_comic/router/aware_page_wrapper.dart'; 5 | import 'package:haka_comic/views/search/hot_search_words.dart'; 6 | import 'package:haka_comic/views/search/search_history.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class Search extends StatefulWidget { 10 | const Search({super.key}); 11 | 12 | @override 13 | State createState() => _SearchState(); 14 | } 15 | 16 | class _SearchState extends State { 17 | final TextEditingController _searchController = TextEditingController(); 18 | bool isRouteAnimationCompleted = false; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final List history = context.watch().history; 23 | return RouteAwarePageWrapper( 24 | builder: (context, completed) { 25 | return Scaffold( 26 | appBar: AppBar( 27 | title: TextField( 28 | controller: _searchController, 29 | textAlignVertical: TextAlignVertical.center, 30 | decoration: InputDecoration( 31 | hintText: '搜索', 32 | border: InputBorder.none, 33 | suffixIcon: IconButton( 34 | onPressed: () { 35 | if (_searchController.text.isEmpty) { 36 | context.pop(); 37 | } else { 38 | _searchController.clear(); 39 | } 40 | }, 41 | icon: const Icon(Icons.close), 42 | ), 43 | ), 44 | onSubmitted: (value) { 45 | if (value.isNotEmpty) { 46 | context.read().add(value); 47 | context.push('/search_comics?keyword=$value'); 48 | } 49 | }, 50 | ), 51 | ), 52 | body: Padding( 53 | padding: const EdgeInsets.all(10.0), 54 | child: Column( 55 | crossAxisAlignment: CrossAxisAlignment.start, 56 | spacing: history.isEmpty ? 0 : 20, 57 | children: [ 58 | const SearchHistory(), 59 | HotSearchWords(isRouteAnimationCompleted: completed), 60 | ], 61 | ), 62 | ), 63 | ); 64 | }, 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/views/search/search_history.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:haka_comic/model/search_provider.dart'; 4 | import 'package:haka_comic/utils/extension.dart'; 5 | import 'package:haka_comic/views/search/item.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class SearchHistory extends StatefulWidget { 9 | const SearchHistory({super.key}); 10 | 11 | @override 12 | State createState() => _SearchHistoryState(); 13 | } 14 | 15 | class _SearchHistoryState extends State { 16 | void clear() async { 17 | final result = await showDialog( 18 | context: context, 19 | builder: (context) { 20 | return AlertDialog( 21 | title: const Text('清除'), 22 | content: const Text('确定要清除所有搜索历史吗?'), 23 | actions: [ 24 | TextButton( 25 | onPressed: () => context.pop(false), 26 | child: const Text('取消'), 27 | ), 28 | TextButton( 29 | onPressed: () => context.pop(true), 30 | child: const Text('确定'), 31 | ), 32 | ], 33 | ); 34 | }, 35 | ); 36 | if (result == true) { 37 | if (mounted) { 38 | context.read().clear(); 39 | } 40 | } 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | final List history = context.watch().history; 46 | return history.isEmpty 47 | ? const SizedBox.shrink() 48 | : Column( 49 | mainAxisSize: MainAxisSize.min, 50 | crossAxisAlignment: CrossAxisAlignment.start, 51 | spacing: 2, 52 | children: [ 53 | Row( 54 | spacing: 5, 55 | children: [ 56 | Text( 57 | '搜索历史', 58 | style: context.textTheme.titleMedium?.copyWith( 59 | fontWeight: FontWeight.bold, 60 | ), 61 | ), 62 | const Spacer(), 63 | IconButton(onPressed: clear, icon: const Icon(Icons.clear_all)), 64 | ], 65 | ), 66 | Wrap( 67 | spacing: 5, 68 | runSpacing: 5, 69 | children: 70 | history 71 | .map( 72 | (e) => Item( 73 | title: e, 74 | onTap: () { 75 | context.read().add(e); 76 | context.push('/search_comics?keyword=$e'); 77 | }, 78 | ), 79 | ) 80 | .toList(), 81 | ), 82 | ], 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/views/settings/blacklist.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/config/app_config.dart'; 3 | 4 | /// 所有分类 5 | List categories = [ 6 | "嗶咔漢化", 7 | "全彩", 8 | "長篇", 9 | "同人", 10 | "短篇", 11 | "圓神領域", 12 | "碧藍幻想", 13 | "CG雜圖", 14 | "英語 ENG", 15 | "生肉", 16 | "純愛", 17 | "百合花園", 18 | "後宮閃光", 19 | "扶他樂園", 20 | "耽美花園", 21 | "偽娘哲學", 22 | "單行本", 23 | "姐姐系", 24 | "妹妹系", 25 | "性轉換", 26 | "SM", 27 | "足の恋", 28 | "人妻", 29 | "NTR", 30 | "強暴", 31 | "非人類", 32 | "艦隊收藏", 33 | "Love Live", 34 | "SAO 刀劍神域", 35 | "Fate", 36 | "東方", 37 | "WEBTOON", 38 | "禁書目錄", 39 | "歐美", 40 | "Cosplay", 41 | "重口地帶", 42 | ]; 43 | 44 | class Blacklist extends StatefulWidget { 45 | const Blacklist({super.key}); 46 | 47 | @override 48 | State createState() => _BlacklistState(); 49 | } 50 | 51 | class _BlacklistState extends State { 52 | List selectedCategories = AppConf().blacklist; 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | return Scaffold( 57 | appBar: AppBar(title: const Text('屏蔽')), 58 | body: ListView.builder( 59 | itemCount: categories.length, 60 | itemBuilder: (context, index) { 61 | final item = categories[index]; 62 | return SwitchListTile( 63 | title: Text(item), 64 | value: selectedCategories.contains(item), 65 | onChanged: (value) { 66 | setState(() { 67 | if (value) { 68 | selectedCategories.add(item); 69 | AppConf().blacklist = selectedCategories; 70 | } else { 71 | selectedCategories.remove(item); 72 | AppConf().blacklist = selectedCategories; 73 | } 74 | }); 75 | }, 76 | ); 77 | }, 78 | ), 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/views/settings/browse_mode.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/config/app_config.dart'; 3 | import 'package:haka_comic/views/settings/widgets/menu_list_tile.dart'; 4 | 5 | enum ComicBlockMode { simple, detailed } 6 | 7 | String comicBlockModeToString(ComicBlockMode mode) { 8 | return switch (mode) { 9 | ComicBlockMode.simple => '简洁', 10 | ComicBlockMode.detailed => '详细', 11 | }; 12 | } 13 | 14 | ComicBlockMode stringToComicBlockMode(String mode) { 15 | return switch (mode) { 16 | '简洁' => ComicBlockMode.simple, 17 | '详细' => ComicBlockMode.detailed, 18 | _ => ComicBlockMode.detailed, 19 | }; 20 | } 21 | 22 | class BrowseMode extends StatefulWidget { 23 | const BrowseMode({super.key}); 24 | 25 | @override 26 | State createState() => _BrowseModeState(); 27 | } 28 | 29 | class _BrowseModeState extends State { 30 | ComicBlockMode _mode = AppConf().comicBlockMode; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return MenuListTile.withValue( 35 | icon: Icons.view_day_outlined, 36 | title: '漫画块', 37 | value: comicBlockModeToString(_mode), 38 | items: 39 | ComicBlockMode.values.map((mode) { 40 | return PopupMenuItem( 41 | value: mode, 42 | child: ListTile( 43 | leading: Icon( 44 | _mode == mode 45 | ? Icons.check_circle 46 | : Icons.radio_button_unchecked, 47 | color: Theme.of(context).primaryColor, 48 | ), 49 | title: Text(comicBlockModeToString(mode)), 50 | ), 51 | ); 52 | }).toList(), 53 | onSelected: (value) { 54 | setState(() { 55 | _mode = value; 56 | AppConf().comicBlockMode = value; 57 | }); 58 | }, 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/views/settings/change_image_quality.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/config/app_config.dart'; 3 | import 'package:haka_comic/network/utils.dart'; 4 | import 'package:haka_comic/views/settings/widgets/menu_list_tile.dart'; 5 | 6 | class ChangeImageQuality extends StatefulWidget { 7 | const ChangeImageQuality({super.key}); 8 | 9 | @override 10 | State createState() => _ChangeImageQualityState(); 11 | } 12 | 13 | class _ChangeImageQualityState extends State { 14 | ImageQuality _imageQuality = AppConf().imageQuality; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return MenuListTile.withValue( 19 | icon: Icons.image_outlined, 20 | title: '图片质量', 21 | value: getImageQualityDisplayName(_imageQuality), 22 | items: 23 | ImageQuality.values.map((imageQuality) { 24 | return PopupMenuItem( 25 | value: imageQuality, 26 | child: ListTile( 27 | leading: Icon( 28 | _imageQuality == imageQuality 29 | ? Icons.check_circle 30 | : Icons.radio_button_unchecked, 31 | color: Theme.of(context).primaryColor, 32 | ), 33 | title: Text(getImageQualityDisplayName(imageQuality)), 34 | ), 35 | ); 36 | }).toList(), 37 | onSelected: 38 | (value) => setState(() { 39 | _imageQuality = value; 40 | AppConf().imageQuality = value; 41 | }), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/views/settings/change_password.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:haka_comic/config/app_config.dart'; 4 | import 'package:haka_comic/network/http.dart'; 5 | import 'package:haka_comic/network/models.dart'; 6 | import 'package:haka_comic/utils/extension.dart'; 7 | import 'package:haka_comic/views/settings/widgets/menu_list_tile.dart'; 8 | import 'package:haka_comic/widgets/button.dart'; 9 | import 'package:haka_comic/widgets/toast.dart'; 10 | 11 | class ChangePassword extends StatelessWidget { 12 | const ChangePassword({super.key}); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return MenuListTile.withAction( 17 | icon: Icons.edit_outlined, 18 | title: '修改密码', 19 | onTap: () { 20 | showDialog( 21 | context: context, 22 | builder: (context) => const ChangePassWordDialog(), 23 | ); 24 | }, 25 | ); 26 | } 27 | } 28 | 29 | class ChangePassWordDialog extends StatefulWidget { 30 | const ChangePassWordDialog({super.key}); 31 | 32 | @override 33 | State createState() => _ChangePassWordDialogState(); 34 | } 35 | 36 | class _ChangePassWordDialogState extends State { 37 | final controller = TextEditingController(); 38 | 39 | late final _handler = updatePassword.useRequest( 40 | onSuccess: (data, _) { 41 | Toast.show(message: '修改成功'); 42 | context.pop(); 43 | }, 44 | onError: (e, _) { 45 | Toast.show(message: '修改失败'); 46 | }, 47 | ); 48 | 49 | void _update() => setState(() {}); 50 | 51 | @override 52 | void initState() { 53 | super.initState(); 54 | _handler 55 | ..addListener(_update) 56 | ..isLoading = false; 57 | } 58 | 59 | @override 60 | void dispose() { 61 | _handler.dispose(); 62 | super.dispose(); 63 | } 64 | 65 | @override 66 | Widget build(BuildContext context) { 67 | return SimpleDialog( 68 | contentPadding: const EdgeInsets.all(20), 69 | title: const Text('修改密码'), 70 | children: [ 71 | TextField( 72 | controller: controller, 73 | autofocus: true, 74 | decoration: const InputDecoration( 75 | border: OutlineInputBorder(), 76 | hintText: '新密码', 77 | ), 78 | ), 79 | const SizedBox(height: 10), 80 | Button.filled( 81 | onPressed: () { 82 | final password = controller.text; 83 | if (password.isEmpty) { 84 | Toast.show(message: '请输入新密码'); 85 | return; 86 | } 87 | _handler.run( 88 | UpdatePasswordPayload( 89 | newPassword: password, 90 | oldPassword: AppConf.instance.password, 91 | ), 92 | ); 93 | }, 94 | isLoading: _handler.isLoading, 95 | child: const Text('确定'), 96 | ), 97 | ], 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/views/settings/change_server.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/config/app_config.dart'; 3 | import 'package:haka_comic/network/utils.dart'; 4 | import 'package:haka_comic/views/settings/widgets/menu_list_tile.dart'; 5 | 6 | class ChangeServer extends StatefulWidget { 7 | const ChangeServer({super.key}); 8 | 9 | @override 10 | State createState() => _ChangeServerState(); 11 | } 12 | 13 | class _ChangeServerState extends State { 14 | Server _server = AppConf().server; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return MenuListTile.withValue( 19 | icon: Icons.swap_horiz_outlined, 20 | title: '切换分流', 21 | value: getServerDisplayName(_server), 22 | items: 23 | Server.values.map((server) { 24 | return PopupMenuItem( 25 | value: server, 26 | child: ListTile( 27 | leading: Icon( 28 | _server == server 29 | ? Icons.check_circle 30 | : Icons.radio_button_unchecked, 31 | color: Theme.of(context).primaryColor, 32 | ), 33 | title: Text(getServerDisplayName(server)), 34 | ), 35 | ); 36 | }).toList(), 37 | onSelected: 38 | (value) => setState(() { 39 | _server = value; 40 | AppConf().server = value; 41 | }), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/views/settings/comic_block_scale.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/config/app_config.dart'; 3 | import 'package:haka_comic/utils/extension.dart'; 4 | 5 | class ComicBlockScale extends StatefulWidget { 6 | const ComicBlockScale({super.key}); 7 | 8 | @override 9 | State createState() => _ComicBlockScaleState(); 10 | } 11 | 12 | class _ComicBlockScaleState extends State { 13 | double _scale = AppConf().scale; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return ListTile( 18 | title: const Text('简洁漫画块大小'), 19 | leading: Container( 20 | padding: const EdgeInsets.all(6), 21 | decoration: BoxDecoration( 22 | color: context.colorScheme.primary.withValues(alpha: 0.1), 23 | shape: BoxShape.circle, 24 | ), 25 | child: const Icon(Icons.grid_3x3_outlined, size: 22), 26 | ), 27 | subtitle: Column( 28 | mainAxisSize: MainAxisSize.min, 29 | children: [ 30 | const SizedBox(height: 8), 31 | Slider( 32 | padding: const EdgeInsets.symmetric(horizontal: 0.0), 33 | value: _scale, 34 | max: 2.0, 35 | min: 0.5, 36 | divisions: 15, 37 | label: _scale.toString(), 38 | onChanged: (double value) { 39 | setState(() { 40 | _scale = value; 41 | AppConf().scale = value; 42 | }); 43 | }, 44 | ), 45 | ], 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/views/settings/logout.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:haka_comic/router/app_router.dart'; 4 | import 'package:haka_comic/views/settings/widgets/menu_list_tile.dart'; 5 | 6 | class Logout extends StatefulWidget { 7 | const Logout({super.key}); 8 | 9 | @override 10 | State createState() => _LogoutState(); 11 | } 12 | 13 | class _LogoutState extends State { 14 | @override 15 | Widget build(BuildContext context) { 16 | return MenuListTile.withAction( 17 | icon: Icons.logout_outlined, 18 | title: '退出登录', 19 | onTap: () async { 20 | final bool? result = await showDialog( 21 | context: context, 22 | builder: (BuildContext context) { 23 | return AlertDialog( 24 | title: const Text('退出登录'), 25 | content: const Text('确定要退出登录吗?'), 26 | actions: [ 27 | TextButton( 28 | child: const Text('取消'), 29 | onPressed: () => context.pop(false), 30 | ), 31 | TextButton( 32 | child: const Text('确定'), 33 | onPressed: () => context.pop(true), 34 | ), 35 | ], 36 | ); 37 | }, 38 | ); 39 | if (result == true) { 40 | logout(); 41 | } 42 | }, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/views/settings/network.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/config/app_config.dart'; 3 | import 'package:haka_comic/network/client.dart'; 4 | import 'package:haka_comic/network/utils.dart'; 5 | import 'package:haka_comic/utils/extension.dart'; 6 | import 'package:haka_comic/views/settings/widgets/menu_list_tile.dart'; 7 | import 'package:haka_comic/widgets/toast.dart'; 8 | 9 | class Network extends StatefulWidget { 10 | const Network({super.key}); 11 | 12 | @override 13 | State createState() => _NetworkState(); 14 | } 15 | 16 | class _NetworkState extends State { 17 | Api _api = AppConf().api; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return MenuListTile.withValue( 22 | icon: Icons.network_check_outlined, 23 | title: 'API切换', 24 | value: _api.value, 25 | items: 26 | Api.values.map((api) { 27 | return PopupMenuItem( 28 | value: api.value, 29 | child: ListTile( 30 | leading: Icon( 31 | _api.value == api.value 32 | ? Icons.check_circle 33 | : Icons.radio_button_unchecked, 34 | color: context.colorScheme.primary, 35 | ), 36 | title: Text(api.name), 37 | ), 38 | ); 39 | }).toList(), 40 | onSelected: (value) { 41 | if (value == _api.value) return; 42 | setState(() { 43 | final api = Api.fromValue(value); 44 | _api = api; 45 | AppConf().api = api; 46 | Client.setBaseUrl(api.host); 47 | }); 48 | Toast.show(message: '建议重启应用以确保切换生效'); 49 | }, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/views/settings/pager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/config/app_config.dart'; 3 | import 'package:haka_comic/views/settings/widgets/menu_list_tile.dart'; 4 | 5 | class Pager extends StatefulWidget { 6 | const Pager({super.key}); 7 | 8 | @override 9 | State createState() => _PagerState(); 10 | } 11 | 12 | class _PagerState extends State { 13 | bool _pagination = AppConf().pagination; 14 | final maps = {'分页': true, '连续': false}; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return MenuListTile.withValue( 19 | icon: Icons.unfold_more_outlined, 20 | title: '分页模式', 21 | value: _pagination ? '分页' : '连续', 22 | items: 23 | maps.entries.map((entry) { 24 | return PopupMenuItem( 25 | value: entry.value, 26 | child: ListTile( 27 | leading: Icon( 28 | _pagination == entry.value 29 | ? Icons.check_circle 30 | : Icons.radio_button_unchecked, 31 | color: Theme.of(context).primaryColor, 32 | ), 33 | title: Text(entry.key), 34 | ), 35 | ); 36 | }).toList(), 37 | onSelected: (value) { 38 | setState(() { 39 | _pagination = value; 40 | AppConf().pagination = value; 41 | }); 42 | }, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/views/settings/read_mode.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/config/app_config.dart'; 3 | import 'package:haka_comic/model/reader_provider.dart'; 4 | import 'package:haka_comic/views/settings/widgets/menu_list_tile.dart'; 5 | 6 | class ReadModeChanger extends StatefulWidget { 7 | const ReadModeChanger({super.key}); 8 | 9 | @override 10 | State createState() => _ReadModeChangerState(); 11 | } 12 | 13 | class _ReadModeChangerState extends State { 14 | ReadMode _readMode = AppConf().readMode; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return MenuListTile.withValue( 19 | icon: Icons.chrome_reader_mode_outlined, 20 | title: '阅读模式', 21 | value: readModeToString(_readMode), 22 | items: 23 | ReadMode.values.map((mode) { 24 | return PopupMenuItem( 25 | value: mode, 26 | child: ListTile( 27 | leading: Icon( 28 | _readMode == mode 29 | ? Icons.check_circle 30 | : Icons.radio_button_unchecked, 31 | color: Theme.of(context).primaryColor, 32 | ), 33 | title: Text(readModeToString(mode)), 34 | ), 35 | ); 36 | }).toList(), 37 | onSelected: (value) { 38 | setState(() { 39 | _readMode = value; 40 | AppConf().readMode = value; 41 | }); 42 | }, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/views/settings/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/model/theme_provider.dart'; 3 | import 'package:haka_comic/utils/extension.dart'; 4 | import 'package:haka_comic/views/settings/theme_icon.dart'; 5 | import 'package:haka_comic/views/settings/theme_switch.dart'; 6 | import 'package:haka_comic/views/settings/widgets/menu_list_tile.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class Theme extends StatelessWidget { 10 | const Theme({super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final ThemeMode themeMode = context.select( 15 | (data) => data.themeMode, 16 | ); 17 | return MenuListTile.withAction( 18 | icon: Icons.contrast_outlined, 19 | title: '主题模式', 20 | value: ThemeProvider.themeModeToString[themeMode] ?? 'System', 21 | onTap: () { 22 | showModalBottomSheet( 23 | context: context, 24 | shape: const RoundedRectangleBorder( 25 | borderRadius: BorderRadius.all(Radius.circular(20)), 26 | ), 27 | useSafeArea: true, 28 | constraints: const BoxConstraints(maxWidth: 400), 29 | isScrollControlled: true, 30 | builder: (context) { 31 | return Padding( 32 | padding: const EdgeInsets.fromLTRB(16, 20, 16, 20), 33 | child: Column( 34 | mainAxisSize: MainAxisSize.min, 35 | children: [ 36 | const ThemeIcon(), 37 | const SizedBox(height: 20), 38 | Text('选择主题模式', style: context.textTheme.titleMedium), 39 | Text( 40 | '选择System,亮暗模式会随着系统模式的变化而变化', 41 | style: context.textTheme.bodySmall, 42 | ), 43 | const SizedBox(height: 20), 44 | const ThemeSwitch(), 45 | const SizedBox(height: 20), 46 | ], 47 | ), 48 | ); 49 | }, 50 | ); 51 | }, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/views/settings/theme_color.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:haka_comic/model/theme_provider.dart'; 4 | import 'package:haka_comic/views/settings/widgets/menu_list_tile.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class ThemeColor extends StatelessWidget { 8 | const ThemeColor({super.key}); 9 | 10 | static final List _colors = [ 11 | 'System', 12 | 'Red', 13 | 'Pink', 14 | 'Green', 15 | 'Blue', 16 | 'Yellow', 17 | 'Orange', 18 | 'Purple', 19 | ]; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | final color = context.select( 24 | (values) => values.primaryColor, 25 | ); 26 | 27 | return MenuListTile.withValue( 28 | title: '主题颜色', 29 | value: color, 30 | icon: Icons.color_lens_outlined, 31 | items: 32 | _colors.map((String color) { 33 | return PopupMenuItem( 34 | value: color, 35 | child: ListTile( 36 | leading: 37 | color == 'System' 38 | ? Container( 39 | width: 20, 40 | height: 20, 41 | decoration: BoxDecoration( 42 | gradient: SweepGradient( 43 | colors: [ 44 | Colors.red, 45 | Colors.pink, 46 | Colors.green, 47 | Colors.blue, 48 | Colors.yellow, 49 | Colors.orange, 50 | Colors.purple, 51 | ], 52 | stops: _generateStops(7), 53 | center: Alignment.center, 54 | startAngle: 0, 55 | endAngle: 2 * pi, 56 | tileMode: TileMode.clamp, 57 | ), 58 | shape: BoxShape.circle, 59 | ), 60 | child: const Center( 61 | child: Icon( 62 | Icons.settings, 63 | size: 18, 64 | color: Colors.white, 65 | ), 66 | ), 67 | ) 68 | : Container( 69 | width: 20, 70 | height: 20, 71 | decoration: BoxDecoration( 72 | color: ThemeProvider.stringToColor(color), 73 | shape: BoxShape.circle, 74 | ), 75 | ), 76 | title: Text(color), 77 | ), 78 | ); 79 | }).toList(), 80 | onSelected: 81 | (value) => context.read().setPrimaryColor(value), 82 | ); 83 | } 84 | 85 | List _generateStops(int count) { 86 | final stops = []; 87 | for (int i = 0; i < count; i++) { 88 | stops.add(i / (count - 1)); 89 | } 90 | return stops; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/views/settings/visible_categories.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/config/app_config.dart'; 3 | 4 | /// 所有分类 5 | List categories = [ 6 | "排行榜", 7 | "留言板", 8 | "最近更新", 9 | "随机本子", 10 | "大家都在看", 11 | "大濕推薦", 12 | "那年今天", 13 | "官方都在看", 14 | "嗶咔漢化", 15 | "全彩", 16 | "長篇", 17 | "同人", 18 | "短篇", 19 | "圓神領域", 20 | "碧藍幻想", 21 | "CG雜圖", 22 | "英語 ENG", 23 | "生肉", 24 | "純愛", 25 | "百合花園", 26 | "後宮閃光", 27 | "扶他樂園", 28 | "耽美花園", 29 | "偽娘哲學", 30 | "單行本", 31 | "姐姐系", 32 | "妹妹系", 33 | "性轉換", 34 | "SM", 35 | "足の恋", 36 | "人妻", 37 | "NTR", 38 | "強暴", 39 | "非人類", 40 | "艦隊收藏", 41 | "Love Live", 42 | "SAO 刀劍神域", 43 | "Fate", 44 | "東方", 45 | "WEBTOON", 46 | "禁書目錄", 47 | "歐美", 48 | "Cosplay", 49 | "重口地帶", 50 | ]; 51 | 52 | class VisibleCategories extends StatefulWidget { 53 | const VisibleCategories({super.key}); 54 | 55 | @override 56 | State createState() => _VisibleCategoriesState(); 57 | } 58 | 59 | class _VisibleCategoriesState extends State { 60 | List selectedCategories = List.from( 61 | AppConf().visibleCategories.isEmpty 62 | ? categories 63 | : AppConf().visibleCategories, 64 | ); 65 | 66 | @override 67 | Widget build(BuildContext context) { 68 | return Scaffold( 69 | appBar: AppBar(title: const Text('显示的分类')), 70 | body: ListView.builder( 71 | itemCount: categories.length, 72 | itemBuilder: (context, index) { 73 | final item = categories[index]; 74 | return SwitchListTile( 75 | title: Text(item), 76 | value: selectedCategories.contains(item), 77 | onChanged: (value) { 78 | setState(() { 79 | if (value) { 80 | selectedCategories.add(item); 81 | AppConf().visibleCategories = selectedCategories; 82 | } else { 83 | selectedCategories.remove(item); 84 | AppConf().visibleCategories = selectedCategories; 85 | } 86 | }); 87 | }, 88 | ); 89 | }, 90 | ), 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/views/settings/widgets/block.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/utils/extension.dart'; 3 | 4 | class Block extends StatelessWidget { 5 | const Block({super.key, required this.children, this.title}); 6 | 7 | final List children; 8 | final String? title; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Column( 13 | mainAxisSize: MainAxisSize.min, 14 | crossAxisAlignment: CrossAxisAlignment.start, 15 | children: [ 16 | if (title != null) 17 | Padding( 18 | padding: const EdgeInsets.fromLTRB(12.0, 0, 12.0, 8.0), 19 | child: Text( 20 | title!, 21 | style: context.textTheme.bodySmall?.copyWith( 22 | color: context.colorScheme.onSurfaceVariant, 23 | ), 24 | ), 25 | ), 26 | Container( 27 | clipBehavior: Clip.hardEdge, 28 | decoration: BoxDecoration( 29 | color: context.colorScheme.secondaryContainer.withValues( 30 | alpha: 0.45, 31 | ), 32 | borderRadius: const BorderRadius.all(Radius.circular(10)), 33 | ), 34 | child: Material( 35 | type: MaterialType.transparency, 36 | child: Column(mainAxisSize: MainAxisSize.min, children: children), 37 | ), 38 | ), 39 | ], 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/views/settings/widgets/menu_list_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/utils/extension.dart'; 3 | 4 | class MenuListTile extends StatelessWidget { 5 | const MenuListTile.withValue({ 6 | super.key, 7 | required this.icon, 8 | required this.title, 9 | required this.value, 10 | required this.items, 11 | this.onSelected, 12 | }) : onTap = null; 13 | 14 | const MenuListTile.withAction({ 15 | super.key, 16 | required this.icon, 17 | required this.title, 18 | required this.onTap, 19 | this.value, 20 | }) : items = null, 21 | onSelected = null; 22 | 23 | final String title; 24 | final String? value; 25 | final IconData icon; 26 | final List>? items; 27 | final ValueChanged? onSelected; 28 | final VoidCallback? onTap; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return ListTile( 33 | title: Text(title), 34 | leading: Container( 35 | padding: const EdgeInsets.all(6), 36 | decoration: BoxDecoration( 37 | color: context.colorScheme.primary.withValues(alpha: 0.1), 38 | shape: BoxShape.circle, 39 | ), 40 | child: Icon(icon, size: 22), 41 | ), 42 | trailing: Row( 43 | mainAxisSize: MainAxisSize.min, 44 | spacing: 5.0, 45 | children: [ 46 | if (value != null) Text(value!, style: const TextStyle(fontSize: 12)), 47 | const Icon(Icons.chevron_right), 48 | ], 49 | ), 50 | onTap: onTap ?? () => _showMenu(context), 51 | ); 52 | } 53 | 54 | void _showMenu(BuildContext context) async { 55 | final RenderBox button = context.findRenderObject() as RenderBox; 56 | final RenderBox overlay = 57 | Overlay.of(context).context.findRenderObject() as RenderBox; 58 | final RelativeRect position = RelativeRect.fromRect( 59 | Rect.fromPoints( 60 | button.localToGlobal(Offset.zero, ancestor: overlay), 61 | button.localToGlobal( 62 | button.size.bottomRight(Offset.zero), 63 | ancestor: overlay, 64 | ), 65 | ), 66 | Offset.zero & overlay.size, 67 | ); 68 | 69 | final value = await showMenu( 70 | context: context, 71 | position: position, 72 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), 73 | items: items!, 74 | ); 75 | 76 | if (value != null) { 77 | onSelected?.call(value); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/widgets/base_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class BaseImage extends StatefulWidget { 5 | const BaseImage({ 6 | super.key, 7 | this.url = '', 8 | this.aspectRatio, 9 | this.fit = BoxFit.cover, 10 | this.width, 11 | this.height, 12 | this.progressIndicatorBuilder, 13 | this.errorBuilder, 14 | this.shape, 15 | this.imageBuilder, 16 | }); 17 | 18 | final String url; 19 | 20 | final double? aspectRatio; 21 | 22 | final BoxFit fit; 23 | 24 | final double? width; 25 | 26 | final double? height; 27 | 28 | final ShapeBorder? shape; 29 | 30 | final Widget Function(BuildContext, String, DownloadProgress)? 31 | progressIndicatorBuilder; 32 | 33 | final Widget Function(BuildContext, String, Object)? errorBuilder; 34 | 35 | final Widget Function(BuildContext, ImageProvider)? imageBuilder; 36 | 37 | @override 38 | State createState() => _BaseImageState(); 39 | } 40 | 41 | class _BaseImageState extends State { 42 | final ValueNotifier keyNotifier = ValueNotifier( 43 | UniqueKey(), 44 | ); 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return widget.aspectRatio == null ? _buildImage() : _buildAspectRatio(); 49 | } 50 | 51 | Widget _buildAspectRatio() { 52 | return AspectRatio(aspectRatio: widget.aspectRatio!, child: _buildImage()); 53 | } 54 | 55 | Widget _buildImage() { 56 | return Card( 57 | clipBehavior: Clip.hardEdge, 58 | shape: widget.shape, 59 | elevation: 0, 60 | child: ValueListenableBuilder( 61 | valueListenable: keyNotifier, 62 | builder: 63 | (context, value, child) => 64 | widget.url.isEmpty 65 | ? Image.asset( 66 | 'assets/images/login.png', 67 | fit: widget.fit, 68 | width: widget.width ?? double.infinity, 69 | height: widget.height ?? double.infinity, 70 | ) 71 | : CachedNetworkImage( 72 | key: value, 73 | imageUrl: widget.url, 74 | fit: widget.fit, 75 | width: widget.width ?? double.infinity, 76 | height: widget.height ?? double.infinity, 77 | progressIndicatorBuilder: widget.progressIndicatorBuilder, 78 | errorWidget: 79 | widget.errorBuilder ?? 80 | (context, url, error) => Center( 81 | child: IconButton( 82 | onPressed: () { 83 | keyNotifier.value = UniqueKey(); 84 | }, 85 | icon: const Icon(Icons.refresh), 86 | ), 87 | ), 88 | imageBuilder: widget.imageBuilder, 89 | ), 90 | ), 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/widgets/base_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:haka_comic/utils/common.dart'; 3 | import 'package:haka_comic/widgets/error_page.dart'; 4 | 5 | class BasePage extends StatefulWidget { 6 | const BasePage({ 7 | super.key, 8 | this.error, 9 | required this.isLoading, 10 | required this.onRetry, 11 | required this.child, 12 | this.indicatorBuilder, 13 | this.errorBuilder, 14 | }); 15 | 16 | final Object? error; 17 | 18 | final bool isLoading; 19 | 20 | final VoidCallback onRetry; 21 | 22 | final Widget child; 23 | 24 | final Widget Function(BuildContext)? indicatorBuilder; 25 | 26 | final Widget Function(BuildContext)? errorBuilder; 27 | 28 | @override 29 | State createState() => _BasePageState(); 30 | } 31 | 32 | class _BasePageState extends State { 33 | @override 34 | Widget build(BuildContext context) { 35 | return widget.error != null 36 | ? widget.errorBuilder != null 37 | ? widget.errorBuilder!(context) 38 | : ErrorPage( 39 | errorMessage: getTextBeforeNewLine(widget.error.toString()), 40 | onRetry: widget.onRetry, 41 | ) 42 | : widget.isLoading 43 | ? widget.indicatorBuilder != null 44 | ? widget.indicatorBuilder!(context) 45 | : const Center(child: CircularProgressIndicator()) 46 | : widget.child; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/widgets/button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum ButtonType { filled, text } 4 | 5 | class Button extends StatefulWidget { 6 | final ButtonType type; 7 | 8 | final Widget child; 9 | 10 | final void Function()? onPressed; 11 | 12 | final bool isLoading; 13 | 14 | const Button({ 15 | super.key, 16 | required this.type, 17 | required this.child, 18 | this.onPressed, 19 | this.isLoading = false, 20 | }); 21 | 22 | const Button.filled({ 23 | super.key, 24 | required this.child, 25 | this.onPressed, 26 | this.isLoading = false, 27 | }) : type = ButtonType.filled; 28 | 29 | const Button.text({ 30 | super.key, 31 | required this.child, 32 | this.onPressed, 33 | this.isLoading = false, 34 | }) : type = ButtonType.text; 35 | 36 | @override 37 | State