├── .github ├── ISSUE_TEMPLATE │ ├── bug.yaml │ ├── enhancement.yaml │ └── other.yaml └── workflows │ ├── delete_old_workflows.yml │ ├── ios_simulator.yml │ ├── linux.yml │ └── main.yml ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── kokoiro │ │ │ │ └── xyz │ │ │ │ └── pica_comic │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── notification.png │ │ │ ├── drawable-mdpi │ │ │ └── notification.png │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ └── notification.png │ │ │ ├── drawable-xxhdpi │ │ │ └── notification.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── notification.png │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── init.js ├── tags.json ├── tags_tw.json └── translation.json ├── debian ├── build.py ├── debian.yaml └── gui │ ├── pica-comic.desktop │ └── pica-comic.png ├── doc ├── comic_source.md └── hosts.md ├── fonts └── NotoSansSC-Regular.ttf ├── images ├── app_icon.png ├── app_icon_no_bg.png ├── avatar.png ├── avatar_small.png └── github.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── 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 │ │ ├── AppIcon-20@2x.png │ │ ├── AppIcon-20@2x~ipad.png │ │ ├── AppIcon-20@3x.png │ │ ├── AppIcon-20~ipad.png │ │ ├── AppIcon-29.png │ │ ├── AppIcon-29@2x.png │ │ ├── AppIcon-29@2x~ipad.png │ │ ├── AppIcon-29@3x.png │ │ ├── AppIcon-29~ipad.png │ │ ├── AppIcon-40@2x.png │ │ ├── AppIcon-40@2x~ipad.png │ │ ├── AppIcon-40@3x.png │ │ ├── AppIcon-40~ipad.png │ │ ├── AppIcon-60@2x~car.png │ │ ├── AppIcon-60@3x~car.png │ │ ├── AppIcon-83.5@2x~ipad.png │ │ ├── AppIcon@2x.png │ │ ├── AppIcon@2x~ipad.png │ │ ├── AppIcon@3x.png │ │ ├── AppIcon~ios-marketing.png │ │ ├── AppIcon~ipad.png │ │ └── Contents.json │ └── 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 ├── lib ├── base.dart ├── comic_source │ ├── app_build_in_category.dart │ ├── app_build_in_favorites.dart │ ├── built_in │ │ ├── ehentai.dart │ │ ├── hitomi.dart │ │ ├── ht_manga.dart │ │ ├── jm.dart │ │ ├── nhentai.dart │ │ └── picacg.dart │ ├── category.dart │ ├── comic_source.dart │ ├── favorites.dart │ └── parser.dart ├── components │ ├── animated_image.dart │ ├── appbar.dart │ ├── avatar.dart │ ├── button.dart │ ├── comic_tile.dart │ ├── comics_list.dart │ ├── comment.dart │ ├── components.dart │ ├── consts.dart │ ├── custom_slider.dart │ ├── flyout.dart │ ├── layout.dart │ ├── loading.dart │ ├── menu.dart │ ├── message.dart │ ├── navigation_bar.dart │ ├── pop_up_widget.dart │ ├── scroll.dart │ ├── scrollable_list │ │ ├── scrollable_positioned_list.dart │ │ └── src │ │ │ ├── element_registry.dart │ │ │ ├── item_positions_listener.dart │ │ │ ├── item_positions_notifier.dart │ │ │ ├── positioned_list.dart │ │ │ ├── post_mount_callback.dart │ │ │ ├── scroll_view.dart │ │ │ ├── scrollable_positioned_list.dart │ │ │ ├── viewport.dart │ │ │ └── wrapping.dart │ ├── select.dart │ ├── select_download_eps.dart │ ├── side_bar.dart │ └── window_frame.dart ├── foundation │ ├── app.dart │ ├── app_page_route.dart │ ├── cache_manager.dart │ ├── def.dart │ ├── history.dart │ ├── image_favorites.dart │ ├── image_loader │ │ ├── base_image_provider.dart │ │ ├── cached_image.dart │ │ ├── file_image_loader.dart │ │ ├── image_recombine.dart │ │ └── stream_image_provider.dart │ ├── image_manager.dart │ ├── js_engine.dart │ ├── local_favorites.dart │ ├── log.dart │ ├── pair.dart │ ├── stack.dart │ ├── state_controller.dart │ ├── ui_mode.dart │ └── widget_utils.dart ├── init.dart ├── main.dart ├── network │ ├── app_dio.dart │ ├── base_comic.dart │ ├── cache_network.dart │ ├── cloudflare.dart │ ├── cookie_jar.dart │ ├── custom_download_model.dart │ ├── download.dart │ ├── download_model.dart │ ├── eh_network │ │ ├── eh_download_model.dart │ │ ├── eh_main_network.dart │ │ ├── eh_models.dart │ │ └── get_gallery_id.dart │ ├── favorite_download.dart │ ├── file_downloader.dart │ ├── hitomi_network │ │ ├── fetch_data.dart │ │ ├── hitomi_download_model.dart │ │ ├── hitomi_main_network.dart │ │ ├── hitomi_models.dart │ │ ├── image.dart │ │ └── search.dart │ ├── htmanga_network │ │ ├── ht_download_model.dart │ │ ├── htmanga_main_network.dart │ │ └── models.dart │ ├── http_client.dart │ ├── http_proxy.dart │ ├── jm_network │ │ ├── headers.dart │ │ ├── jm_download.dart │ │ ├── jm_image.dart │ │ ├── jm_models.dart │ │ └── jm_network.dart │ ├── net_fav_to_local.dart │ ├── nhentai_network │ │ ├── download.dart │ │ ├── login.dart │ │ ├── models.dart │ │ ├── nhentai_main_network.dart │ │ └── tags.dart │ ├── picacg_network │ │ ├── headers.dart │ │ ├── methods.dart │ │ ├── models.dart │ │ └── picacg_download_model.dart │ ├── res.dart │ ├── update.dart │ └── webdav.dart ├── pages │ ├── accounts_page.dart │ ├── auth_page.dart │ ├── category_comics_page.dart │ ├── category_page.dart │ ├── comic_page.dart │ ├── download_page.dart │ ├── downloading_page.dart │ ├── ehentai │ │ ├── accounts.dart │ │ ├── eh_comments_page.dart │ │ ├── eh_gallery_page.dart │ │ ├── eh_login_page.dart │ │ ├── eh_user_cookie_parser.dart │ │ └── subscription.dart │ ├── explore_page.dart │ ├── favorites │ │ ├── local_favorites.dart │ │ ├── local_search_page.dart │ │ ├── main_favorites_page.dart │ │ ├── network_favorite_page.dart │ │ └── network_to_local.dart │ ├── history_page.dart │ ├── hitomi │ │ ├── hitomi_comic_page.dart │ │ ├── hitomi_home_page.dart │ │ └── hitomi_search.dart │ ├── htmanga │ │ └── ht_comic_page.dart │ ├── image_favorites.dart │ ├── jm │ │ ├── jm_comic_page.dart │ │ ├── jm_comments_page.dart │ │ └── week_recommendation_page.dart │ ├── logs_page.dart │ ├── main_page.dart │ ├── me_page.dart │ ├── nhentai │ │ ├── comic_page.dart │ │ └── comments.dart │ ├── picacg │ │ ├── collections_page.dart │ │ ├── comic_page.dart │ │ └── comments_page.dart │ ├── pre_search_page.dart │ ├── ranking_page.dart │ ├── reader │ │ ├── comic_reading_page.dart │ │ ├── eps_view.dart │ │ ├── image.dart │ │ ├── image_view.dart │ │ ├── reading_data.dart │ │ ├── reading_logic.dart │ │ ├── reading_settings.dart │ │ ├── reading_type.dart │ │ ├── tool_bar.dart │ │ └── touch_control.dart │ ├── search_result_page.dart │ ├── settings │ │ ├── app_settings.dart │ │ ├── blocking_keyword_page.dart │ │ ├── comic_source_settings.dart │ │ ├── components.dart │ │ ├── eh_settings.dart │ │ ├── explore_settings.dart │ │ ├── ht_settings.dart │ │ ├── jm_settings.dart │ │ ├── local_favorite_settings.dart │ │ ├── multi_pages_filter.dart │ │ ├── network_setting.dart │ │ ├── picacg_settings.dart │ │ ├── reading_settings.dart │ │ └── settings_page.dart │ ├── show_image_page.dart │ ├── tools.dart │ ├── webview.dart │ └── welcome_page.dart └── tools │ ├── app_links.dart │ ├── background_service.dart │ ├── block_screenshot.dart │ ├── cache_auto_clear.dart │ ├── debug.dart │ ├── extensions.dart │ ├── file_type.dart │ ├── io_extensions.dart │ ├── io_tools.dart │ ├── js.dart │ ├── keep_screen_on.dart │ ├── key_down_event.dart │ ├── mouse_listener.dart │ ├── notification.dart │ ├── pdf.dart │ ├── save_image.dart │ ├── tags_translation.dart │ ├── time.dart │ └── translations.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake ├── main.cc ├── my_application.cc └── my_application.h ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Podfile ├── Runner.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 ├── pubspec.lock ├── pubspec.yaml ├── screenshots ├── 1.png ├── 10.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png └── 9.png ├── test └── widget_test.dart ├── utils ├── check_translation.dart └── tags_translation.dart ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── index.html ├── loading.gif └── manifest.json └── windows ├── .gitignore ├── CMakeLists.txt ├── build.iss ├── build_windows.py ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── RCa13944 ├── 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 /.github/ISSUE_TEMPLATE/bug.yaml: -------------------------------------------------------------------------------- 1 | name: 报告Bug/Report a bug 2 | description: 报告APP出现的问题/Reporting problems with the APP 3 | title: "[Bug]: " 4 | labels: ["bug🐞"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 感谢报告问题, 请先补全标题后填写以下信息. 10 | 11 | Thank you for reporting a problem, please complete the title and fill in the following information. 12 | - type: textarea 13 | id: what-happened 14 | attributes: 15 | label: 描述/Description 16 | description: 描述问题/Describe the problem 17 | validations: 18 | required: true 19 | - type: input 20 | id: version 21 | attributes: 22 | label: Version 23 | description: | 24 | 使用的APP版本/App version 25 | 非最新版本请尝试更新/Please try to update if it is not the latest version 26 | validations: 27 | required: true 28 | - type: dropdown 29 | id: platform 30 | attributes: 31 | label: 使用的操作系统/Operating system 32 | multiple: true 33 | options: 34 | - Android 35 | - iOS 36 | - Windows 37 | - macOS 38 | - other 39 | validations: 40 | required: true 41 | - type: textarea 42 | id: logs 43 | attributes: 44 | label: 日志/logs 45 | description: 上传日志, 在设置-logs中, 点击右上角的菜单后, 点击导出日志; 或者将错误相关日志粘贴到这里 46 | - type: textarea 47 | id: screenshotOrVideo 48 | attributes: 49 | label: 截图或视频/Screenshot or video 50 | description: 在这里上传相关的屏幕截图或者视频/Upload relevant screenshots or videos here -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.yaml: -------------------------------------------------------------------------------- 1 | name: 功能建议/Feature Request 2 | description: 提出改进APP的建议/Suggest improvements to the APP 3 | title: "[Enhancement]: " 4 | labels: ["enhancement🚀"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 欢迎提出功能建议, 请先补全标题后填写以下信息. 10 | 11 | Welcome to make a feature request, please fill in the following information after completing the title. 12 | - type: textarea 13 | id: what-happened 14 | attributes: 15 | label: 描述/Description 16 | description: 描述具体的建议/Describe your suggestion. 17 | validations: 18 | required: true 19 | - type: dropdown 20 | id: platform 21 | attributes: 22 | label: 操作系统/Operating System 23 | description: 如果建议针对某个平台, 请在此选择/If the feature is for a particular platform, please select here 24 | multiple: true 25 | options: 26 | - Android 27 | - iOS 28 | - Windows 29 | - macOS 30 | - other 31 | validations: 32 | required: false 33 | - type: textarea 34 | id: screenshotOrVideo 35 | attributes: 36 | label: 图片/picture 37 | description: 如果需要图片描述, 请在这里上传/If you need a picture description, please upload it here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.yaml: -------------------------------------------------------------------------------- 1 | name: 其它/other 2 | description: 其它内容/Other contents 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | 如果你想报告App运行时出现的问题(无法查看某个漫画源, 无法登录, 某个功能无法使用等情况), 请切换到报告Bug模板; 8 | 9 | 如果你想提出功能建议或者优化建议, 请切换到功能建议模板; 10 | 11 | 对于其它情况, 填写并提交此处的内容. 12 | 13 | If you wish to report issues occurring during the app's runtime (such as the inability to view a particular comic source, login issues, non-functional features, etc.), please switch to the Bug Report template. 14 | 15 | If you would like to make feature requests or optimization suggestions, please switch to the Feature Request template. 16 | 17 | For any other situations, please fill out and submit the content here. 18 | - type: textarea 19 | id: what-happened 20 | attributes: 21 | label: Content 22 | validations: 23 | required: true 24 | -------------------------------------------------------------------------------- /.github/workflows/delete_old_workflows.yml: -------------------------------------------------------------------------------- 1 | name: Delete old workflow runs 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | days: 6 | description: 'Days-worth of runs to keep for each workflow' 7 | required: true 8 | default: '30' 9 | minimum_runs: 10 | description: 'Minimum runs to keep for each workflow' 11 | required: true 12 | default: '6' 13 | delete_workflow_pattern: 14 | description: 'Name or filename of the workflow (if not set, all workflows are targeted)' 15 | required: false 16 | delete_workflow_by_state_pattern: 17 | description: 'Filter workflows by state: active, deleted, disabled_fork, disabled_inactivity, disabled_manually' 18 | required: true 19 | default: "ALL" 20 | type: choice 21 | options: 22 | - "ALL" 23 | - active 24 | - deleted 25 | - disabled_inactivity 26 | - disabled_manually 27 | delete_run_by_conclusion_pattern: 28 | description: 'Remove runs based on conclusion: action_required, cancelled, failure, skipped, success' 29 | required: true 30 | default: "ALL" 31 | type: choice 32 | options: 33 | - "ALL" 34 | - "Unsuccessful: action_required,cancelled,failure,skipped" 35 | - action_required 36 | - cancelled 37 | - failure 38 | - skipped 39 | - success 40 | dry_run: 41 | description: 'Logs simulated changes, no deletions are performed' 42 | required: false 43 | 44 | jobs: 45 | del_runs: 46 | runs-on: ubuntu-latest 47 | permissions: 48 | actions: write 49 | contents: read 50 | steps: 51 | - name: Delete workflow runs 52 | uses: Mattraks/delete-workflow-runs@v2 53 | with: 54 | token: ${{ github.token }} 55 | repository: ${{ github.repository }} 56 | retain_days: ${{ github.event.inputs.days }} 57 | keep_minimum_runs: ${{ github.event.inputs.minimum_runs }} 58 | delete_workflow_pattern: ${{ github.event.inputs.delete_workflow_pattern }} 59 | delete_workflow_by_state_pattern: ${{ github.event.inputs.delete_workflow_by_state_pattern }} 60 | delete_run_by_conclusion_pattern: >- 61 | ${{ 62 | startsWith(github.event.inputs.delete_run_by_conclusion_pattern, 'Unsuccessful:') 63 | && 'action_required,cancelled,failure,skipped' 64 | || github.event.inputs.delete_run_by_conclusion_pattern 65 | }} 66 | dry_run: ${{ github.event.inputs.dry_run }} -------------------------------------------------------------------------------- /.github/workflows/ios_simulator.yml: -------------------------------------------------------------------------------- 1 | name: Build IOS SIMULATOR 2 | run-name: Build IOS SIMULATOR 3 | on: 4 | workflow_dispatch: {} 5 | jobs: 6 | Build_IOS_SIMULATOR: 7 | runs-on: macos-13 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: subosito/flutter-action@v2 11 | with: 12 | channel: 'stable' 13 | architecture: x64 14 | - run: sudo xcode-select --switch /Applications/Xcode_14.3.1.app 15 | - run: flutter pub get 16 | - run: flutter build ios --simulator --no-codesign 17 | - uses: actions/upload-artifact@v3 18 | with: 19 | name: build_files 20 | path: /Users/runner/work/PicaComic/PicaComic/build/ios/iphonesimulator 21 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Build Linux 2 | run-name: Build Linux 3 | on: 4 | workflow_dispatch: {} 5 | jobs: 6 | Build_Linux: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: subosito/flutter-action@v2 11 | with: 12 | channel: 'stable' 13 | architecture: x64 14 | - run: | 15 | sudo apt-get update -y 16 | sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1 17 | dart pub global activate flutter_to_debian 18 | - run: python3 debian/build.py 19 | - run: dart run flutter_to_arch 20 | - run: | 21 | sudo rm -rf build/linux/arch/app.tar.gz 22 | sudo rm -rf build/linux/arch/pkg 23 | sudo rm -rf build/linux/arch/src 24 | sudo rm -rf build/linux/arch/PKGBUILD 25 | - uses: actions/upload-artifact@v4 26 | with: 27 | name: deb_build 28 | path: build/linux/x64/release/debian 29 | - uses: actions/upload-artifact@v4 30 | with: 31 | name: arch_build 32 | path: build/linux/arch/ -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build IOS 2 | run-name: Build IOS 3 | on: 4 | workflow_dispatch: {} 5 | jobs: 6 | Build_IOS: 7 | runs-on: macos-13 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: subosito/flutter-action@v2 11 | with: 12 | channel: 'stable' 13 | architecture: x64 14 | - run: sudo xcode-select --switch /Applications/Xcode_14.3.1.app 15 | - run: flutter pub get 16 | - run: flutter build ios --release --no-codesign 17 | - run: | 18 | mkdir -p /Users/runner/work/PicaComic/PicaComic/build/ios/iphoneos/Payload 19 | mv /Users/runner/work/PicaComic/PicaComic/build/ios/iphoneos/Runner.app /Users/runner/work/PicaComic/PicaComic/build/ios/iphoneos/Payload 20 | cd /Users/runner/work/PicaComic/PicaComic/build/ios/iphoneos/ 21 | zip -r app-ios.ipa Payload 22 | - uses: actions/upload-artifact@v4 23 | with: 24 | name: app-ios.ipa 25 | path: /Users/runner/work/PicaComic/PicaComic/build/ios/iphoneos/app-ios.ipa 26 | Build_MacOS: 27 | runs-on: macos-13 28 | steps: 29 | - uses: actions/checkout@v3 30 | - uses: subosito/flutter-action@v2 31 | with: 32 | channel: 'stable' 33 | architecture: x64 34 | - run: sudo xcode-select --switch /Applications/Xcode_14.3.1.app 35 | - run: flutter pub get 36 | - run: flutter build macos --release 37 | - uses: actions/upload-artifact@v4 38 | with: 39 | name: macos-build.zip 40 | path: build/macos/Build/Products/Release/pica_comic.app 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | .vscode/ -------------------------------------------------------------------------------- /.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. 5 | 6 | version: 7 | revision: 9944297138845a94256f1cf37beb88ff9a8e811a 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: 9944297138845a94256f1cf37beb88ff9a8e811a 17 | base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 18 | - platform: android 19 | create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 20 | base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 21 | - platform: ios 22 | create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 23 | base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 24 | - platform: linux 25 | create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 26 | base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 27 | - platform: macos 28 | create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 29 | base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 30 | - platform: web 31 | create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 32 | base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 33 | - platform: windows 34 | create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 35 | base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nyne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pica Comic 2 | 3 | [![flutter](https://img.shields.io/badge/flutter-3.24.1-blue)](https://flutter.dev/) 4 | [![License](https://img.shields.io/github/license/wgh136/PicaComic)](https://github.com/wgh136/PicaComic/blob/master/LICENSE) 5 | [![Download](https://img.shields.io/github/v/release/wgh136/PicaComic)](https://github.com/wgh136/PicaComic/releases) 6 | [![stars](https://img.shields.io/github/stars/wgh136/PicaComic)](https://github.com/wgh136/PicaComic/stargazers) 7 | 8 | A comic app with multiple sources built with flutter. 9 | 10 | ## How to use 11 | 12 | 1. Clone the repository 13 | ```shell 14 | git clone https://github.com/wgh136/PicaComic 15 | ``` 16 | 2. Install flutter: https://docs.flutter.dev/get-started/install 17 | 3. Build Application: https://docs.flutter.dev/deployment 18 | 19 | ## Introduction 20 | 21 | ### Built-in Comic Source 22 | 23 | Currently, Pica Comic has 5 built-in comic sources: 24 | - picacg 25 | - e-hentai/exhentai 26 | - jmcomic 27 | - hitomi 28 | - 绅士漫画 29 | - nhentai 30 | 31 | ### Custom Comic Source 32 | 33 | You can add custom comic sources in the app after version 3.0.0. 34 | 35 | ### Features 36 | 37 | - Browse manga 38 | - Online reading 39 | - Download manga 40 | - Manage local favorites and network favorites 41 | - Data sync(using webdav) 42 | - Reading history 43 | 44 | ### History 45 | 46 | This project initially started as an unofficial app for picacg 47 | and later evolved into an app that supports multiple comic sources. 48 | 49 | ## Build From Source Code 50 | See [https://docs.flutter.dev/](https://docs.flutter.dev/) 51 | 52 | ## Thanks 53 | 54 | ### Projects 55 | [![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=tonquer&repo=JMComic-qt)](https://github.com/tonquer/JMComic-qt) 56 | 57 | The image restructuring algorithm used to display jm images is from this project. 58 | 59 | ### Tags Translation 60 | [![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=EhTagTranslation&repo=Database)](https://github.com/EhTagTranslation/Database) 61 | 62 | The Chinese translation of the manga tags is from this project. 63 | 64 | ## Screenshots 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /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 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 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 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 23 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 51 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/drawable-hdpi/notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/drawable-mdpi/notification.png -------------------------------------------------------------------------------- /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-xhdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/drawable-xhdpi/notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/drawable-xxhdpi/notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/drawable-xxxhdpi/notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 20 | 21 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 20 | 21 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile 2 | 3 | allprojects { 4 | tasks.withType(AbstractKotlinCompile).configureEach { 5 | kotlinOptions { 6 | jvmTarget = "1.8" 7 | apiVersion = "1.8" 8 | languageVersion = "1.8" 9 | } 10 | } 11 | repositories { 12 | google() 13 | mavenCentral() 14 | } 15 | } 16 | 17 | rootProject.buildDir = '../build' 18 | subprojects { 19 | project.buildDir = "${rootProject.buildDir}/${project.name}" 20 | } 21 | subprojects { 22 | project.evaluationDependsOn(':app') 23 | } 24 | 25 | tasks.register("clean", Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.defaults.buildfeatures.buildconfig=true 5 | android.nonTransitiveRClass=false 6 | android.nonFinalResIds=false 7 | -------------------------------------------------------------------------------- /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.0-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | } 19 | 20 | plugins { 21 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 22 | id "com.android.application" version "8.1.1" apply false 23 | id "org.jetbrains.kotlin.android" version "1.9.0" apply false 24 | } 25 | 26 | include ":app" -------------------------------------------------------------------------------- /debian/build.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | debianContent = '' 4 | desktopContent = '' 5 | version = '' 6 | 7 | with open('debian/debian.yaml', 'r') as f: 8 | debianContent = f.read() 9 | with open('debian/gui/pica-comic.desktop', 'r') as f: 10 | desktopContent = f.read() 11 | with open('pubspec.yaml', 'r') as f: 12 | version = str.split(str.split(f.read(), 'version: ')[1], '+')[0] 13 | 14 | with open('debian/debian.yaml', 'w') as f: 15 | f.write(debianContent.replace('{{Version}}', version)) 16 | with open('debian/gui/pica-comic.desktop', 'w') as f: 17 | f.write(desktopContent.replace('{{Version}}', version)) 18 | 19 | subprocess.run(["flutter", "build", "linux"]) 20 | 21 | subprocess.run(["$HOME/.pub-cache/bin/flutter_to_debian"], shell=True) 22 | 23 | with open('debian/debian.yaml', 'w') as f: 24 | f.write(debianContent) 25 | with open('debian/gui/pica-comic.desktop', 'w') as f: 26 | f.write(desktopContent) 27 | -------------------------------------------------------------------------------- /debian/debian.yaml: -------------------------------------------------------------------------------- 1 | flutter_app: 2 | command: pica_comic 3 | arch: x64 4 | parent: /usr/local/lib 5 | nonInteractive: true 6 | execFieldCodes: u 7 | 8 | control: 9 | Package: pica-comic 10 | Version: {{Version}} 11 | Architecture: amd64 12 | Priority: optional 13 | Depends: libwebkit2gtk-4.1-0, libgtk-3-0 14 | Maintainer: nyne 15 | Description: pica comic 16 | 17 | #options: 18 | # exec_out_dir: debian/packages -------------------------------------------------------------------------------- /debian/gui/pica-comic.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version={{Version}} 3 | Name=PicaComic 4 | GenericName=PicaComic 5 | Comment=pica comic 6 | Terminal=false 7 | Type=Application 8 | Categories=Utility 9 | Keywords=Flutter;comic;images; -------------------------------------------------------------------------------- /debian/gui/pica-comic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/debian/gui/pica-comic.png -------------------------------------------------------------------------------- /doc/hosts.md: -------------------------------------------------------------------------------- 1 | # 关于hosts的使用 2 | 3 | ## 工作原理及存在的问题 4 | 5 | 启用后, app会在本地启动一个代理服务器用于转发流量, 如果指定了域名对应的ip地址, 代理服务器将直接通过ip与目标服务器建立连接. 6 | 7 | 使用这种方式可以解决Dart中无法手动校验证书的问题, 在Dart中如果尝试直接通过ip建立https连接, 将会出现证书校验错误, 8 | 如果忽略证书校验, 将存在严重的安全性问题. 代理服务器似乎是此问题的唯一解法. 9 | 10 | 由于eh被通过sni嗅探的方式封锁, 因此必须绕过sni嗅探才能解决问题. 目前没有好的解决方式, app中将直接通过ip建立https连接并忽略证书校验. 11 | 12 | ## 配置文件书写方式 13 | 14 | 使用json, 如果不了解, 可以去搜索json的格式 15 | 16 | 退出修改页面时将自动保存文件, 如果修改了端口, 需要手动重启代理服务器 17 | 18 | ### port 19 | 20 | 指定代理服务器的端口, 如果端口与其它程序冲突, 在此处修改 21 | 22 | ### rule 23 | 24 | 指定域名的ip地址 25 | 26 | ### sni 27 | 28 | 尝试访问列在此处的域名时, app将使用绕过sni嗅探方式工作, 这将降低安全性 29 | 30 | ## 示例 31 | 32 | 这是app的默认配置 33 | 34 | ```json 35 | { 36 | "port": 7891, 37 | "rule": { 38 | "picaapi.picacomic.com": "104.21.91.145", 39 | "img.picacomic.com": "104.21.91.145", 40 | "storage1.picacomic.com": "104.21.91.145", 41 | "storage-b.picacomic.com": "104.21.91.145", 42 | "e-hentai.org": "172.67.0.127", 43 | "exhentai.org": "178.175.129.254" 44 | }, 45 | "sni": [ 46 | "e-hentai.org", 47 | "exhentai.org" 48 | ] 49 | } 50 | ``` -------------------------------------------------------------------------------- /fonts/NotoSansSC-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/fonts/NotoSansSC-Regular.ttf -------------------------------------------------------------------------------- /images/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/images/app_icon.png -------------------------------------------------------------------------------- /images/app_icon_no_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/images/app_icon_no_bg.png -------------------------------------------------------------------------------- /images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/images/avatar.png -------------------------------------------------------------------------------- /images/avatar_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/images/avatar_small.png -------------------------------------------------------------------------------- /images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/images/github.png -------------------------------------------------------------------------------- /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 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '12.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 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | #target 'RunnerTests' do 36 | # inherit! :search_paths 37 | #end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /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 | 8 | -------------------------------------------------------------------------------- /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 UIKit 2 | import Flutter 3 | import flutter_local_notifications 4 | 5 | @UIApplicationMain 6 | @objc class AppDelegate: FlutterAppDelegate { 7 | override func application( 8 | _ application: UIApplication, 9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 10 | ) -> Bool { 11 | let controller: FlutterViewController = window?.rootViewController as! FlutterViewController 12 | 13 | FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in 14 | GeneratedPluginRegistrant.register(with: registry) 15 | } 16 | 17 | if #available(iOS 10.0, *) { 18 | UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate 19 | } 20 | 21 | // 用于获取系统代理配置的 MethodChannel 22 | let methodChannel = FlutterMethodChannel(name: "kokoiro.xyz.pica_comic/proxy", binaryMessenger: controller.binaryMessenger) 23 | methodChannel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in 24 | if let proxySettings = CFNetworkCopySystemProxySettings()?.takeUnretainedValue() as NSDictionary?, 25 | let dict = proxySettings.object(forKey: kCFNetworkProxiesHTTPProxy) as? NSDictionary, 26 | let host = dict.object(forKey: kCFNetworkProxiesHTTPProxy) as? String, 27 | let port = dict.object(forKey: kCFNetworkProxiesHTTPPort) as? Int { 28 | let proxyConfig = "\(host):\(port)" 29 | result(proxyConfig) 30 | } else { 31 | result("") 32 | } 33 | } 34 | 35 | // 用于设置屏幕常亮的 MethodChannel 36 | let channel2 = FlutterMethodChannel(name: "com.kokoiro.xyz.pica_comic/keepScreenOn", binaryMessenger: controller.binaryMessenger) 37 | channel2.setMethodCallHandler { (call: FlutterMethodCall, result: FlutterResult) in 38 | if call.method == "set" { 39 | let screenOn = true // 设置屏幕常亮 40 | UIApplication.shared.isIdleTimerDisabled = screenOn 41 | } else { 42 | let screenOn = false // 设置屏幕不常亮 43 | UIApplication.shared.isIdleTimerDisabled = screenOn 44 | } 45 | result(nil) 46 | } 47 | 48 | // 用于监听音量键的 MethodChannel 49 | let volumeChannel = FlutterEventChannel(name: "com.kokoiro.xyz.pica_comic/volume", binaryMessenger: controller.binaryMessenger) 50 | volumeChannel.setStreamHandler(VolumeStreamHandler()) 51 | 52 | GeneratedPluginRegistrant.register(with: self) 53 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 54 | } 55 | } 56 | 57 | class VolumeStreamHandler: NSObject, FlutterStreamHandler { 58 | private var eventSink: FlutterEventSink? 59 | 60 | func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { 61 | eventSink = events 62 | return nil 63 | } 64 | 65 | func onCancel(withArguments arguments: Any?) -> FlutterError? { 66 | eventSink = nil 67 | return nil 68 | } 69 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "AppIcon@2x.png", 5 | "idiom": "iphone", 6 | "scale": "2x", 7 | "size": "60x60" 8 | }, 9 | { 10 | "filename": "AppIcon@3x.png", 11 | "idiom": "iphone", 12 | "scale": "3x", 13 | "size": "60x60" 14 | }, 15 | { 16 | "filename": "AppIcon~ipad.png", 17 | "idiom": "ipad", 18 | "scale": "1x", 19 | "size": "76x76" 20 | }, 21 | { 22 | "filename": "AppIcon@2x~ipad.png", 23 | "idiom": "ipad", 24 | "scale": "2x", 25 | "size": "76x76" 26 | }, 27 | { 28 | "filename": "AppIcon-83.5@2x~ipad.png", 29 | "idiom": "ipad", 30 | "scale": "2x", 31 | "size": "83.5x83.5" 32 | }, 33 | { 34 | "filename": "AppIcon-40@2x.png", 35 | "idiom": "iphone", 36 | "scale": "2x", 37 | "size": "40x40" 38 | }, 39 | { 40 | "filename": "AppIcon-40@3x.png", 41 | "idiom": "iphone", 42 | "scale": "3x", 43 | "size": "40x40" 44 | }, 45 | { 46 | "filename": "AppIcon-40~ipad.png", 47 | "idiom": "ipad", 48 | "scale": "1x", 49 | "size": "40x40" 50 | }, 51 | { 52 | "filename": "AppIcon-40@2x~ipad.png", 53 | "idiom": "ipad", 54 | "scale": "2x", 55 | "size": "40x40" 56 | }, 57 | { 58 | "filename": "AppIcon-20@2x.png", 59 | "idiom": "iphone", 60 | "scale": "2x", 61 | "size": "20x20" 62 | }, 63 | { 64 | "filename": "AppIcon-20@3x.png", 65 | "idiom": "iphone", 66 | "scale": "3x", 67 | "size": "20x20" 68 | }, 69 | { 70 | "filename": "AppIcon-20~ipad.png", 71 | "idiom": "ipad", 72 | "scale": "1x", 73 | "size": "20x20" 74 | }, 75 | { 76 | "filename": "AppIcon-20@2x~ipad.png", 77 | "idiom": "ipad", 78 | "scale": "2x", 79 | "size": "20x20" 80 | }, 81 | { 82 | "filename": "AppIcon-29.png", 83 | "idiom": "iphone", 84 | "scale": "1x", 85 | "size": "29x29" 86 | }, 87 | { 88 | "filename": "AppIcon-29@2x.png", 89 | "idiom": "iphone", 90 | "scale": "2x", 91 | "size": "29x29" 92 | }, 93 | { 94 | "filename": "AppIcon-29@3x.png", 95 | "idiom": "iphone", 96 | "scale": "3x", 97 | "size": "29x29" 98 | }, 99 | { 100 | "filename": "AppIcon-29~ipad.png", 101 | "idiom": "ipad", 102 | "scale": "1x", 103 | "size": "29x29" 104 | }, 105 | { 106 | "filename": "AppIcon-29@2x~ipad.png", 107 | "idiom": "ipad", 108 | "scale": "2x", 109 | "size": "29x29" 110 | }, 111 | { 112 | "filename": "AppIcon-60@2x~car.png", 113 | "idiom": "car", 114 | "scale": "2x", 115 | "size": "60x60" 116 | }, 117 | { 118 | "filename": "AppIcon-60@3x~car.png", 119 | "idiom": "car", 120 | "scale": "3x", 121 | "size": "60x60" 122 | }, 123 | { 124 | "filename": "AppIcon~ios-marketing.png", 125 | "idiom": "ios-marketing", 126 | "scale": "1x", 127 | "size": "1024x1024" 128 | } 129 | ], 130 | "info": { 131 | "author": "iconkitchen", 132 | "version": 1 133 | } 134 | } -------------------------------------------------------------------------------- /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/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/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 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Pica Comic 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | pica_comic 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | NSPhotoLibraryAddUsageDescription 51 | Save the images selected by user 52 | NSPhotoLibraryUsageDescription 53 | Allow user to Choose image as his avatar 54 | NSFaceIDUsageDescription 55 | Protect user's privacy 56 | 57 | 58 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/comic_source/app_build_in_favorites.dart: -------------------------------------------------------------------------------- 1 | import 'package:pica_comic/base.dart'; 2 | import 'package:pica_comic/network/htmanga_network/htmanga_main_network.dart'; 3 | import 'package:pica_comic/network/jm_network/jm_network.dart'; 4 | import 'package:pica_comic/network/nhentai_network/nhentai_main_network.dart'; 5 | import 'package:pica_comic/network/picacg_network/methods.dart'; 6 | import 'package:pica_comic/network/res.dart'; 7 | import 'comic_source.dart'; 8 | 9 | final picacgFavorites = FavoriteData( 10 | key: "picacg", 11 | title: "Picacg", 12 | multiFolder: false, 13 | loadComic: (i, [folder]) => PicacgNetwork().getFavorites(i, appdata.settings[30]=="1"), 14 | loadFolders: null, 15 | addOrDelFavorite: (id, folder, isAdding) async{ 16 | var res = await PicacgNetwork().favouriteOrUnfavouriteComic(id); 17 | return res ? const Res(true) : const Res(false, errorMessage: "Network Error"); 18 | } 19 | ); 20 | 21 | /// eh较为特殊, 写统一接口有点麻烦, 不要使用这个进行构建页面 22 | final ehFavorites = FavoriteData( 23 | key: "ehentai", 24 | title: "ehentai", 25 | multiFolder: true, 26 | loadComic: (i, [folder]) => throw UnimplementedError(), 27 | loadFolders: null 28 | ); 29 | 30 | final jmFavorites = FavoriteData( 31 | key: "jm", 32 | title: "禁漫天堂", 33 | multiFolder: true, 34 | loadComic: (i, [folder]) => JmNetwork().getFolderComicsPage(folder!, i), 35 | loadFolders: ([String? comicId]) => JmNetwork().getFolders(), 36 | deleteFolder: (id) => JmNetwork().deleteFolder(id), 37 | addFolder: (name) => JmNetwork().createFolder(name), 38 | allFavoritesId: "0", 39 | addOrDelFavorite: (id, folder, isAdding) async{ 40 | if(isAdding) return const Res.error("invalid"); 41 | var res = await JmNetwork().favorite(id, folder); 42 | return res; 43 | } 44 | ); 45 | 46 | final htFavorites = FavoriteData( 47 | key: "htmanga", 48 | title: "绅士漫画", 49 | multiFolder: true, 50 | loadComic: (i, [folder]) => HtmangaNetwork().getFavoriteFolderComics(folder!, i), 51 | loadFolders: ([String? comicId]) => HtmangaNetwork().getFolders(), 52 | allFavoritesId: "0", 53 | deleteFolder: (id) async{ 54 | var res = await HtmangaNetwork().deleteFolder(id); 55 | return res ? const Res(true) : const Res(false, errorMessage: "Network Error"); 56 | }, 57 | addFolder: (name) async{ 58 | var res = await HtmangaNetwork().createFolder(name); 59 | return res ? const Res(true) : const Res(false, errorMessage: "Network Error"); 60 | }, 61 | addOrDelFavorite: (id, folder, isAdding) async{ 62 | if(isAdding) return const Res.error("invalid"); 63 | var res = await HtmangaNetwork().delFavorite(id); 64 | return res; 65 | } 66 | ); 67 | 68 | final nhentaiFavorites = FavoriteData( 69 | key: "nhentai", 70 | title: "nhentai", 71 | multiFolder: false, 72 | loadComic: (i, [folder]) => NhentaiNetwork().getFavorites(i), 73 | loadFolders: null, 74 | ); -------------------------------------------------------------------------------- /lib/comic_source/favorites.dart: -------------------------------------------------------------------------------- 1 | part of comic_source; 2 | 3 | typedef AddOrDelFavFunc = Future> Function(String comicId, String folderId, bool isAdding); 4 | 5 | class FavoriteData{ 6 | final String key; 7 | 8 | final String title; 9 | 10 | final bool multiFolder; 11 | 12 | final Future>> Function(int page, [String? folder]) loadComic; 13 | 14 | /// key-id, value-name 15 | /// 16 | /// if comicId is not null, Res.subData is the folders that the comic is in 17 | final Future>> Function([String? comicId])? loadFolders; 18 | 19 | /// A value of null disables this feature 20 | final Future> Function(String key)? deleteFolder; 21 | 22 | /// A value of null disables this feature 23 | final Future> Function(String name)? addFolder; 24 | 25 | /// A value of null disables this feature 26 | final String? allFavoritesId; 27 | 28 | final AddOrDelFavFunc? addOrDelFavorite; 29 | 30 | const FavoriteData({ 31 | required this.key, 32 | required this.title, 33 | required this.multiFolder, 34 | required this.loadComic, 35 | this.loadFolders, 36 | this.deleteFolder, 37 | this.addFolder, 38 | this.allFavoritesId, 39 | this.addOrDelFavorite}); 40 | } 41 | 42 | FavoriteData getFavoriteData(String key){ 43 | var source = ComicSource.find(key) ?? (throw "Unknown source key: $key"); 44 | return source.favoriteData!; 45 | } 46 | 47 | FavoriteData? getFavoriteDataOrNull(String key){ 48 | var source = ComicSource.find(key); 49 | return source?.favoriteData; 50 | } -------------------------------------------------------------------------------- /lib/components/components.dart: -------------------------------------------------------------------------------- 1 | library components; 2 | 3 | import 'dart:async'; 4 | import 'dart:collection'; 5 | import 'dart:math' as math; 6 | import 'dart:ui'; 7 | 8 | import 'package:flutter/gestures.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter/rendering.dart'; 11 | import 'package:flutter/scheduler.dart'; 12 | import 'package:flutter/services.dart'; 13 | import 'package:pica_comic/comic_source/comic_source.dart'; 14 | import 'package:pica_comic/foundation/app.dart'; 15 | import 'package:pica_comic/foundation/app_page_route.dart'; 16 | import 'package:pica_comic/foundation/history.dart'; 17 | import 'package:pica_comic/foundation/image_loader/cached_image.dart'; 18 | import 'package:pica_comic/foundation/image_loader/stream_image_provider.dart'; 19 | import 'package:pica_comic/foundation/image_manager.dart'; 20 | import 'package:pica_comic/foundation/local_favorites.dart'; 21 | import 'package:pica_comic/network/base_comic.dart'; 22 | import 'package:pica_comic/network/cloudflare.dart'; 23 | import 'package:pica_comic/network/res.dart'; 24 | import 'package:pica_comic/pages/comic_page.dart'; 25 | import 'package:pica_comic/pages/pre_search_page.dart'; 26 | import 'package:pica_comic/pages/reader/comic_reading_page.dart'; 27 | import 'package:pica_comic/pages/show_image_page.dart'; 28 | import 'package:pica_comic/tools/extensions.dart'; 29 | import 'package:pica_comic/tools/tags_translation.dart'; 30 | import 'package:pica_comic/tools/translations.dart'; 31 | 32 | import '../base.dart'; 33 | import '../foundation/ui_mode.dart'; 34 | 35 | part 'animated_image.dart'; 36 | part 'appbar.dart'; 37 | part 'avatar.dart'; 38 | part 'button.dart'; 39 | part 'comic_tile.dart'; 40 | part 'comics_list.dart'; 41 | part 'consts.dart'; 42 | part 'flyout.dart'; 43 | part 'layout.dart'; 44 | part 'loading.dart'; 45 | part 'menu.dart'; 46 | part 'message.dart'; 47 | part 'navigation_bar.dart'; 48 | part 'pop_up_widget.dart'; 49 | part 'scroll.dart'; 50 | part 'select.dart'; 51 | part 'side_bar.dart'; -------------------------------------------------------------------------------- /lib/components/consts.dart: -------------------------------------------------------------------------------- 1 | part of 'components.dart'; 2 | 3 | const _fastAnimationDuration = Duration(milliseconds: 160); -------------------------------------------------------------------------------- /lib/components/scroll.dart: -------------------------------------------------------------------------------- 1 | part of 'components.dart'; 2 | 3 | class SmoothCustomScrollView extends StatelessWidget { 4 | const SmoothCustomScrollView({super.key, required this.slivers, this.controller}); 5 | 6 | final ScrollController? controller; 7 | 8 | final List slivers; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return SmoothScrollProvider( 13 | controller: controller, 14 | builder: (context, controller, physics) { 15 | return CustomScrollView( 16 | controller: controller, 17 | physics: physics, 18 | slivers: slivers, 19 | ); 20 | }, 21 | ); 22 | } 23 | } 24 | 25 | 26 | class SmoothScrollProvider extends StatefulWidget { 27 | const SmoothScrollProvider({super.key, this.controller, required this.builder}); 28 | 29 | final ScrollController? controller; 30 | 31 | final Widget Function(BuildContext, ScrollController, ScrollPhysics) builder; 32 | 33 | static bool get isMouseScroll => _SmoothScrollProviderState._isMouseScroll; 34 | 35 | @override 36 | State createState() => _SmoothScrollProviderState(); 37 | } 38 | 39 | class _SmoothScrollProviderState extends State { 40 | late final ScrollController _controller; 41 | 42 | double? _futurePosition; 43 | 44 | static bool _isMouseScroll = App.isDesktop; 45 | 46 | @override 47 | void initState() { 48 | _controller = widget.controller ?? ScrollController(); 49 | super.initState(); 50 | } 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | if(App.isMacOS) { 55 | return widget.builder( 56 | context, 57 | _controller, 58 | const ClampingScrollPhysics(), 59 | ); 60 | } 61 | return Listener( 62 | behavior: HitTestBehavior.translucent, 63 | onPointerDown: (event) { 64 | if (_isMouseScroll) { 65 | setState(() { 66 | _isMouseScroll = false; 67 | }); 68 | } 69 | }, 70 | onPointerSignal: (pointerSignal) { 71 | if (pointerSignal is PointerScrollEvent) { 72 | if (pointerSignal.kind == PointerDeviceKind.mouse && 73 | !_isMouseScroll) { 74 | setState(() { 75 | _isMouseScroll = true; 76 | }); 77 | } 78 | if (!_isMouseScroll) return; 79 | var currentLocation = _controller.position.pixels; 80 | _futurePosition ??= currentLocation; 81 | double k = (_futurePosition! - currentLocation).abs() / 1600 + 1; 82 | _futurePosition = 83 | _futurePosition! + pointerSignal.scrollDelta.dy * k; 84 | _futurePosition = _futurePosition!.clamp( 85 | _controller.position.minScrollExtent, 86 | _controller.position.maxScrollExtent); 87 | _controller.animateTo(_futurePosition!, 88 | duration: _fastAnimationDuration, curve: Curves.linear); 89 | } 90 | }, 91 | child: widget.builder( 92 | context, 93 | _controller, 94 | _isMouseScroll 95 | ? const NeverScrollableScrollPhysics() 96 | : const ClampingScrollPhysics(), 97 | ), 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/components/scrollable_list/scrollable_positioned_list.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | export 'src/item_positions_listener.dart'; 6 | export 'src/scrollable_positioned_list.dart'; 7 | -------------------------------------------------------------------------------- /lib/components/scrollable_list/src/item_positions_listener.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/foundation.dart'; 6 | 7 | import 'item_positions_notifier.dart'; 8 | import 'scrollable_positioned_list.dart'; 9 | 10 | /// Provides a listenable iterable of [itemPositions] of items that are on 11 | /// screen and their locations. 12 | abstract class ItemPositionsListener { 13 | /// Creates an [ItemPositionsListener] that can be used by a 14 | /// [ScrollablePositionedList] to return the current position of items. 15 | factory ItemPositionsListener.create() => ItemPositionsNotifier(); 16 | 17 | /// The position of items that are at least partially visible in the viewport. 18 | ValueListenable> get itemPositions; 19 | } 20 | 21 | /// Position information for an item in the list. 22 | class ItemPosition { 23 | /// Create an [ItemPosition]. 24 | const ItemPosition( 25 | {required this.index, 26 | required this.itemLeadingEdge, 27 | required this.itemTrailingEdge}); 28 | 29 | /// Index of the item. 30 | final int index; 31 | 32 | /// Distance in proportion of the viewport's main axis length from the leading 33 | /// edge of the viewport to the leading edge of the item. 34 | /// 35 | /// May be negative if the item is partially visible. 36 | final double itemLeadingEdge; 37 | 38 | /// Distance in proportion of the viewport's main axis length from the leading 39 | /// edge of the viewport to the trailing edge of the item. 40 | /// 41 | /// May be greater than one if the item is partially visible. 42 | final double itemTrailingEdge; 43 | 44 | @override 45 | bool operator ==(Object other) { 46 | if (other.runtimeType != runtimeType) return false; 47 | final otherPosition = other as ItemPosition; 48 | return otherPosition.index == index && 49 | otherPosition.itemLeadingEdge == itemLeadingEdge && 50 | otherPosition.itemTrailingEdge == itemTrailingEdge; 51 | } 52 | 53 | @override 54 | int get hashCode => 55 | 31 * (31 * (7 + index.hashCode) + itemLeadingEdge.hashCode) + 56 | itemTrailingEdge.hashCode; 57 | 58 | @override 59 | String toString() => 60 | 'ItemPosition(index: $index, itemLeadingEdge: $itemLeadingEdge, itemTrailingEdge: $itemTrailingEdge)'; 61 | } 62 | -------------------------------------------------------------------------------- /lib/components/scrollable_list/src/item_positions_notifier.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/foundation.dart'; 6 | 7 | import 'item_positions_listener.dart'; 8 | 9 | /// Internal implementation of [ItemPositionsListener]. 10 | class ItemPositionsNotifier implements ItemPositionsListener { 11 | @override 12 | final ValueNotifier> itemPositions = ValueNotifier([]); 13 | } 14 | -------------------------------------------------------------------------------- /lib/components/scrollable_list/src/post_mount_callback.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | /// Widget whose [Element] calls a callback when the element is mounted. 8 | class PostMountCallback extends StatelessWidget { 9 | /// Creates a [PostMountCallback] widget. 10 | const PostMountCallback({required this.child, this.callback, Key? key}) 11 | : super(key: key); 12 | 13 | /// The widget below this widget in the tree. 14 | final Widget child; 15 | 16 | /// Callback to call when the element for this widget is mounted. 17 | final void Function()? callback; 18 | 19 | @override 20 | StatelessElement createElement() => _PostMountCallbackElement(this); 21 | 22 | @override 23 | Widget build(BuildContext context) => child; 24 | } 25 | 26 | class _PostMountCallbackElement extends StatelessElement { 27 | _PostMountCallbackElement(PostMountCallback widget) : super(widget); 28 | 29 | @override 30 | void mount(Element? parent, dynamic newSlot) { 31 | super.mount(parent, newSlot); 32 | final PostMountCallback postMountCallback = widget as PostMountCallback; 33 | postMountCallback.callback?.call(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/components/scrollable_list/src/scroll_view.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/gestures.dart'; 6 | import 'package:flutter/rendering.dart'; 7 | import 'package:flutter/widgets.dart'; 8 | 9 | import 'wrapping.dart'; 10 | import 'viewport.dart'; 11 | 12 | /// A version of [CustomScrollView] that allows does not constrict the extents 13 | /// to be within 0 and 1. See [CustomScrollView] for more information. 14 | class UnboundedCustomScrollView extends CustomScrollView { 15 | final bool _shrinkWrap; 16 | 17 | const UnboundedCustomScrollView({ 18 | Key? key, 19 | Axis scrollDirection = Axis.vertical, 20 | bool reverse = false, 21 | ScrollController? controller, 22 | bool? primary, 23 | ScrollPhysics? physics, 24 | bool shrinkWrap = false, 25 | Key? center, 26 | double anchor = 0.0, 27 | double? cacheExtent, 28 | List slivers = const [], 29 | int? semanticChildCount, 30 | DragStartBehavior dragStartBehavior = DragStartBehavior.down, 31 | ScrollBehavior? scrollBehavior 32 | }) : _shrinkWrap = shrinkWrap, 33 | _anchor = anchor, 34 | super( 35 | key: key, 36 | scrollDirection: scrollDirection, 37 | reverse: reverse, 38 | controller: controller, 39 | primary: primary, 40 | physics: physics, 41 | shrinkWrap: false, 42 | center: center, 43 | cacheExtent: cacheExtent, 44 | semanticChildCount: semanticChildCount, 45 | dragStartBehavior: dragStartBehavior, 46 | slivers: slivers, 47 | scrollBehavior: scrollBehavior, 48 | ); 49 | 50 | // [CustomScrollView] enforces constraints on [CustomScrollView.anchor], so 51 | // we need our own version. 52 | final double _anchor; 53 | 54 | @override 55 | double get anchor => _anchor; 56 | 57 | /// Build the viewport. 58 | @override 59 | @protected 60 | Widget buildViewport( 61 | BuildContext context, 62 | ViewportOffset offset, 63 | AxisDirection axisDirection, 64 | List slivers, 65 | ) { 66 | if (_shrinkWrap) { 67 | return CustomShrinkWrappingViewport( 68 | axisDirection: axisDirection, 69 | offset: offset, 70 | slivers: slivers, 71 | cacheExtent: cacheExtent, 72 | center: center, 73 | anchor: anchor, 74 | ); 75 | } 76 | return UnboundedViewport( 77 | axisDirection: axisDirection, 78 | offset: offset, 79 | slivers: slivers, 80 | cacheExtent: cacheExtent, 81 | center: center, 82 | anchor: anchor, 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/foundation/def.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | typedef ActionFunc = void Function(); 4 | 5 | enum ComicType { 6 | picacg, 7 | ehentai, 8 | jm, 9 | hitomi, 10 | htManga, 11 | htFavorite, 12 | nhentai, 13 | other; 14 | 15 | @override 16 | toString() => name; 17 | } 18 | 19 | const String webUA = 20 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"; 21 | 22 | //App版本 23 | const appVersion = "4.0.4"; 24 | 25 | //定义宽屏设备的临界值 26 | const changePoint = 600; 27 | const changePoint2 = 1300; 28 | 29 | List get colors => [ 30 | Colors.redAccent, 31 | Colors.pinkAccent, 32 | Colors.purpleAccent, 33 | Colors.indigoAccent, 34 | Colors.blueAccent, 35 | Colors.cyanAccent, 36 | Colors.tealAccent, 37 | Colors.greenAccent, 38 | Colors.limeAccent, 39 | Colors.yellowAccent, 40 | Colors.amberAccent, 41 | Colors.orangeAccent, 42 | ]; 43 | 44 | const builtInSources = [ 45 | "picacg", 46 | "ehentai", 47 | "jm", 48 | "hitomi", 49 | "htmanga", 50 | "nhentai" 51 | ]; -------------------------------------------------------------------------------- /lib/foundation/image_favorites.dart: -------------------------------------------------------------------------------- 1 | part of "history.dart"; 2 | 3 | // 直接用history.db了, 没必要再加一个favorites.db 4 | 5 | class ImageFavorite{ 6 | /// unique id for the comic 7 | final String id; 8 | 9 | final String imagePath; 10 | 11 | final String title; 12 | 13 | final int ep; 14 | 15 | final int page; 16 | 17 | final Map otherInfo; 18 | 19 | const ImageFavorite(this.id, this.imagePath, this.title, this.ep, this.page, this.otherInfo); 20 | } 21 | 22 | class ImageFavoriteManager{ 23 | static Database get _db => HistoryManager()._db; 24 | 25 | /// 检查表image_favorites是否存在, 不存在则创建 26 | static void init(){ 27 | _db.execute("CREATE TABLE IF NOT EXISTS image_favorites (" 28 | "id TEXT," 29 | "title TEXT NOT NULL," 30 | "cover TEXT NOT NULL," 31 | "ep INTEGER NOT NULL," 32 | "page INTEGER NOT NULL," 33 | "other TEXT NOT NULL," 34 | "PRIMARY KEY (id, ep, page)" 35 | ");"); 36 | } 37 | 38 | static void add(ImageFavorite favorite){ 39 | _db.execute(""" 40 | insert into image_favorites(id, title, cover, ep, page, other) 41 | values(?, ?, ?, ?, ?, ?); 42 | """, [favorite.id, favorite.title, favorite.imagePath, favorite.ep, favorite.page, jsonEncode(favorite.otherInfo)]); 43 | Webdav.uploadData(); 44 | Future.microtask(() => StateController.findOrNull(tag: "me_page")?.update()); 45 | } 46 | 47 | static List getAll(){ 48 | var res = _db.select("select * from image_favorites;"); 49 | return res.map((e) => 50 | ImageFavorite(e["id"], e["cover"], e["title"], e["ep"], e["page"], jsonDecode(e["other"]))).toList(); 51 | } 52 | 53 | static void delete(ImageFavorite favorite){ 54 | _db.execute(""" 55 | delete from image_favorites 56 | where id = ? and ep = ? and page = ?; 57 | """, [favorite.id, favorite.ep, favorite.page]); 58 | Webdav.uploadData(); 59 | } 60 | 61 | static int get length { 62 | var res = _db.select("select count(*) from image_favorites;"); 63 | return res.first.values.first! as int; 64 | } 65 | } -------------------------------------------------------------------------------- /lib/foundation/image_loader/cached_image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async' show Future, StreamController; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import '../image_manager.dart'; 5 | import 'base_image_provider.dart'; 6 | import 'cached_image.dart' as image_provider; 7 | 8 | /// Function which is called after loading the image failed. 9 | typedef ErrorListener = void Function(); 10 | 11 | class CachedImageProvider 12 | extends BaseImageProvider { 13 | 14 | /// Image provider for normal image. 15 | const CachedImageProvider(this.url, {this.headers, this.sourceKey}); 16 | 17 | final String url; 18 | 19 | final Map? headers; 20 | 21 | final String? sourceKey; 22 | 23 | @override 24 | Future load(StreamController chunkEvents) async{ 25 | chunkEvents.add(const ImageChunkEvent( 26 | cumulativeBytesLoaded: 0, 27 | expectedTotalBytes: 100) 28 | ); 29 | var manager = ImageManager(); 30 | DownloadProgress? finishProgress; 31 | 32 | var stream = sourceKey == null 33 | ? manager.getImage(url, headers) 34 | : manager.getCustomThumbnail(url, sourceKey!); 35 | await for (var progress in stream) { 36 | if (progress.currentBytes == progress.expectedBytes) { 37 | finishProgress = progress; 38 | } 39 | chunkEvents.add(ImageChunkEvent( 40 | cumulativeBytesLoaded: progress.currentBytes, 41 | expectedTotalBytes: progress.expectedBytes) 42 | ); 43 | } 44 | 45 | if(finishProgress!.data != null){ 46 | return finishProgress.data!; 47 | } 48 | 49 | var file = finishProgress.getFile(); 50 | return await file.readAsBytes(); 51 | } 52 | 53 | @override 54 | Future obtainKey(ImageConfiguration configuration) { 55 | return SynchronousFuture(this); 56 | } 57 | 58 | @override 59 | String get key => url; 60 | } 61 | -------------------------------------------------------------------------------- /lib/foundation/image_loader/file_image_loader.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async' show Future; 2 | import 'dart:ui'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:pica_comic/network/download.dart'; 6 | 7 | class FileImageProvider extends ImageProvider { 8 | 9 | /// Image provider for downloaded comic 10 | const FileImageProvider(this.id, this.ep, this.index); 11 | 12 | final String id; 13 | 14 | final int ep; 15 | 16 | final int index; 17 | 18 | @override 19 | Future obtainKey(ImageConfiguration configuration) { 20 | return SynchronousFuture(this); 21 | } 22 | 23 | @override 24 | ImageStreamCompleter loadImage(FileImageProvider key, ImageDecoderCallback decode) { 25 | return MultiFrameImageStreamCompleter( 26 | codec: _loadAsync(key, decode: decode), 27 | scale: 1.0, 28 | debugLabel: key.toString(), 29 | ); 30 | } 31 | 32 | Future _loadAsync( 33 | FileImageProvider key, { 34 | required ImageDecoderCallback decode, 35 | }) async { 36 | var file = await DownloadManager().getImageAsync(id, ep, index); 37 | final int lengthInBytes = await file.length(); 38 | if (lengthInBytes == 0) { 39 | // The file may become available later. 40 | PaintingBinding.instance.imageCache.evict(key); 41 | throw StateError('$file is empty and cannot be loaded as an image.'); 42 | } 43 | return decode(await ImmutableBuffer.fromFilePath(file.path)); 44 | } 45 | 46 | @override 47 | bool operator ==(Object other) { 48 | if (other.runtimeType != runtimeType) { 49 | return false; 50 | } 51 | return other is FileImageProvider 52 | && other.id == id 53 | && other.ep == ep 54 | && other.index == index; 55 | } 56 | 57 | @override 58 | int get hashCode => Object.hash("FileImageProvider", id, ep, index); 59 | 60 | @override 61 | String toString() => 'FileImageProvider $id $ep $index'; 62 | } 63 | -------------------------------------------------------------------------------- /lib/foundation/image_loader/stream_image_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async' show Future, StreamController; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import '../image_manager.dart'; 5 | import 'base_image_provider.dart'; 6 | 7 | /// Function which is called after loading the image failed. 8 | typedef ErrorListener = void Function(); 9 | 10 | class StreamImageProvider 11 | extends BaseImageProvider { 12 | 13 | /// Image provider with [Stream]. 14 | const StreamImageProvider(this.streamBuilder, this.key); 15 | 16 | final Stream Function() streamBuilder; 17 | 18 | @override 19 | final String key; 20 | 21 | @override 22 | Future load(StreamController chunkEvents) async{ 23 | chunkEvents.add(const ImageChunkEvent( 24 | cumulativeBytesLoaded: 0, 25 | expectedTotalBytes: 100) 26 | ); 27 | DownloadProgress? finishProgress; 28 | 29 | await for (var progress in streamBuilder()) { 30 | if (progress.currentBytes == progress.expectedBytes) { 31 | finishProgress = progress; 32 | } 33 | chunkEvents.add(ImageChunkEvent( 34 | cumulativeBytesLoaded: progress.currentBytes, 35 | expectedTotalBytes: progress.expectedBytes) 36 | ); 37 | } 38 | 39 | if(finishProgress!.data != null){ 40 | return finishProgress.data!; 41 | } 42 | 43 | var file = finishProgress.getFile(); 44 | return await file.readAsBytes(); 45 | } 46 | 47 | @override 48 | Future obtainKey(ImageConfiguration configuration) { 49 | return SynchronousFuture(this); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/foundation/log.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:pica_comic/tools/extensions.dart'; 5 | 6 | void log(String content, 7 | [String title = "debug", LogLevel level = LogLevel.info]) { 8 | LogManager.addLog(level, title, content); 9 | } 10 | 11 | class LogManager { 12 | static final List _logs = []; 13 | 14 | static List get logs => _logs; 15 | 16 | static const maxLogLength = 3000; 17 | 18 | static const maxLogNumber = 500; 19 | 20 | static bool ignoreLimitation = false; 21 | 22 | static void printWarning(String text) { 23 | print('\x1B[33m$text\x1B[0m'); 24 | } 25 | 26 | static void printError(String text) { 27 | print('\x1B[31m$text\x1B[0m'); 28 | } 29 | 30 | static void addLog(LogLevel level, String title, String content) { 31 | if (!ignoreLimitation && content.length > maxLogLength) { 32 | content = "${content.substring(0, maxLogLength)}..."; 33 | } 34 | 35 | if (kDebugMode) { 36 | switch (level) { 37 | case LogLevel.error: 38 | printError("$title: $content"); 39 | case LogLevel.warning: 40 | printWarning("$title: $content"); 41 | case LogLevel.info: 42 | print("$title: $content"); 43 | } 44 | } 45 | 46 | var newLog = Log(level, title, content); 47 | 48 | if (newLog == _logs.lastOrNull) { 49 | return; 50 | } 51 | 52 | _logs.add(newLog); 53 | writeLog(level, title, content); 54 | if (_logs.length > maxLogNumber) { 55 | var res = _logs.remove( 56 | _logs.firstWhereOrNull((element) => element.level == LogLevel.info)); 57 | if (!res) { 58 | _logs.removeAt(0); 59 | } 60 | } 61 | } 62 | 63 | static void clear() => _logs.clear(); 64 | 65 | @override 66 | String toString() { 67 | var res = "Logs\n\n"; 68 | for (var log in _logs) { 69 | res += log.toString(); 70 | } 71 | return res; 72 | } 73 | 74 | static File? logFile; 75 | 76 | static void writeLog(LogLevel level, String title, String content) { 77 | if(logFile != null) { 78 | logFile!.writeAsString( 79 | "${DateTime.now().toIso8601String()} ${level.name}\n$title: $content\n\n", 80 | mode: FileMode.append, 81 | ); 82 | } 83 | } 84 | } 85 | 86 | class Log { 87 | final LogLevel level; 88 | final String title; 89 | final String content; 90 | final DateTime time = DateTime.now(); 91 | 92 | @override 93 | toString() => "${level.name} $title $time \n$content\n\n"; 94 | 95 | Log(this.level, this.title, this.content); 96 | 97 | static void info(String title, String message) { 98 | LogManager.addLog(LogLevel.info, title, message); 99 | } 100 | 101 | static void warning(String title, String message) { 102 | LogManager.addLog(LogLevel.warning, title, message); 103 | } 104 | 105 | static void error(String title, String message) { 106 | LogManager.addLog(LogLevel.error, title, message); 107 | } 108 | 109 | @override 110 | bool operator ==(Object other) { 111 | if (other is! Log) return false; 112 | return other.level == level && other.title == title && other.content == content; 113 | } 114 | 115 | @override 116 | int get hashCode => level.hashCode ^ title.hashCode ^ content.hashCode; 117 | } 118 | 119 | enum LogLevel { error, warning, info } 120 | -------------------------------------------------------------------------------- /lib/foundation/pair.dart: -------------------------------------------------------------------------------- 1 | class Pair{ 2 | M left; 3 | V right; 4 | 5 | Pair(this.left, this.right); 6 | 7 | Pair.fromMap(Map map, M key): left = key, right = map[key] 8 | ?? (throw Exception("Pair not found")); 9 | } -------------------------------------------------------------------------------- /lib/foundation/stack.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | class Stack{ 4 | final Queue _values; 5 | 6 | Stack():_values = Queue(); 7 | 8 | int get length => _values.length; 9 | bool get isEmpty => _values.isEmpty; 10 | bool get isNotEmpty => _values.isNotEmpty; 11 | 12 | void push(T value){ 13 | _values.addLast(value); 14 | } 15 | 16 | T pop(){ 17 | return _values.removeLast(); 18 | } 19 | 20 | T get last => _values.last; 21 | } -------------------------------------------------------------------------------- /lib/foundation/ui_mode.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'app.dart' as app; 3 | 4 | class UiMode{ 5 | static bool m1(BuildContext context){ 6 | return app.App.uiMode(context) == app.UiModes.m1; 7 | } 8 | 9 | static bool m2(BuildContext context){ 10 | return app.App.uiMode(context) == app.UiModes.m2; 11 | } 12 | 13 | static bool m3(BuildContext context){ 14 | return app.App.uiMode(context) == app.UiModes.m3; 15 | } 16 | } -------------------------------------------------------------------------------- /lib/network/base_comic.dart: -------------------------------------------------------------------------------- 1 | abstract class BaseComic { 2 | String get title; 3 | 4 | String get subTitle; 5 | 6 | String get cover; 7 | 8 | String get id; 9 | 10 | List get tags; 11 | 12 | String get description; 13 | 14 | bool get enableTagsTranslation => false; 15 | 16 | const BaseComic(); 17 | } 18 | 19 | class CustomComic extends BaseComic { 20 | @override 21 | final String title; 22 | 23 | @override 24 | final String subTitle; 25 | 26 | @override 27 | final String cover; 28 | 29 | @override 30 | final String id; 31 | 32 | @override 33 | final List tags; 34 | 35 | @override 36 | final String description; 37 | 38 | final String sourceKey; 39 | 40 | const CustomComic( 41 | this.title, 42 | this.subTitle, 43 | this.cover, 44 | this.id, 45 | this.tags, 46 | this.description, 47 | this.sourceKey, 48 | ); 49 | 50 | CustomComic.fromJson(Map json, this.sourceKey) 51 | : title = json["title"], 52 | subTitle = json["subTitle"] ?? "", 53 | cover = json["cover"], 54 | id = json["id"], 55 | tags = List.from(json["tags"] ?? []), 56 | description = json["description"] ?? ""; 57 | } 58 | -------------------------------------------------------------------------------- /lib/network/cache_network.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:typed_data'; 4 | import 'package:crypto/crypto.dart'; 5 | import 'package:dio/dio.dart'; 6 | import 'package:pica_comic/foundation/cache_manager.dart'; 7 | import 'package:pica_comic/network/cookie_jar.dart'; 8 | import 'package:pica_comic/network/http_client.dart'; 9 | import 'app_dio.dart'; 10 | 11 | ///缓存网络请求, 仅提供get方法, 其它的没有意义 12 | class CachedNetwork { 13 | Future> get(String url, BaseOptions options, 14 | {CacheExpiredTime expiredTime = CacheExpiredTime.short, 15 | CookieJarSql? cookieJar, 16 | bool log = true, 17 | bool http2 = false}) async { 18 | await setNetworkProxy(); 19 | var fileName = md5.convert(const Utf8Encoder().convert(url)).toString(); 20 | if (fileName.length > 20) { 21 | fileName = fileName.substring(0, 21); 22 | } 23 | final key = url; 24 | if (expiredTime != CacheExpiredTime.no) { 25 | var cache = await CacheManager().findCache(key); 26 | if (cache != null) { 27 | var file = File(cache); 28 | return CachedNetworkRes(await file.readAsString(), 200, url); 29 | } 30 | } 31 | options.responseType = ResponseType.bytes; 32 | var dio = log ? logDio(options, http2) : Dio(options); 33 | if (cookieJar != null) { 34 | dio.interceptors.add(CookieManagerSql(cookieJar)); 35 | } 36 | 37 | var res = await dio.get(url); 38 | if (res.data == null && !url.contains("random")) { 39 | throw Exception("Empty data"); 40 | } 41 | if (expiredTime != CacheExpiredTime.no) { 42 | await CacheManager().writeCache(key, res.data!, expiredTime.time); 43 | } 44 | return CachedNetworkRes(utf8.decode(res.data!, allowMalformed: true), 45 | res.statusCode, res.realUri.toString(), res.headers.map); 46 | } 47 | 48 | void delete(String url) async { 49 | await CacheManager().delete(url); 50 | } 51 | } 52 | 53 | enum CacheExpiredTime { 54 | no(-1), 55 | short(86400000), 56 | long(604800000), 57 | persistent(0); 58 | 59 | ///过期时间, 单位为微秒 60 | final int time; 61 | 62 | const CacheExpiredTime(this.time); 63 | } 64 | 65 | class CachedNetworkRes { 66 | T data; 67 | int? statusCode; 68 | Map> headers; 69 | String url; 70 | 71 | CachedNetworkRes(this.data, this.statusCode, this.url, 72 | [this.headers = const {}]); 73 | } 74 | -------------------------------------------------------------------------------- /lib/network/eh_network/get_gallery_id.dart: -------------------------------------------------------------------------------- 1 | ///从画廊链接中获取画廊id 2 | String getGalleryId(String url){ 3 | var i = url.indexOf("/g/"); 4 | i += 3; 5 | String res = ""; 6 | while(i < url.length){ 7 | res += url[i]; 8 | i++; 9 | if(url[i] == '/'){ 10 | break; 11 | } 12 | } 13 | return res; 14 | } -------------------------------------------------------------------------------- /lib/network/hitomi_network/fetch_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'package:pica_comic/foundation/def.dart'; 3 | import 'package:pica_comic/foundation/log.dart'; 4 | import 'package:pica_comic/network/res.dart'; 5 | import 'package:dio/dio.dart'; 6 | 7 | import '../http_client.dart'; 8 | 9 | ///改写自 hitomi.la 网站上的js脚本 10 | /// 11 | /// 接收byte数据, 将每4个byte合成1个int32即为漫画id 12 | /// 13 | /// 发送请求时需要在请求头设置开始接收位置和最后接收位置, 14 | /// 15 | /// 获取主页时不需要传入end, 因为需要和js脚本保持一致, 设置获取宽度100, 避免出现问题 16 | /// 17 | /// 响应头中 Content-Range 指明数据范围, 此函数用subData形式返回此值 18 | Future>> fetchComicData(String url, int start, {int? maxLength, int? endData, String? ref}) async{ 19 | await getProxy(); 20 | try{ 21 | var end = start + 100 - 1; 22 | if(endData != null){ 23 | end = endData; 24 | } 25 | if(maxLength != null && maxLength < end){ 26 | end = maxLength; 27 | } 28 | assert(start < end); 29 | var dio = Dio(BaseOptions( 30 | connectTimeout: const Duration(seconds: 5), 31 | receiveTimeout: const Duration(seconds: 5), 32 | sendTimeout: const Duration(seconds: 5), 33 | )); 34 | dio.options.responseType = ResponseType.bytes; 35 | dio.options.headers = { 36 | "User-Agent": webUA, 37 | "Range": "bytes=$start-$end", 38 | if(ref != null) 39 | "Referer": ref 40 | }; 41 | var res = await dio.get(url); 42 | var bytes = Uint8List.fromList(res.data); 43 | var comicIds = []; 44 | for (int i = 0; i < bytes.length; i += 4) { 45 | Int8List list = Int8List(4); 46 | list[0] = bytes[i]; 47 | list[1] = bytes[i + 1]; 48 | list[2] = bytes[i + 2]; 49 | list[3] = bytes[i + 3]; 50 | int number = list.buffer.asByteData().getInt32(0); 51 | comicIds.add(number); 52 | } 53 | var range = (res.headers["content-range"]?? res.headers["Content-Range"])![0]; 54 | int i = 0; 55 | for(;i size; 21 | 22 | @override 23 | List get downloadedEps => [0]; 24 | 25 | @override 26 | List get eps => ["EP 1"]; 27 | 28 | @override 29 | String get id => "Ht${comic.id}"; 30 | 31 | @override 32 | String get name => comic.name; 33 | 34 | @override 35 | String get subTitle => comic.uploader; 36 | 37 | @override 38 | DownloadType get type => DownloadType.htmanga; 39 | 40 | @override 41 | Map toJson() => {"comic": comic.toJson(), "size": size}; 42 | 43 | DownloadedHtComic.fromJson(Map json) 44 | : comic = HtComicInfo.fromJson(json["comic"]), 45 | size = json["size"]; 46 | 47 | @override 48 | set comicSize(double? value) => size = value; 49 | 50 | @override 51 | List get tags => comic.tags.keys.toList(); 52 | } 53 | 54 | class DownloadingHtComic extends DownloadingItem { 55 | DownloadingHtComic( 56 | this.comic, super.whenFinish, super.whenError, super.updateInfo, super.id, 57 | {super.type = DownloadType.htmanga}); 58 | 59 | final HtComicInfo comic; 60 | 61 | String _getCover() { 62 | var uri = comic.coverPath; 63 | if (uri.contains("https:") && !uri.contains("https://")) { 64 | uri = uri.replaceFirst("https:", "https://"); 65 | } 66 | return uri; 67 | } 68 | 69 | @override 70 | String get cover => _getCover(); 71 | 72 | @override 73 | String get title => comic.name; 74 | 75 | @override 76 | Future>> getLinks() async { 77 | var res = await HtmangaNetwork().getImages(comic.id); 78 | return {0: res.data}; 79 | } 80 | 81 | @override 82 | Stream downloadImage(String link) { 83 | return ImageManager().getImage(link); 84 | } 85 | 86 | @override 87 | Map toMap() => 88 | {"comic": comic.toJson(), ...super.toBaseMap()}; 89 | 90 | DownloadingHtComic.fromMap( 91 | Map map, 92 | DownloadProgressCallback whenFinish, 93 | DownloadProgressCallback whenError, 94 | DownloadProgressCallbackAsync updateInfo, 95 | String id) 96 | : comic = HtComicInfo.fromJson(map["comic"]), 97 | super.fromMap(map, whenFinish, whenError, updateInfo); 98 | 99 | @override 100 | FutureOr toDownloadedItem() async { 101 | return DownloadedHtComic( 102 | comic, 103 | await getFolderSize(Directory(path)), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/network/htmanga_network/models.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:pica_comic/foundation/history.dart'; 3 | import 'package:pica_comic/network/base_comic.dart'; 4 | 5 | @immutable 6 | class HtHomePageData { 7 | final List> comics; 8 | final Map links; 9 | 10 | /// 主页 11 | const HtHomePageData(this.comics, this.links); 12 | } 13 | 14 | @immutable 15 | class HtComicBrief extends BaseComic{ 16 | final String name; 17 | final String time; 18 | final String image; 19 | final int pages; 20 | @override 21 | final String id; 22 | final String? favoriteId; 23 | 24 | /// 漫画简略信息 25 | const HtComicBrief(this.name, this.time, this.image, this.id, this.pages, 26 | {this.favoriteId}); 27 | 28 | @override 29 | String get cover => image; 30 | 31 | @override 32 | String get description => time; 33 | 34 | @override 35 | String get subTitle => id; 36 | 37 | @override 38 | List get tags => const []; 39 | 40 | @override 41 | String get title => name; 42 | } 43 | 44 | @immutable 45 | class HtComicInfo with HistoryMixin { 46 | final String id; 47 | final String coverPath; 48 | final String name; 49 | final String category; 50 | final int pages; 51 | final Map tags; 52 | final String description; 53 | final String uploader; 54 | final String avatar; 55 | final int uploadNum; 56 | final List thumbnails; 57 | 58 | const HtComicInfo(this.id, this.coverPath, this.name, this.category, this.pages, this.tags, 59 | this.description, this.uploader, this.avatar, this.uploadNum, this.thumbnails); 60 | 61 | HtComicBrief toBrief() => HtComicBrief(name, "", coverPath, id, pages); 62 | 63 | Map toJson() => { 64 | "id": id, 65 | "coverPath": coverPath, 66 | "name": name, 67 | "category": category, 68 | "pages": pages, 69 | "tags": tags, 70 | "description": description, 71 | "uploader": uploader, 72 | "avatar": avatar, 73 | "uploadNum": uploadNum 74 | }; 75 | 76 | HtComicInfo.fromJson(Map json): 77 | id = json["id"], 78 | coverPath = json["coverPath"], 79 | name = json["name"], 80 | category = json["category"], 81 | pages = json["pages"], 82 | tags = Map.from(json["tags"]), 83 | description = json["description"], 84 | uploader = json["uploader"], 85 | avatar = json["avatar"], 86 | uploadNum = json["uploadNum"], 87 | thumbnails = []; 88 | 89 | @override 90 | String get cover => coverPath; 91 | 92 | @override 93 | HistoryType get historyType => HistoryType.htmanga; 94 | 95 | @override 96 | String get subTitle => uploader; 97 | 98 | @override 99 | String get target => id; 100 | 101 | @override 102 | String get title => name; 103 | } 104 | -------------------------------------------------------------------------------- /lib/network/jm_network/headers.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:crypto/crypto.dart'; 3 | import 'package:dio/dio.dart'; 4 | import 'dart:math' as math; 5 | import 'jm_network.dart'; 6 | 7 | var _device = ''; 8 | 9 | String get _jmUA { 10 | // 生成随机的设备标识符 11 | if(_device.isEmpty) { 12 | var chars = "abcdefghijklmnopqrstuvwxyz0123456789"; 13 | var random = math.Random(); 14 | for (var i = 0; i < 9; i++) { 15 | _device += chars[random.nextInt(chars.length)]; 16 | } 17 | } 18 | return "Mozilla/5.0 (Linux; Android 13; $_device Build/TQ1A.230305.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/114.0.5735.196 Safari/537.36"; 19 | } 20 | 21 | const _jmVersion = "1.7.2"; 22 | 23 | const _jmAuthKey = "18comicAPPContent"; 24 | 25 | BaseOptions getHeader(int time, 26 | {bool post = false, Map? headers, bool byte = true}) { 27 | var token = md5.convert(const Utf8Encoder().convert("$time$_jmAuthKey")); 28 | 29 | return BaseOptions( 30 | receiveDataWhenStatusError: true, 31 | connectTimeout: const Duration(seconds: 8), 32 | responseType: byte ? ResponseType.bytes : null, 33 | headers: { 34 | "token": token.toString(), 35 | "tokenparam": "$time,$_jmVersion", 36 | "user-agent": _jmUA, 37 | "accept-encoding": "gzip", 38 | "Host": JmNetwork().baseUrl.replaceFirst("https://", ""), 39 | ...headers ?? {}, 40 | if (post) "Content-Type": "application/x-www-form-urlencoded" 41 | }); 42 | } -------------------------------------------------------------------------------- /lib/network/jm_network/jm_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:pica_comic/base.dart'; 2 | 3 | const imageUrls = [ 4 | "https://cdn-msp.jmapiproxy3.cc", 5 | "https://cdn-msp3.jmapiproxy3.cc", 6 | "https://cdn-msp2.jmapiproxy1.cc", 7 | "https://cdn-msp3.jmapiproxy3.cc", 8 | "https://cdn-msp2.jmapiproxy4.cc", 9 | "https://cdn-msp2.jmapiproxy3.cc", 10 | ]; 11 | 12 | String getBaseUrl(){ 13 | return imageUrls[int.parse(appdata.settings[37])]; 14 | } 15 | 16 | String getJmCoverUrl(String id) { 17 | return "${getBaseUrl()}/media/albums/${id}_3x4.jpg"; 18 | } 19 | 20 | String getJmImageUrl(String imageName, String id) { 21 | return "${getBaseUrl()}/media/photos/$id/$imageName"; 22 | } 23 | 24 | String getJmAvatarUrl(String imageName) { 25 | return "${getBaseUrl()}/media/users/$imageName"; 26 | } 27 | -------------------------------------------------------------------------------- /lib/network/nhentai_network/download.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | import 'dart:typed_data'; 5 | import 'package:pica_comic/network/nhentai_network/nhentai_main_network.dart'; 6 | import 'package:pica_comic/tools/translations.dart'; 7 | import '../../base.dart'; 8 | import '../../foundation/image_manager.dart'; 9 | import '../../tools/io_tools.dart'; 10 | import '../download_model.dart'; 11 | 12 | class NhentaiDownloadedComic extends DownloadedItem { 13 | NhentaiDownloadedComic( 14 | this.comicID, this.title, this.size, this.cover, this.tags); 15 | 16 | final String comicID; 17 | 18 | final String title; 19 | 20 | final double? size; 21 | 22 | final String cover; 23 | 24 | @override 25 | double? get comicSize => size; 26 | 27 | @override 28 | List get downloadedEps => [0]; 29 | 30 | @override 31 | List get eps => ["第一章".tl]; 32 | 33 | @override 34 | String get id => comicID; 35 | 36 | @override 37 | String get name => title; 38 | 39 | @override 40 | String get subTitle => ""; 41 | 42 | @override 43 | DownloadType get type => DownloadType.nhentai; 44 | 45 | @override 46 | Map toJson() => 47 | {'comicID': comicID, 'title': title, 'size': size, 'cover': cover}; 48 | 49 | NhentaiDownloadedComic.fromJson(Map json) 50 | : comicID = json["comicID"], 51 | title = json["title"], 52 | size = json["size"], 53 | tags = List.from(json["tags"] ?? []), 54 | cover = json["cover"]; 55 | 56 | @override 57 | set comicSize(double? value) {} 58 | 59 | @override 60 | List tags; 61 | } 62 | 63 | class NhentaiDownloadingItem extends DownloadingItem { 64 | NhentaiDownloadingItem( 65 | this.comic, super.whenFinish, super.whenError, super.updateInfo, super.id, 66 | {super.type = DownloadType.nhentai}); 67 | 68 | final NhentaiComic comic; 69 | 70 | @override 71 | String get cover => comic.cover; 72 | 73 | @override 74 | Future>> getLinks() async { 75 | var res = await NhentaiNetwork().getImages(comic.id); 76 | return {0: res.data}; 77 | } 78 | 79 | @override 80 | Stream downloadImage(String link) { 81 | return ImageManager().getImage(link); 82 | } 83 | 84 | @override 85 | String get title => comic.title; 86 | 87 | @override 88 | Map toMap() => 89 | {"comic": comic.toMap(), ...super.toBaseMap()}; 90 | 91 | NhentaiDownloadingItem.fromMap( 92 | Map map, 93 | DownloadProgressCallback whenFinish, 94 | DownloadProgressCallback whenError, 95 | DownloadProgressCallbackAsync updateInfo, 96 | String id) 97 | : comic = NhentaiComic.fromMap(map["comic"]), 98 | super.fromMap(map, whenFinish, whenError, updateInfo); 99 | 100 | @override 101 | FutureOr toDownloadedItem() async { 102 | return NhentaiDownloadedComic( 103 | id, 104 | title, 105 | await getFolderSize(Directory(path)), 106 | comic.cover, 107 | comic.tags["tags"] ?? [], 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/network/nhentai_network/login.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' as io; 2 | 3 | import 'package:pica_comic/base.dart'; 4 | import 'package:pica_comic/components/components.dart'; 5 | import 'package:pica_comic/foundation/app.dart'; 6 | import 'package:pica_comic/network/nhentai_network/nhentai_main_network.dart'; 7 | import 'package:pica_comic/pages/webview.dart'; 8 | import 'package:pica_comic/tools/translations.dart'; 9 | 10 | 11 | void nhLogin(void Function() onFinished) async{ 12 | if(NhentaiNetwork().baseUrl.contains("xxx")){ 13 | showToast(message: "暂不支持"); 14 | return; 15 | } 16 | 17 | if(App.isDesktop && (await DesktopWebview.isAvailable())){ 18 | var webview = DesktopWebview( 19 | initialUrl: "${NhentaiNetwork().baseUrl}/login/?next=/", 20 | onTitleChange: (title, controller) async{ 21 | print(title); 22 | if(title == "nhentai.net") return; 23 | if (!title.contains("Login") && !title.contains("Register") && title.contains("nhentai")) { 24 | var ua = controller.userAgent; 25 | if(ua != null){ 26 | appdata.implicitData[3] = ua; 27 | appdata.writeImplicitData(); 28 | } 29 | var cookies = await controller.getCookies("${NhentaiNetwork().baseUrl}/"); 30 | List cookiesList = []; 31 | cookies.forEach((key, value) { 32 | var cookie = io.Cookie(key, value); 33 | if(key == "sessionid" || key == "XSRF-TOKEN"){ 34 | NhentaiNetwork().logged = true; 35 | } 36 | cookie.domain = ".nhentai.net"; 37 | cookiesList.add(cookie); 38 | }); 39 | NhentaiNetwork().cookieJar!.saveFromResponse( 40 | Uri.parse(NhentaiNetwork().baseUrl), cookiesList); 41 | onFinished(); 42 | controller.close(); 43 | } 44 | }, 45 | ); 46 | webview.open(); 47 | } else if(App.isMobile) { 48 | App.globalTo(() => AppWebview( 49 | initialUrl: "${NhentaiNetwork().baseUrl}/login/?next=/", 50 | singlePage: true, 51 | onTitleChange: (title, controller) async{ 52 | if (!title.contains("Login") && !title.contains("Register") && title.contains("nhentai")) { 53 | var ua = await controller.getUA(); 54 | if(ua != null){ 55 | appdata.implicitData[3] = ua; 56 | appdata.writeImplicitData(); 57 | } 58 | var cookies = await controller.getCookies("${NhentaiNetwork().baseUrl}/") ?? {}; 59 | List cookiesList = []; 60 | cookies.forEach((key, value) { 61 | var cookie = io.Cookie(key, value); 62 | if(key == "sessionid" || key == "XSRF-TOKEN"){ 63 | NhentaiNetwork().logged = true; 64 | } 65 | cookie.domain = ".nhentai.net"; 66 | cookiesList.add(cookie); 67 | }); 68 | NhentaiNetwork().cookieJar!.saveFromResponse( 69 | Uri.parse(NhentaiNetwork().baseUrl), cookiesList); 70 | onFinished(); 71 | App.globalBack(); 72 | } 73 | }, 74 | )); 75 | } else { 76 | showToast(message: "当前设备不支持".tl); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/network/nhentai_network/models.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:pica_comic/foundation/history.dart'; 3 | import 'package:pica_comic/network/base_comic.dart'; 4 | 5 | @immutable 6 | class NhentaiComicBrief extends BaseComic{ 7 | @override 8 | final String title; 9 | @override 10 | final String cover; 11 | @override 12 | final String id; 13 | final String lang; 14 | @override 15 | final List tags; 16 | 17 | const NhentaiComicBrief(this.title, this.cover, this.id, this.lang, this.tags); 18 | 19 | @override 20 | String get description => lang; 21 | 22 | @override 23 | String get subTitle => id; 24 | 25 | @override 26 | bool get enableTagsTranslation => true; 27 | } 28 | 29 | class NhentaiHomePageData{ 30 | final List popular; 31 | List latest; 32 | int page = 1; 33 | 34 | NhentaiHomePageData(this.popular, this.latest); 35 | } 36 | 37 | class NhentaiComic with HistoryMixin{ 38 | String id; 39 | @override 40 | String title; 41 | @override 42 | String subTitle; 43 | @override 44 | String cover; 45 | Map> tags; 46 | bool favorite; 47 | List thumbnails; 48 | List recommendations; 49 | String token; 50 | 51 | NhentaiComic(this.id, this.title, this.subTitle, this.cover, this.tags, this.favorite, 52 | this.thumbnails, this.recommendations, this.token); 53 | 54 | Map toMap() => { 55 | "id": id, 56 | "title": title, 57 | "subTitle": subTitle, 58 | "cover": cover, 59 | }; 60 | 61 | NhentaiComic.fromMap(Map map): 62 | id = map["id"], 63 | title = map["title"], 64 | subTitle = map["subTitle"], 65 | cover = map["cover"], 66 | tags = {}, 67 | favorite = false, 68 | thumbnails = [], 69 | recommendations = [], 70 | token = ""; 71 | 72 | @override 73 | HistoryType get historyType => HistoryType.nhentai; 74 | 75 | @override 76 | String get target => id; 77 | } 78 | 79 | class NhentaiComment{ 80 | String userName; 81 | String avatar; 82 | String content; 83 | int date; 84 | 85 | NhentaiComment(this.userName, this.avatar, this.content, this.date); 86 | } -------------------------------------------------------------------------------- /lib/network/picacg_network/headers.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:crypto/crypto.dart'; 3 | import 'package:pica_comic/comic_source/built_in/picacg.dart'; 4 | import 'dart:convert'; 5 | import 'package:uuid/uuid.dart'; 6 | 7 | var apiKey = "C69BAF41DA5ABD1FFEDC6D2FEA56B"; 8 | 9 | String createNonce() { 10 | var uuid = const Uuid(); 11 | String nonce = uuid.v1(); 12 | return nonce.replaceAll("-", ""); 13 | } 14 | 15 | String createSignature(String path, String nonce, String time, String method) { 16 | String key = path + time + nonce + method + apiKey; 17 | String data = 18 | '~d}\$Q7\$eIni=V)9\\RK/P.RM4;9[7|@/CA}b~OW!3?EV`:<>M7pddUBL5n|0/*Cn'; 19 | var s = utf8.encode(key.toLowerCase()); 20 | var f = utf8.encode(data); 21 | var hmacSha256 = Hmac(sha256, f); 22 | var digest = hmacSha256.convert(s); 23 | return digest.toString(); 24 | } 25 | 26 | BaseOptions getHeaders(String method, String token, String url) { 27 | var nonce = createNonce(); 28 | var time = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(); 29 | var signature = createSignature(url, nonce, time, method.toUpperCase()); 30 | var headers = { 31 | "api-key": "C69BAF41DA5ABD1FFEDC6D2FEA56B", 32 | "accept": "application/vnd.picacomic.com.v1+json", 33 | "app-channel": picacg.data['appChannel'] ?? '3', 34 | "authorization": token, 35 | "time": time, 36 | "nonce": nonce, 37 | "app-version": "2.2.1.3.3.4", 38 | "app-uuid": "defaultUuid", 39 | "image-quality": picacg.data['imageQuality'] ?? "original", 40 | "app-platform": "android", 41 | "app-build-version": "45", 42 | "Content-Type": "application/json; charset=UTF-8", 43 | "user-agent": "okhttp/3.8.1", 44 | "version": "v1.4.1", 45 | "Host": "picaapi.picacomic.com", 46 | "signature": signature, 47 | }; 48 | return BaseOptions( 49 | receiveDataWhenStatusError: true, 50 | responseType: ResponseType.plain, 51 | headers: headers, 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /lib/network/res.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | @immutable 4 | class Res { 5 | /// error info 6 | final String? errorMessage; 7 | 8 | String get errorMessageWithoutNull => errorMessage ?? "Unknown Error"; 9 | 10 | /// data 11 | final T? _data; 12 | 13 | /// is there an error 14 | bool get error => errorMessage != null; 15 | 16 | /// whether succeed 17 | bool get success => !error; 18 | 19 | /// data 20 | /// 21 | /// must be called when no error happened 22 | T get data => _data ?? (throw Exception(errorMessage)); 23 | 24 | /// get data, or null if there is an error 25 | T? get dataOrNull => _data; 26 | 27 | final dynamic subData; 28 | 29 | @override 30 | String toString() => _data.toString(); 31 | 32 | Res.fromErrorRes(Res another, {this.subData}) 33 | : _data = null, 34 | errorMessage = another.errorMessageWithoutNull; 35 | 36 | /// network result 37 | const Res(this._data, {this.errorMessage, this.subData}); 38 | 39 | const Res.error(String err) 40 | : _data = null, 41 | subData = null, 42 | errorMessage = err; 43 | } 44 | -------------------------------------------------------------------------------- /lib/network/update.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:pica_comic/base.dart'; 3 | import 'package:pica_comic/network/app_dio.dart'; 4 | 5 | String? _updateInfo; 6 | 7 | Future getLatestVersion() async { 8 | var dio = logDio(); 9 | var res = await dio 10 | .get("https://api.github.com/repos/wgh136/PicaComic/releases/latest"); 11 | _updateInfo = res.data["body"]; 12 | return (res.data["tag_name"] as String).replaceFirst("v", ""); 13 | } 14 | 15 | Future checkUpdate() async { 16 | try { 17 | var version = appVersion; 18 | var res = await getLatestVersion(); 19 | return compareSemVer(res, version); 20 | } catch (e) { 21 | return null; 22 | } 23 | } 24 | 25 | bool compareSemVer(String ver1, String ver2) { 26 | ver1 = ver1.replaceFirst("-", "."); 27 | ver2 = ver2.replaceFirst("-", "."); 28 | List v1 = ver1.split('.'); 29 | List v2 = ver2.split('.'); 30 | 31 | for (int i = 0; i < 3; i++) { 32 | int num1 = int.parse(v1[i]); 33 | int num2 = int.parse(v2[i]); 34 | 35 | if (num1 > num2) { 36 | return true; 37 | } else if (num1 < num2) { 38 | return false; 39 | } 40 | } 41 | 42 | var v14 = v1.elementAtOrNull(3); 43 | var v24 = v2.elementAtOrNull(3); 44 | 45 | if (v14 != v24) { 46 | if (v14 == null && v24 != "hotfix") { 47 | return true; 48 | } else if (v14 == null) { 49 | return false; 50 | } 51 | if (v24 == null) { 52 | if (v14 == "hotfix") { 53 | return true; 54 | } 55 | return false; 56 | } 57 | return v14.compareTo(v24) > 0; 58 | } 59 | 60 | return false; 61 | } 62 | 63 | Future getUpdatesInfo() async { 64 | if(_updateInfo == null) return null; 65 | _updateInfo!.replaceAll('\r\n', '\n'); 66 | var lines = _updateInfo!.split("\n"); 67 | if(lines.length > 5) { 68 | lines.add("..."); 69 | return lines.sublist(5).join("\n"); 70 | } 71 | return _updateInfo; 72 | } 73 | 74 | Future getDownloadUrl() async { 75 | return "https://github.com/wgh136/PicaComic/releases/latest"; 76 | } 77 | -------------------------------------------------------------------------------- /lib/pages/auth_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/scheduler.dart'; 2 | import 'package:pica_comic/foundation/app.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:local_auth/local_auth.dart'; 5 | import 'package:pica_comic/pages/main_page.dart'; 6 | import 'package:pica_comic/tools/translations.dart'; 7 | 8 | class AuthPage extends StatefulWidget { 9 | const AuthPage({Key? key}) : super(key: key); 10 | 11 | static bool lock = false; 12 | 13 | static bool initial = true; 14 | 15 | @override 16 | State createState() => _AuthPageState(); 17 | } 18 | 19 | class _AuthPageState extends State with WidgetsBindingObserver { 20 | @override 21 | void initState() { 22 | AuthPage.lock = true; 23 | WidgetsBinding.instance.addObserver(this); 24 | if(SchedulerBinding.instance.lifecycleState == AppLifecycleState.resumed) { 25 | auth(); 26 | } 27 | super.initState(); 28 | } 29 | 30 | @override 31 | void didChangeAppLifecycleState(AppLifecycleState state) { 32 | if(state == AppLifecycleState.resumed && AuthPage.lock && mounted && !inProgress) { 33 | auth(); 34 | } 35 | super.didChangeAppLifecycleState(state); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return GestureDetector( 41 | onTap: auth, 42 | child: Scaffold( 43 | body: PopScope( 44 | canPop: false, 45 | child: SizedBox( 46 | width: MediaQuery.of(context).size.width, 47 | height: MediaQuery.of(context).size.height, 48 | child: Center( 49 | child: SizedBox( 50 | height: 100, 51 | child: Column( 52 | children: [ 53 | Icon( 54 | Icons.security, 55 | size: 40, 56 | color: context.colorScheme.secondary, 57 | ), 58 | const SizedBox( 59 | height: 5, 60 | ), 61 | Text("点击完成身份验证".tl) 62 | ], 63 | ), 64 | ), 65 | ), 66 | ), 67 | ), 68 | ), 69 | ); 70 | } 71 | 72 | bool inProgress = false; 73 | 74 | void auth() async { 75 | if(inProgress) { 76 | return; 77 | } 78 | inProgress = true; 79 | var res = 80 | await LocalAuthentication().authenticate(localizedReason: "需要身份验证".tl); 81 | inProgress = false; 82 | if (res) { 83 | AuthPage.lock = false; 84 | if (AuthPage.initial) { 85 | App.offAll(() => const MainPage()); 86 | } else { 87 | App.globalBack(); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/pages/ehentai/accounts.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:pica_comic/components/components.dart'; 6 | import 'package:pica_comic/foundation/app.dart'; 7 | import 'package:pica_comic/network/eh_network/eh_main_network.dart'; 8 | import 'package:pica_comic/tools/translations.dart'; 9 | 10 | class CookieManagementView extends StatefulWidget { 11 | const CookieManagementView({super.key}); 12 | 13 | @override 14 | State createState() => _CookieManagementViewState(); 15 | } 16 | 17 | class _CookieManagementViewState extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | return ExpansionTile( 21 | title: const Text("cookies"), 22 | shape: const RoundedRectangleBorder(), 23 | children: [ 24 | ListTile( 25 | title: const Text("ipb_member_id"), 26 | subtitle: Text(EhNetwork().id), 27 | onTap: () => setClipboard(EhNetwork().id), 28 | ), 29 | ListTile( 30 | title: const Text("ipb_pass_hash"), 31 | subtitle: Text(EhNetwork().hash), 32 | onTap: () => setClipboard(EhNetwork().hash), 33 | ), 34 | ListTile( 35 | title: const Text("igneous"), 36 | subtitle: Text(EhNetwork().igneous), 37 | onTap: () => setClipboard(EhNetwork().igneous), 38 | trailing: IconButton( 39 | icon: const Icon(Icons.edit_outlined), 40 | onPressed: () { 41 | showDialog( 42 | context: context, 43 | builder: (context) { 44 | String text = EhNetwork().igneous; 45 | return AlertDialog( 46 | title: const Text("igneous"), 47 | content: TextField( 48 | controller: TextEditingController(text: text), 49 | onChanged: (s) => text = s, 50 | ), 51 | actions: [ 52 | TextButton(onPressed: context.pop, child: Text("取消".tl)), 53 | TextButton( 54 | onPressed: () { 55 | EhNetwork().igneous = text; 56 | EhNetwork().cookieJar.saveFromResponse( 57 | Uri.parse("https://exhentai.org"), 58 | [Cookie("igneous", text)], 59 | ); 60 | EhNetwork().cookieJar.saveFromResponse( 61 | Uri.parse("https://e-hentai.org"), 62 | [Cookie("igneous", text)], 63 | ); 64 | context.pop(); 65 | setState(() {}); 66 | }, 67 | child: Text("确定".tl), 68 | ), 69 | ], 70 | ); 71 | }, 72 | ); 73 | }, 74 | ), 75 | ), 76 | ], 77 | ); 78 | } 79 | 80 | void setClipboard(String text) { 81 | Clipboard.setData(ClipboardData(text: text)); 82 | showToast(message: "已复制".tl, icon: const Icon(Icons.check)); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/pages/ehentai/subscription.dart: -------------------------------------------------------------------------------- 1 | import 'package:pica_comic/components/components.dart'; 2 | import 'package:pica_comic/foundation/app.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:pica_comic/tools/translations.dart'; 5 | import '../../network/eh_network/eh_main_network.dart'; 6 | import '../../network/eh_network/eh_models.dart'; 7 | import '../../network/res.dart'; 8 | 9 | class SubscriptionPage extends StatefulWidget { 10 | const SubscriptionPage({super.key}); 11 | 12 | @override 13 | State createState() => _SubscriptionPageState(); 14 | } 15 | 16 | class _SubscriptionPageState extends State { 17 | @override 18 | Widget build(BuildContext context) { 19 | return Scaffold( 20 | appBar: Appbar(title: Text("EH订阅".tl), actions: [ 21 | Tooltip( 22 | message: "更多".tl, 23 | child: IconButton( 24 | icon: const Icon(Icons.more_horiz), 25 | onPressed: (){ 26 | Future.microtask(() => showDialog(context: App.globalContext!, builder: (context){ 27 | return AlertDialog( 28 | title: Text("订阅".tl), 29 | content: Text("其它漫画源的订阅尚未完成\n如需管理EH订阅, 请前往EH网站".tl), 30 | actions: [ 31 | TextButton(onPressed: ()=>App.globalBack(), child: Text("返回".tl)), 32 | ], 33 | ); 34 | })); 35 | }, 36 | ), 37 | ) 38 | ],), 39 | body: EhSubscriptionComics(), 40 | ); 41 | } 42 | } 43 | 44 | 45 | class PageData{ 46 | Galleries? galleries; 47 | int page = 1; 48 | Map> comics = {}; 49 | } 50 | 51 | class EhSubscriptionComics extends ComicsPage{ 52 | EhSubscriptionComics({super.key}); 53 | 54 | final data = PageData(); 55 | 56 | @override 57 | Future>> getComics(int i) async{ 58 | if(data.galleries == null){ 59 | Res res = await EhNetwork().getGalleries("${EhNetwork().ehBaseUrl}/watched"); 60 | if(res.error){ 61 | return Res(null, errorMessage: res.errorMessage); 62 | }else{ 63 | data.galleries = res.data; 64 | data.comics[1] = []; 65 | data.comics[1]!.addAll(data.galleries!.galleries); 66 | data.galleries!.galleries.clear(); 67 | } 68 | } 69 | if(data.comics[i] != null){ 70 | return Res(data.comics[i]!); 71 | }else{ 72 | while(data.comics[i] == null){ 73 | data.page++; 74 | if(! await EhNetwork().getNextPageGalleries(data.galleries!)){ 75 | return const Res(null, errorMessage: "网络错误"); 76 | } 77 | data.comics[data.page] = []; 78 | data.comics[data.page]!.addAll(data.galleries!.galleries); 79 | data.galleries!.galleries.clear(); 80 | } 81 | return Res(data.comics[i]); 82 | } 83 | } 84 | 85 | @override 86 | String? get tag => "EhSubscriptionPage"; 87 | 88 | @override 89 | String? get title => null; 90 | 91 | @override 92 | String get sourceKey => 'ehentai'; 93 | } -------------------------------------------------------------------------------- /lib/pages/picacg/collections_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pica_comic/network/picacg_network/methods.dart'; 3 | import 'package:pica_comic/tools/translations.dart'; 4 | import 'package:pica_comic/components/components.dart'; 5 | import 'package:pica_comic/foundation/app.dart'; 6 | 7 | class CollectionPageLogic extends StateController { 8 | bool isLoading = true; 9 | var c1 = []; 10 | var c2 = []; 11 | bool status = true; 12 | String? message; 13 | 14 | void change() { 15 | isLoading = !isLoading; 16 | update(); 17 | } 18 | 19 | void get() async { 20 | var collections = await network.getCollection(); 21 | if (collections.success) { 22 | c1 = collections.data[0]; 23 | c2 = collections.data[1]; 24 | change(); 25 | } else { 26 | status = false; 27 | message = collections.errorMessageWithoutNull; 28 | change(); 29 | } 30 | } 31 | } 32 | 33 | class CollectionsPage extends StatelessWidget { 34 | const CollectionsPage({Key? key}) : super(key: key); 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return Scaffold( 39 | appBar: AppBar( 40 | title: Text("推荐".tl), 41 | ), 42 | body: StateBuilder( 43 | init: CollectionPageLogic(), 44 | builder: (logic) { 45 | if (logic.isLoading) { 46 | network.getCollection().then((collections) { 47 | if (collections.success) { 48 | logic.c1 = collections.data[0]; 49 | logic.c2 = collections.data[1]; 50 | logic.change(); 51 | } else { 52 | logic.status = false; 53 | logic.change(); 54 | } 55 | }); 56 | return const Center( 57 | child: CircularProgressIndicator(), 58 | ); 59 | } else if (logic.status) { 60 | return CustomScrollView( 61 | slivers: [ 62 | SliverGridComics( 63 | comics: logic.c1 + logic.c2, 64 | sourceKey: 'picacg', 65 | ), 66 | SliverPadding( 67 | padding: EdgeInsets.only( 68 | top: MediaQuery.of(App.globalContext!).padding.bottom, 69 | ), 70 | ) 71 | ], 72 | ); 73 | } else { 74 | return NetworkError( 75 | message: logic.message ?? "网络错误".tl, 76 | retry: () { 77 | logic.status = true; 78 | logic.change(); 79 | }, 80 | ); 81 | } 82 | }, 83 | ), 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/pages/reader/reading_type.dart: -------------------------------------------------------------------------------- 1 | part of pica_reader; 2 | 3 | typedef ReadingType = ComicType; 4 | 5 | enum ReadingMethod { 6 | leftToRight, 7 | rightToLeft, 8 | topToBottom, 9 | topToBottomContinuously, 10 | twoPage, 11 | twoPageReversed; 12 | 13 | bool get isTwoPage => this == ReadingMethod.twoPage 14 | || this == ReadingMethod.twoPageReversed; 15 | 16 | bool get useComicImage => this == ReadingMethod.topToBottomContinuously || 17 | this == ReadingMethod.twoPage || this == ReadingMethod.twoPageReversed; 18 | } -------------------------------------------------------------------------------- /lib/pages/settings/ht_settings.dart: -------------------------------------------------------------------------------- 1 | part of pica_settings; 2 | 3 | class HtSettings extends StatefulWidget { 4 | const HtSettings(this.popUp, {super.key}); 5 | 6 | final bool popUp; 7 | 8 | @override 9 | State createState() => _HtSettingsState(); 10 | } 11 | 12 | class _HtSettingsState extends State { 13 | @override 14 | Widget build(BuildContext context) { 15 | return Column( 16 | children: [ 17 | ListTile( 18 | title: Text("绅士漫画".tl), 19 | ), 20 | ListTile( 21 | leading: const Icon(Icons.domain_rounded), 22 | title: Text("Domain: ${appdata.settings[31].replaceFirst("https://", "")}"), 23 | trailing: IconButton(onPressed: () => changeDomain(context), icon: const Icon(Icons.edit)), 24 | ) 25 | ], 26 | ); 27 | } 28 | 29 | void changeDomain(BuildContext context){ 30 | var controller = TextEditingController(); 31 | 32 | void onFinished() { 33 | var text = controller.text; 34 | if(!text.contains("https://")){ 35 | text = "https://$text"; 36 | } 37 | App.globalBack(); 38 | if(!text.isURL){ 39 | showToast(message: "Invalid URL"); 40 | }else { 41 | appdata.settings[31] = text; 42 | appdata.updateSettings(); 43 | setState(() {}); 44 | } 45 | } 46 | 47 | showDialog(context: context, builder: (context){ 48 | return SimpleDialog( 49 | title: const Text("Change Domain"), 50 | children: [ 51 | Container( 52 | padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), 53 | width: 400, 54 | child: TextField( 55 | decoration: const InputDecoration( 56 | border: OutlineInputBorder(), 57 | label: Text("Domain") 58 | ), 59 | controller: controller, 60 | onEditingComplete: onFinished, 61 | ), 62 | ), 63 | Row( 64 | mainAxisAlignment: MainAxisAlignment.end, 65 | children: [ 66 | TextButton(onPressed: onFinished, child: Text("完成".tl)), 67 | const SizedBox(width: 16,), 68 | ], 69 | ) 70 | ], 71 | ); 72 | }); 73 | } 74 | } -------------------------------------------------------------------------------- /lib/pages/settings/local_favorite_settings.dart: -------------------------------------------------------------------------------- 1 | part of pica_settings; 2 | 3 | class LocalFavoritesSettings extends StatefulWidget { 4 | const LocalFavoritesSettings({super.key}); 5 | 6 | @override 7 | State createState() => _LocalFavoritesSettingsState(); 8 | } 9 | 10 | class _LocalFavoritesSettingsState extends State { 11 | @override 12 | Widget build(BuildContext context) { 13 | return Column( 14 | children: [ 15 | ListTile( 16 | leading: const Icon(Icons.book), 17 | title: Text("快速收藏".tl), 18 | subtitle: Text("长按收藏按钮执行快速收藏".tl), 19 | trailing: Select( 20 | initialValue: LocalFavoritesManager() 21 | .folderNames 22 | .indexOf(appdata.settings[51]), 23 | onChange: (i) { 24 | appdata.settings[51] = 25 | LocalFavoritesManager().folderNames[i]; 26 | appdata.updateSettings(); 27 | }, 28 | values: LocalFavoritesManager().folderNames, 29 | ), 30 | ), 31 | SelectSettingWithAppdata( 32 | icon: const Icon(Icons.bookmark_add), 33 | title: "新收藏添加至".tl, 34 | options: ["最后".tl, "最前".tl], 35 | settingsIndex: 53, 36 | ), 37 | SelectSettingWithAppdata( 38 | icon: const Icon(Icons.move_up), 39 | title: "阅读后移动本地收藏至".tl, 40 | options: ["无操作".tl, "最后".tl, "最前".tl], 41 | settingsIndex: 54, 42 | ), 43 | SelectSettingWithAppdata( 44 | icon: const Icon(Icons.touch_app), 45 | title: "点击漫画时".tl, 46 | options: ["查看信息".tl, "阅读".tl], 47 | settingsIndex: 60, 48 | ), 49 | ListTile( 50 | leading: const Icon(Icons.sync), 51 | title: Text("下拉更新拉取页数".tl), 52 | trailing: Select( 53 | initialValue: ["1", "2", "3", "4", "5", "10", "99"] 54 | .indexOf(appdata.settings[71]), 55 | values: const ["1", "2", "3", "4", "5", "10", "99"], 56 | onChange: (i) { 57 | appdata.settings[71] = ["1", "2", "3", "4", "5", "10", "99"][i]; 58 | appdata.updateSettings(); 59 | }, 60 | width: 140, 61 | ), 62 | ), 63 | Padding( 64 | padding: 65 | EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom)) 66 | ], 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/pages/show_image_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_view/photo_view.dart'; 3 | import 'package:pica_comic/foundation/image_loader/cached_image.dart'; 4 | import 'package:pica_comic/foundation/image_manager.dart'; 5 | import 'package:pica_comic/tools/save_image.dart'; 6 | import 'package:pica_comic/tools/translations.dart'; 7 | 8 | class ShowImagePage extends StatelessWidget { 9 | const ShowImagePage(this.url, {super.key}); 10 | 11 | final String url; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: Text("图片".tl), 18 | ), 19 | body: PhotoView( 20 | minScale: PhotoViewComputedScale.contained * 0.9, 21 | imageProvider: CachedImageProvider(url), 22 | loadingBuilder: (context, event) { 23 | return Container( 24 | decoration: const BoxDecoration(color: Colors.black), 25 | child: const Center( 26 | child: CircularProgressIndicator(), 27 | ), 28 | ); 29 | }, 30 | ), 31 | ); 32 | } 33 | } 34 | 35 | class ShowImagePageWithHero extends StatelessWidget { 36 | const ShowImagePageWithHero(this.url, this.tag, {super.key}); 37 | 38 | final String url; 39 | 40 | final String tag; 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return Scaffold( 45 | appBar: AppBar( 46 | title: Text("图片".tl), 47 | actions: [ 48 | Tooltip( 49 | message: "保存".tl, 50 | child: IconButton( 51 | icon: const Icon(Icons.download), 52 | onPressed: () async{ 53 | var file = await ImageManager().getFile(url); 54 | if(file != null){ 55 | saveImage(file); 56 | } 57 | } 58 | )) 59 | ], 60 | ), 61 | body: Hero( 62 | tag: tag, 63 | child: PhotoView( 64 | minScale: PhotoViewComputedScale.contained * 0.9, 65 | imageProvider: CachedImageProvider(url), 66 | loadingBuilder: (context, event) { 67 | return Container( 68 | decoration: const BoxDecoration(color: Colors.black), 69 | child: const Center( 70 | child: CircularProgressIndicator(), 71 | ), 72 | ); 73 | }, 74 | ), 75 | ), 76 | ); 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /lib/tools/app_links.dart: -------------------------------------------------------------------------------- 1 | import 'package:pica_comic/components/components.dart'; 2 | import 'package:pica_comic/foundation/log.dart'; 3 | import 'package:pica_comic/tools/extensions.dart'; 4 | import '../foundation/app.dart'; 5 | import '../pages/ehentai/eh_gallery_page.dart'; 6 | import '../pages/hitomi/hitomi_comic_page.dart'; 7 | import '../pages/nhentai/comic_page.dart'; 8 | 9 | bool canHandle(String text){ 10 | if(!text.isURL){ 11 | return false; 12 | } 13 | var uri = Uri.parse(text); 14 | 15 | const acceptedHosts = ["e-hentai.org", "exhentai.org", "nhentai.net", "hitomi.la"]; 16 | 17 | return acceptedHosts.contains(uri.host); 18 | } 19 | 20 | bool handleAppLinks(Uri uri, {bool showMessageWhenError = true}){ 21 | LogManager.addLog(LogLevel.info, "App Link", "Open Link $uri"); 22 | var context = App.mainNavigatorKey!.currentContext!; 23 | switch(uri.host){ 24 | case "e-hentai.org": 25 | case "exhentai.org": 26 | if(uri.path.contains("/g/")){ 27 | context.to(() => EhGalleryPage.fromLink("https://${uri.host}${uri.path}")); 28 | } 29 | case "nhentai.net": 30 | if(uri.path.contains("/g/")){ 31 | context.to(() => NhentaiComicPage(uri.pathSegments.firstWhere((element) => element.isNum))); 32 | } 33 | case "hitomi.la": 34 | if(["doujinshi", "cg", "manga"].contains(uri.pathSegments[0])){ 35 | context.to(() => HitomiComicPage.fromLink("https://${uri.host}${uri.path}")); 36 | }else{ 37 | showToast(message: "Unknown Link"); 38 | return false; 39 | } 40 | default: 41 | return false; 42 | } 43 | return true; 44 | } -------------------------------------------------------------------------------- /lib/tools/background_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:pica_comic/comic_source/built_in/picacg.dart'; 4 | import 'package:pica_comic/foundation/app.dart'; 5 | import 'package:workmanager/workmanager.dart'; 6 | import '../base.dart'; 7 | import '../network/picacg_network/methods.dart'; 8 | import 'notification.dart'; 9 | 10 | @pragma('vm:entry-point') 11 | void onStart() { 12 | WidgetsFlutterBinding.ensureInitialized(); 13 | DartPluginRegistrant.ensureInitialized(); 14 | Workmanager().executeTask((taskName, inputData) async{ 15 | await App.init(); 16 | appdata = Appdata(); 17 | await appdata.readData(); 18 | var notifications = Notifications(); 19 | await notifications.init(); 20 | if (picacg.data['token'] != "") { 21 | var userInfo = await network.getProfile(false); 22 | if (userInfo.error) { 23 | return true; 24 | } 25 | if (userInfo.data.isPunched == false) { 26 | var res = await network.punchIn(); 27 | if (res) { 28 | notifications.sendUnimportantNotification("自动打卡", "成功签到"); 29 | return true; 30 | } 31 | } else { 32 | return true; 33 | } 34 | } 35 | return true; 36 | }); 37 | } 38 | 39 | void runBackgroundService() async{ 40 | await Workmanager().cancelAll(); 41 | await Workmanager().registerPeriodicTask( 42 | "Piacg PunchIn", 43 | "打卡", 44 | frequency: const Duration(minutes: 1440), 45 | constraints: Constraints(networkType: NetworkType.connected), 46 | ); 47 | } 48 | 49 | void cancelBackgroundService() async{ 50 | await Workmanager().cancelAll(); 51 | } -------------------------------------------------------------------------------- /lib/tools/block_screenshot.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | void blockScreenshot(){ 4 | const MethodChannel("com.kokoiro.xyz.pica_comic/screenshot").invokeMethod("blockScreenshot"); 5 | } -------------------------------------------------------------------------------- /lib/tools/cache_auto_clear.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:path_provider/path_provider.dart'; 4 | 5 | /// 清除长期未使用的缓存 6 | Future _autoClearCache(String cachePath) async{ 7 | var imageCachePath = Directory("$cachePath${Platform.pathSeparator}imageCache"); 8 | var networkCachePath = Directory("$cachePath${Platform.pathSeparator}cachedNetwork"); 9 | var time = DateTime.now(); 10 | if(imageCachePath.existsSync()){ 11 | for(var file in imageCachePath.listSync()){ 12 | if(file is File){ 13 | if(time.millisecondsSinceEpoch - file.lastAccessedSync().millisecondsSinceEpoch > 604800000){ 14 | file.deleteSync(); 15 | } 16 | } 17 | } 18 | } 19 | if(networkCachePath.existsSync()){ 20 | for(var file in networkCachePath.listSync()){ 21 | if(file is File){ 22 | if(time.millisecondsSinceEpoch - file.lastAccessedSync().millisecondsSinceEpoch > 604800000){ 23 | file.deleteSync(); 24 | } 25 | } 26 | } 27 | } 28 | } 29 | 30 | /// 清除长期未使用的缓存 31 | Future startClearCache() async{ 32 | var cachePath = await getTemporaryDirectory(); 33 | return await compute(_autoClearCache, cachePath.path); 34 | } -------------------------------------------------------------------------------- /lib/tools/debug.dart: -------------------------------------------------------------------------------- 1 | ///用于测试函数 2 | void debug() async { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /lib/tools/extensions.dart: -------------------------------------------------------------------------------- 1 | extension ListExtension on List{ 2 | /// Remove all blank value and return the list. 3 | List getNoBlankList(){ 4 | List newList = []; 5 | for(var value in this){ 6 | if(value.toString() != ""){ 7 | newList.add(value); 8 | } 9 | } 10 | return newList; 11 | } 12 | 13 | T? firstWhereOrNull(bool Function(T element) test){ 14 | for(var element in this){ 15 | if(test(element)){ 16 | return element; 17 | } 18 | } 19 | return null; 20 | } 21 | 22 | void addIfNotNull(T? value){ 23 | if(value != null){ 24 | add(value); 25 | } 26 | } 27 | } 28 | 29 | extension StringExtension on String{ 30 | ///Remove all value that would display blank on the screen. 31 | String get removeAllBlank => replaceAll("\n", "").replaceAll(" ", "").replaceAll("\t", ""); 32 | 33 | /// convert this to a one-element list. 34 | List toList() => [this]; 35 | 36 | String _nums(){ 37 | String res = ""; 38 | for(int i=0; i _nums(); 45 | 46 | String setValueAt(String value, int index){ 47 | return replaceRange(index, index+1, value); 48 | } 49 | 50 | String? subStringOrNull(int start, [int? end]){ 51 | if(start < 0 || (end != null && end > length)){ 52 | return null; 53 | } 54 | return substring(start, end); 55 | } 56 | 57 | String replaceLast(String from, String to) { 58 | if (isEmpty || from.isEmpty) { 59 | return this; 60 | } 61 | 62 | final lastIndex = lastIndexOf(from); 63 | if (lastIndex == -1) { 64 | return this; 65 | } 66 | 67 | final before = substring(0, lastIndex); 68 | final after = substring(lastIndex + from.length); 69 | return '$before$to$after'; 70 | } 71 | 72 | static bool hasMatch(String? value, String pattern) { 73 | return (value == null) ? false : RegExp(pattern).hasMatch(value); 74 | } 75 | 76 | bool _isURL(){ 77 | final regex = RegExp( 78 | r'^((http|https|ftp)://)?[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-|]*[\w@?^=%&/~+#-])?$', 79 | caseSensitive: false); 80 | return regex.hasMatch(this); 81 | } 82 | 83 | bool get isURL => _isURL(); 84 | 85 | bool get isNum => double.tryParse(this) != null; 86 | } 87 | 88 | extension MapExtension on Map>{ 89 | int _getTotalLength(){ 90 | int res = 0; 91 | for(var l in values.toList()){ 92 | res += l.length; 93 | } 94 | return res; 95 | } 96 | 97 | int get totalLength => _getTotalLength(); 98 | } 99 | 100 | class ListOrNull{ 101 | static List? from(Iterable? i){ 102 | return i == null ? null : List.from(i); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/tools/file_type.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:mime/mime.dart'; 4 | 5 | class FileType { 6 | final String ext; 7 | final String mime; 8 | 9 | const FileType(this.ext, this.mime); 10 | } 11 | 12 | FileType detectFileType(List data) { 13 | var mime = lookupMimeType('no-file', headerBytes: data); 14 | var ext = mime == null ? '' : extensionFromMime(mime); 15 | if(ext == 'jpe') { 16 | ext = 'jpg'; 17 | } 18 | return FileType(".$ext", mime ?? 'application/octet-stream'); 19 | } -------------------------------------------------------------------------------- /lib/tools/io_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:pica_comic/tools/extensions.dart'; 5 | 6 | extension FileSystemEntityExt on FileSystemEntity{ 7 | String get name { 8 | var path = this.path; 9 | if(path.endsWith('/') || path.endsWith('\\')){ 10 | path = path.substring(0, path.length-1); 11 | } 12 | 13 | int i = path.length - 1; 14 | 15 | while(i >= 0 && path[i] != '\\' && path[i] != '/'){ 16 | i--; 17 | } 18 | 19 | return path.substring(i+1); 20 | } 21 | 22 | Future deleteIgnoreError({bool recursive = false}) async{ 23 | try{ 24 | await delete(recursive: recursive); 25 | }catch(e){ 26 | // ignore 27 | } 28 | } 29 | } 30 | 31 | extension FileExtension on File{ 32 | /// Get file size information in MB 33 | double getMBSizeSync(){ 34 | var bytes = lengthSync(); 35 | return bytes/1024/1024; 36 | } 37 | 38 | String get extension => path.split('.').last; 39 | } 40 | 41 | extension DirectoryExtension on Directory{ 42 | /// Get directory size information in MB 43 | /// 44 | /// if directory is not exist, return 0; 45 | double getMBSizeSync(){ 46 | if(!existsSync()) return 0; 47 | double total = 0; 48 | for(var f in listSync(recursive: true)){ 49 | if(FileSystemEntity.typeSync(f.path)==FileSystemEntityType.file){ 50 | total += File(f.path).lengthSync()/1024/1024; 51 | } 52 | } 53 | return total; 54 | } 55 | 56 | Future get size async{ 57 | if(!existsSync()) return 0; 58 | int total = 0; 59 | for(var f in listSync(recursive: true)){ 60 | if(FileSystemEntity.typeSync(f.path)==FileSystemEntityType.file){ 61 | total += await File(f.path).length(); 62 | } 63 | } 64 | return total; 65 | } 66 | 67 | Directory renameX(String newName){ 68 | newName = sanitizeFileName(newName); 69 | return renameSync(path.replaceLast(name, newName)); 70 | } 71 | } 72 | 73 | String sanitizeFileName(String fileName) { 74 | const maxLength = 255; 75 | final invalidChars = RegExp(r'[<>:"/\\|?*]'); 76 | final sanitizedFileName = fileName.replaceAll(invalidChars, ' '); 77 | var trimmedFileName = sanitizedFileName.trim(); 78 | if (trimmedFileName.isEmpty) { 79 | throw Exception('Invalid File Name: Empty length.'); 80 | } 81 | while(true){ 82 | final bytes = utf8.encode(trimmedFileName); 83 | if (bytes.length > maxLength) { 84 | trimmedFileName = trimmedFileName.substring(0, trimmedFileName.length-1); 85 | }else{ 86 | break; 87 | } 88 | } 89 | return trimmedFileName; 90 | } 91 | 92 | String findValidDirectoryName(String path, String directory) { 93 | var name = sanitizeFileName(directory); 94 | var dir = Directory("$path/$name"); 95 | var i = 1; 96 | while(dir.existsSync()){ 97 | name = sanitizeFileName("$directory($i)"); 98 | dir = Directory("$path/$name"); 99 | i++; 100 | } 101 | return name; 102 | } -------------------------------------------------------------------------------- /lib/tools/js.dart: -------------------------------------------------------------------------------- 1 | ///解析JS代码, 返回定义的变量, Js代码必须合法 2 | Map getVariablesFromJsCode(String html){ 3 | Map variables = {}; 4 | 5 | RegExp variableRegex = RegExp(r"var\s+(\w+)\s*=\s*(.*?);"); 6 | var matches = variableRegex.allMatches(html); 7 | 8 | for (Match match in matches) { 9 | if(match.group(2)![0]=="\"" || match.group(2)![0]=="'"){ 10 | variables[match.group(1)!] = match.group(2)!.substring(1,match.group(2)!.length-1); 11 | }else { 12 | variables[match.group(1)!] = match.group(2)!; 13 | } 14 | } 15 | return variables; 16 | } -------------------------------------------------------------------------------- /lib/tools/keep_screen_on.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:pica_comic/foundation/app.dart'; 3 | 4 | 5 | void setKeepScreenOn() async{ 6 | if(!App.isMobile) return; 7 | var channel = const MethodChannel("com.kokoiro.xyz.pica_comic/keepScreenOn"); 8 | await channel.invokeMethod("set"); 9 | } 10 | 11 | void cancelKeepScreenOn() async{ 12 | if(!App.isMobile) return; 13 | var channel = const MethodChannel("com.kokoiro.xyz.pica_comic/keepScreenOn"); 14 | await channel.invokeMethod("cancel"); 15 | } -------------------------------------------------------------------------------- /lib/tools/key_down_event.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:pica_comic/foundation/app.dart'; 4 | 5 | class ListenVolumeController{ 6 | void Function() whenUp; 7 | void Function() whenDown; 8 | static const channel = EventChannel("com.kokoiro.xyz.pica_comic/volume"); 9 | StreamSubscription? _streamSubscription; 10 | 11 | ListenVolumeController(this.whenUp,this.whenDown); 12 | 13 | void listenVolumeChange(){ 14 | if(!App.isMobile) return; 15 | _streamSubscription = channel.receiveBroadcastStream().listen((event) { 16 | if(event == 1){ 17 | whenUp(); 18 | }else if(event==2){ 19 | whenDown(); 20 | } 21 | }); 22 | } 23 | 24 | void stop(){ 25 | if(!App.isMobile) return; 26 | _streamSubscription?.cancel(); 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /lib/tools/mouse_listener.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import '../foundation/app.dart'; 3 | 4 | void mouseSideButtonCallback(){ 5 | if(App.canPop){ 6 | App.globalBack(); 7 | } else if(App.mainNavigatorKey!.currentState!.canPop()){ 8 | App.mainNavigatorKey!.currentState!.pop(); 9 | } 10 | } 11 | 12 | ///监听鼠标侧键, 若为下键, 则调用返回 13 | void listenMouseSideButtonToBack() async{ 14 | if(!App.isWindows){ 15 | return; 16 | } 17 | const channel = EventChannel("kokoiro.xyz.pica_comic/mouse"); 18 | await for(var res in channel.receiveBroadcastStream()){ 19 | if(res == 0){ 20 | mouseSideButtonCallback(); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /lib/tools/save_image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:file_selector/file_selector.dart'; 3 | import 'package:image_gallery_saver/image_gallery_saver.dart'; 4 | import 'package:pica_comic/components/components.dart'; 5 | import 'package:pica_comic/foundation/log.dart'; 6 | import 'package:pica_comic/tools/file_type.dart'; 7 | import 'package:pica_comic/tools/io_tools.dart'; 8 | import 'package:pica_comic/tools/translations.dart'; 9 | import 'package:share_plus/share_plus.dart'; 10 | 11 | import '../foundation/app.dart'; 12 | 13 | ///保存图片 14 | void saveImage(File file) async { 15 | var data = await file.readAsBytes(); 16 | var type = detectFileType(data); 17 | var fileName = file.name; 18 | if(!fileName.contains('.')) { 19 | fileName += type.ext; 20 | } 21 | if (App.isAndroid || App.isIOS) { 22 | await ImageGallerySaver.saveImage( 23 | data, 24 | quality: 100, 25 | name: fileName, 26 | ); 27 | showToast(message: "已保存".tl); 28 | } else if (App.isDesktop) { 29 | try { 30 | final String? path = 31 | (await getSaveLocation(suggestedName: fileName))?.path; 32 | if (path != null) { 33 | final mimeType = type.mime; 34 | final XFile xFile = 35 | XFile.fromData(data, mimeType: mimeType, name: fileName); 36 | await xFile.saveTo(path); 37 | } 38 | } catch (e, s) { 39 | LogManager.addLog(LogLevel.error, "Save Image", "$e\n$s"); 40 | } 41 | } 42 | } 43 | 44 | Future persistentCurrentImage(File file) async { 45 | var newFile = File("${App.dataPath}/images/${file.path.split('/').last})}"); 46 | if (!(await newFile.exists())) { 47 | newFile.createSync(recursive: true); 48 | newFile.writeAsBytesSync(await file.readAsBytes()); 49 | } 50 | return newFile.path; 51 | } 52 | 53 | void shareImage(File file) { 54 | Share.shareXFiles([XFile(file.path)]); 55 | } 56 | -------------------------------------------------------------------------------- /lib/tools/time.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | import 'package:pica_comic/tools/translations.dart'; 3 | 4 | String timeToString(DateTime time){ 5 | var current = DateTime.now(); 6 | if(current.millisecondsSinceEpoch < time.millisecondsSinceEpoch){ 7 | return "Error"; 8 | } 9 | if(current.difference(time).inDays > 360){ 10 | return "@year 年前".tlParams({"year": (current.difference(time).inDays ~/ 360).toString()}); 11 | }else if(current.difference(time).inDays > 30){ 12 | return "@month 个月前".tlParams({"month": (current.difference(time).inDays ~/ 30).toString()}); 13 | }else if(current.difference(time).inHours > 24){ 14 | return "@day 天前".tlParams({"day": (current.difference(time).inDays).toString()}); 15 | }else if(current.difference(time).inMinutes > 60){ 16 | return "@hour 小时前".tlParams({"hour": (current.difference(time).inHours).toString()}); 17 | }else if(current.difference(time).inSeconds > 60){ 18 | return "@minute 分钟前".tlParams({"minute": (current.difference(time).inMinutes).toString()}); 19 | }else{ 20 | return "刚刚".tl; 21 | } 22 | } 23 | 24 | extension TimeExtension on DateTime{ 25 | Duration operator-(DateTime other){ 26 | return Duration(microseconds: microsecondsSinceEpoch - other.microsecondsSinceEpoch); 27 | } 28 | static DateTime parseEhTime(String dateString){ 29 | final format = DateFormat('d MMMM yyyy, HH:mm', 'en_US'); 30 | final dateTime = format.parse(dateString); 31 | return dateTime; 32 | } 33 | 34 | String get toCompareString => timeToString(this); 35 | } -------------------------------------------------------------------------------- /lib/tools/translations.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/services.dart'; 4 | import 'package:pica_comic/foundation/app.dart'; 5 | 6 | extension AppTranslation on String { 7 | String _translate() { 8 | var locale = App.locale; 9 | var key = "${locale.languageCode}_${locale.countryCode}"; 10 | if (locale.languageCode == "en") { 11 | key = "en_US"; 12 | } 13 | return (translations[key]?[this]) ?? this; 14 | } 15 | 16 | String get tl => _translate(); 17 | 18 | String get tlEN => translations["en_US"]![this] ?? this; 19 | 20 | String tlParams(Map values) { 21 | var res = _translate(); 22 | for (var entry in values.entries) { 23 | res = res.replaceFirst("@${entry.key}", entry.value); 24 | } 25 | return res; 26 | } 27 | 28 | static late final Map> translations; 29 | 30 | static Future init() async{ 31 | var data = await rootBundle.load("assets/translation.json"); 32 | var json = jsonDecode(utf8.decode(data.buffer.asUint8List())); 33 | translations = { for (var e in json.entries) e.key : Map.from(e.value) }; 34 | } 35 | } 36 | 37 | extension ListTranslation on List { 38 | List _translate() { 39 | return List.generate(length, (index) => this[index].tl); 40 | } 41 | 42 | List get tl => _translate(); 43 | } 44 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | flutter/generated_plugin_registrant.cc 3 | flutter/generated_plugin_registrant.h -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | void fl_register_plugins(FlPluginRegistry* registry) { 19 | g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = 20 | fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); 21 | desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); 22 | g_autoptr(FlPluginRegistrar) dynamic_color_registrar = 23 | fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); 24 | dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); 25 | g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = 26 | fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); 27 | file_selector_plugin_register_with_registrar(file_selector_linux_registrar); 28 | g_autoptr(FlPluginRegistrar) flutter_qjs_registrar = 29 | fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterQjsPlugin"); 30 | flutter_qjs_plugin_register_with_registrar(flutter_qjs_registrar); 31 | g_autoptr(FlPluginRegistrar) screen_retriever_registrar = 32 | fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); 33 | screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); 34 | g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = 35 | fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); 36 | sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); 37 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 38 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 39 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 40 | g_autoptr(FlPluginRegistrar) window_manager_registrar = 41 | fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); 42 | window_manager_plugin_register_with_registrar(window_manager_registrar); 43 | } 44 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | desktop_webview_window 7 | dynamic_color 8 | file_selector_linux 9 | flutter_qjs 10 | screen_retriever 11 | sqlite3_flutter_libs 12 | url_launcher_linux 13 | window_manager 14 | ) 15 | 16 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 17 | zip_flutter 18 | ) 19 | 20 | set(PLUGIN_BUNDLED_LIBRARIES) 21 | 22 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 23 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 24 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 26 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 27 | endforeach(plugin) 28 | 29 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 30 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 31 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 32 | endforeach(ffi_plugin) 33 | -------------------------------------------------------------------------------- /linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Flutter/GeneratedPluginRegistrant.swift 4 | **/Pods/ 5 | 6 | # Xcode-related 7 | **/dgph 8 | **/xcuserdata/ 9 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import app_links 9 | import desktop_webview_window 10 | import dynamic_color 11 | import file_selector_macos 12 | import flutter_inappwebview_macos 13 | import flutter_local_notifications 14 | import local_auth_darwin 15 | import path_provider_foundation 16 | import screen_retriever 17 | import share_plus 18 | import shared_preferences_foundation 19 | import sqlite3_flutter_libs 20 | import url_launcher_macos 21 | import window_manager 22 | 23 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 24 | AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) 25 | DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) 26 | DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) 27 | FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) 28 | InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) 29 | FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) 30 | FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) 31 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 32 | ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) 33 | SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) 34 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 35 | Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) 36 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 37 | WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) 38 | } 39 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_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_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = pica_comic 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.kokoiro.xyz.picaComic 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.kokoiro.xyz. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import window_manager 4 | 5 | class MainFlutterWindow: NSWindow { 6 | override func awakeFromNib() { 7 | let flutterViewController = FlutterViewController.init() 8 | let windowFrame = self.frame 9 | self.contentViewController = flutterViewController 10 | self.setFrame(windowFrame, display: true) 11 | 12 | RegisterGeneratedPlugins(registry: flutterViewController) 13 | 14 | super.awakeFromNib() 15 | } 16 | 17 | override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { 18 | super.order(place, relativeTo: otherWin) 19 | hiddenWindowAtLaunch() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | com.apple.security.files.user-selected.read-write 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/screenshots/10.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/screenshots/3.png -------------------------------------------------------------------------------- /screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/screenshots/4.png -------------------------------------------------------------------------------- /screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/screenshots/5.png -------------------------------------------------------------------------------- /screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/screenshots/6.png -------------------------------------------------------------------------------- /screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/screenshots/7.png -------------------------------------------------------------------------------- /screenshots/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/screenshots/9.png -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | void main() { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /utils/check_translation.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | Map? translation; 5 | 6 | var keys = []; 7 | 8 | void main() async{ 9 | var file = File("assets/translation.json"); 10 | var data = await file.readAsString(); 11 | translation = jsonDecode(data); 12 | find(Directory('lib')); 13 | file = File("assets/translation.json"); 14 | translation!.forEach((key, value) { 15 | var shouldRemove = []; 16 | for (var element in (value as Map).keys) { 17 | if(!keys.contains(element)){ 18 | shouldRemove.add(element); 19 | } 20 | } 21 | for (var element in shouldRemove) { 22 | value.remove(element); 23 | } 24 | }); 25 | file.writeAsString(const JsonEncoder.withIndent(" ").convert(translation)); 26 | } 27 | 28 | String realText(String text){ 29 | text = text.replaceAll(".tl", ""); 30 | var char = text[text.length-1]; 31 | int index = text.length-2; 32 | while(true){ 33 | if(text[index] == char){ 34 | if(index > 0 && text[index-1] == '\\'){ 35 | index--; 36 | continue; 37 | } 38 | break; 39 | } 40 | index--; 41 | } 42 | return text.substring(index+1, text.length-1); 43 | } 44 | 45 | void find(Directory directory){ 46 | for(var entity in directory.listSync()){ 47 | if(entity is File){ 48 | var code = entity.readAsStringSync(); 49 | for(var match in RegExp(r'".*?"\.tl').allMatches(code)){ 50 | var text = match.group(0); 51 | text = realText(text!); 52 | if(text.isEmpty) continue; 53 | keys.add(text); 54 | if(translation!["zh_TW"][text] == null){ 55 | translation!["zh_TW"][text] = ""; 56 | } 57 | if(translation!["en_US"][text] == null){ 58 | translation!["en_US"][text] = ""; 59 | } 60 | } 61 | for(var match in RegExp(r"'.*?'\.tl").allMatches(code)){ 62 | var text = match.group(0); 63 | text = realText(text!); 64 | if(text.isEmpty) continue; 65 | keys.add(text); 66 | if(translation!["zh_TW"][text] == null){ 67 | translation!["zh_TW"][text] = ""; 68 | } 69 | if(translation!["en_US"][text] == null){ 70 | translation!["en_US"][text] = ""; 71 | } 72 | } 73 | } else if (entity is Directory){ 74 | find(entity); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /utils/tags_translation.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | /// Download tags from https://github.com/EhTagTranslation/DatabaseReleases.git 5 | void main() async{ 6 | await Process.run("git", ["clone", "https://github.com/EhTagTranslation/DatabaseReleases.git"]); 7 | var file = File("DatabaseReleases/db.text.json"); 8 | var db = const JsonDecoder().convert(file.readAsStringSync()); 9 | Map> res = {}; 10 | for(var category in db["data"]){ 11 | Map items = {}; 12 | for(var entry in (category["data"] as Map).entries){ 13 | items[entry.key] = entry.value["name"]; 14 | } 15 | res[category["namespace"]] = items; 16 | } 17 | var output = const JsonEncoder().convert(res); 18 | File("assets/tags.json").writeAsStringSync(output); 19 | Directory("DatabaseReleases").deleteSync(recursive: true); 20 | } -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | pica_comic 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 |
44 | 58 | 59 |
60 | 68 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /web/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/web/loading.gif -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pica_comic", 3 | "short_name": "pica_comic", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | flutter/generated_plugin_registrant.h 3 | flutter/generated_plugin_registrant.cc 4 | 5 | # Visual Studio user-specific files. 6 | *.suo 7 | *.user 8 | *.userosscache 9 | *.sln.docstates 10 | 11 | # Visual Studio build-related files. 12 | x64/ 13 | x86/ 14 | 15 | # Visual Studio cache files 16 | # files ending in .cache can be ignored 17 | *.[Cc]ache 18 | # but keep track of directories ending in .cache 19 | !*.[Cc]ache/ 20 | -------------------------------------------------------------------------------- /windows/build_windows.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | 4 | fontUse = ''' 5 | fonts: 6 | - family: font 7 | fonts: 8 | - asset: fonts/NotoSansSC-Regular.ttf 9 | ''' 10 | 11 | file = open('pubspec.yaml', 'r') 12 | content = file.read() 13 | file.close() 14 | file = open('pubspec.yaml', 'a') 15 | file.write(fontUse) 16 | file.close() 17 | 18 | subprocess.run(["flutter", "build", "windows"], shell=True) 19 | 20 | file = open('pubspec.yaml', 'w') 21 | file.write(content) 22 | 23 | if os.path.exists("build/app-windows.zip"): 24 | os.remove("build/app-windows.zip") 25 | 26 | version = str.split(str.split(content, 'version: ')[1], '+')[0] 27 | 28 | # 压缩build/windows/x64/runner/Release, 生成app-windows.zip, 使用tar命令 29 | subprocess.run(["tar", "-a", "-c", "-f", f"build/windows/PicaComic-{version}-windows.zip", "-C", "build/windows/x64/runner/Release", "."] 30 | , shell=True) 31 | 32 | issContent = "" 33 | file = open('windows/build.iss', 'r') 34 | issContent = file.read() 35 | newContent = issContent 36 | newContent = newContent.replace("{{version}}", version) 37 | newContent = newContent.replace("{{root_path}}", os.getcwd()) 38 | file.close() 39 | file = open('windows/build.iss', 'w') 40 | file.write(newContent) 41 | file.close() 42 | 43 | subprocess.run(["iscc", "windows/build.iss"], shell=True) 44 | 45 | with open('windows/build.iss', 'w') as file: 46 | file.write(issContent) -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | void RegisterPlugins(flutter::PluginRegistry* registry) { 22 | AppLinksPluginCApiRegisterWithRegistrar( 23 | registry->GetRegistrarForPlugin("AppLinksPluginCApi")); 24 | DesktopWebviewWindowPluginRegisterWithRegistrar( 25 | registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); 26 | DynamicColorPluginCApiRegisterWithRegistrar( 27 | registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); 28 | FileSelectorWindowsRegisterWithRegistrar( 29 | registry->GetRegistrarForPlugin("FileSelectorWindows")); 30 | FlutterQjsPluginRegisterWithRegistrar( 31 | registry->GetRegistrarForPlugin("FlutterQjsPlugin")); 32 | LocalAuthPluginRegisterWithRegistrar( 33 | registry->GetRegistrarForPlugin("LocalAuthPlugin")); 34 | ScreenRetrieverPluginRegisterWithRegistrar( 35 | registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); 36 | SharePlusWindowsPluginCApiRegisterWithRegistrar( 37 | registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); 38 | Sqlite3FlutterLibsPluginRegisterWithRegistrar( 39 | registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); 40 | UrlLauncherWindowsRegisterWithRegistrar( 41 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 42 | WindowManagerPluginRegisterWithRegistrar( 43 | registry->GetRegistrarForPlugin("WindowManagerPlugin")); 44 | } 45 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | app_links 7 | desktop_webview_window 8 | dynamic_color 9 | file_selector_windows 10 | flutter_qjs 11 | local_auth_windows 12 | screen_retriever 13 | share_plus 14 | sqlite3_flutter_libs 15 | url_launcher_windows 16 | window_manager 17 | ) 18 | 19 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 20 | zip_flutter 21 | ) 22 | 23 | set(PLUGIN_BUNDLED_LIBRARIES) 24 | 25 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 26 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 27 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 28 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 29 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 30 | endforeach(plugin) 31 | 32 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 33 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 34 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 35 | endforeach(ffi_plugin) 36 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | 32 | RECT windowRect; 33 | }; 34 | 35 | #endif // RUNNER_FLUTTER_WINDOW_H_ 36 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "flutter_window.h" 5 | #include "utils.h" 6 | 7 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 8 | _In_ wchar_t *command_line, _In_ int show_command) { 9 | // Attach to console when present (e.g., 'flutter run') or create a 10 | // new console when running with a debugger. 11 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 12 | CreateAndAttachConsole(); 13 | } 14 | 15 | // Initialize COM, so that it is available for use in the library and/or 16 | // plugins. 17 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 18 | 19 | flutter::DartProject project(L"data"); 20 | 21 | std::vector command_line_arguments = 22 | GetCommandLineArguments(); 23 | 24 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 25 | 26 | FlutterWindow window(project); 27 | Win32Window::Point origin(10, 10); 28 | Win32Window::Size size(1280, 800); 29 | if (!window.Create(L"Pica Comic", origin, size)) { 30 | return EXIT_FAILURE; 31 | } 32 | window.SetQuitOnClose(true); 33 | 34 | ::MSG msg; 35 | while (::GetMessage(&msg, nullptr, 0, 0)) { 36 | ::TranslateMessage(&msg); 37 | ::DispatchMessage(&msg); 38 | } 39 | 40 | ::CoUninitialize(); 41 | return EXIT_SUCCESS; 42 | } 43 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s200801005/PicaComic/0d56923d93d6450a8d45a8c360e3c167f476ee38/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | std::string utf8_string; 52 | if (target_length == 0 || target_length > utf8_string.max_size()) { 53 | return utf8_string; 54 | } 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | --------------------------------------------------------------------------------