├── .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 | [](https://flutter.dev/)
4 | [](https://github.com/wgh136/PicaComic/blob/master/LICENSE)
5 | [](https://github.com/wgh136/PicaComic/releases)
6 | [](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 | [](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 | [](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