├── .gradle ├── 6.1.1 │ ├── gc.properties │ ├── fileChanges │ │ └── last-build.bin │ └── fileHashes │ │ └── fileHashes.lock ├── vcs-1 │ └── gc.properties └── checksums │ └── checksums.lock ├── _config.yml ├── android ├── settings_aar.gradle ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ └── kotlin │ │ │ │ └── top │ │ │ │ └── hanerx │ │ │ │ └── flutterdmzj │ │ │ │ └── MainActivity.kt │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── google-services.json │ └── build.gradle ├── .gitignore ├── proguard-rules.pro ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── build.gradle ├── assets ├── guide │ ├── chapter0 │ │ ├── example0.json │ │ ├── example1.json │ │ └── example2.json │ └── example.json └── js │ └── lzString.js ├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── .gitignore ├── GoogleService-Info.plist └── Podfile ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ └── Icon-512.png ├── manifest.json └── index.html ├── doc ├── image-20210419184914844.png ├── image-20210419193403782.png ├── image-20210419193440494.png └── image-20210419193540196.png ├── lib ├── protobuf │ ├── comic.pbenum.dart │ ├── novel_chapter.pbenum.dart │ ├── comic.pbserver.dart │ ├── novel_chapter.pbserver.dart │ ├── novel_chapter.proto │ └── comic.proto ├── model │ ├── baseModel.dart │ ├── mag_model │ │ ├── OutputMangaModel.dart │ │ └── MangaComicGroupModel.dart │ ├── homepageModel.dart │ ├── novel_source │ │ └── baseNovelSourceModel.dart │ ├── localHistoryModel.dart │ ├── RouterModel.dart │ ├── server_controller │ │ ├── ComicListModel.dart │ │ ├── UserListModel.dart │ │ ├── ServerListModel.dart │ │ ├── NewComicGroupModel.dart │ │ ├── NewComicChapterModel.dart │ │ └── ServerMainPageModel.dart │ ├── download.dart │ ├── comicSearchModel.dart │ ├── comicLatestUpdateModel.dart │ ├── sourceSearchProvider.dart │ ├── comicCommentModel.dart │ ├── cloudHistoryModel.dart │ ├── subjectListModel.dart │ ├── comicCategoryModel.dart │ ├── comicAuthorModel.dart │ ├── subjectDetailModel.dart │ ├── comicFavoriteModel.dart │ ├── trackerModel.dart │ ├── novelFavoriteModel.dart │ ├── comicCategoryDetailModel.dart │ ├── databaseDetailModel.dart │ └── downloadChapters.dart ├── http │ ├── GithubRequestHandler.dart │ ├── KuKuRequestHandler.dart │ └── ManHuaGuiRequestHandler.dart ├── utils │ ├── log_output.dart │ ├── soup.dart │ └── HttpProxyAdapter.dart ├── view │ ├── comic_source │ │ └── comic_source_page.dart │ ├── history_page.dart │ ├── settings │ │ ├── user_setting_page.dart │ │ └── source_setting_page.dart │ ├── download_comic_page.dart │ ├── favorite │ │ ├── tracker_favorite_page.dart │ │ ├── novel_favorite_page.dart │ │ └── comic_favorite_page.dart │ ├── novel_pages │ │ ├── novel_main_page.dart │ │ └── novel_category_page.dart │ ├── download_page.dart │ ├── mag_maker │ │ ├── mag_example_page.dart │ │ ├── new_mag_page.dart │ │ └── output_mag_page.dart │ ├── server_controllers │ │ ├── server_sellect_page.dart │ │ └── user_list_page.dart │ ├── home_page.dart │ ├── author_page.dart │ ├── login_page.dart │ ├── comment_page.dart │ ├── favorite_page.dart │ └── comic_pages │ │ └── subject_list_page.dart ├── component │ ├── ViewPointChip.dart │ ├── LoadingCube.dart │ ├── comic_viewer │ │ ├── Common.dart │ │ ├── EndPage.dart │ │ └── Tips.dart │ ├── comic │ │ ├── BaseListTile.dart │ │ └── SubjectListTile.dart │ ├── DataBaseTable.dart │ ├── search │ │ ├── SearchButton.dart │ │ └── SearchTab.dart │ ├── CustomDrawer.dart │ ├── TypeTags.dart │ ├── DataBaseDefineTile.dart │ ├── Authors.dart │ ├── DownloadComicListTile.dart │ ├── ComicSourceCard.dart │ ├── CategoryCard.dart │ ├── history_tab │ │ └── LocalHistoryTab.dart │ └── AuthorCard.dart ├── database │ ├── tracker.dart │ ├── cookieDatabaseProvider.dart │ └── historyDatabaseProvider.dart └── generated │ └── intl │ └── messages_all.dart ├── .metadata ├── .github ├── ISSUE_TEMPLATE │ ├── ----.md │ └── bug--.md └── workflows │ └── main.yml ├── .gitignore └── test └── widget_test.dart /.gradle/6.1.1/gc.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gradle/vcs-1/gc.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gradle/6.1.1/fileChanges/last-build.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /assets/guide/chapter0/example0.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": "hello world!" 3 | } -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/web/icons/Icon-512.png -------------------------------------------------------------------------------- /assets/guide/chapter0/example1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test_manga", 3 | "title": "简单学会如何制作.manga文件" 4 | } -------------------------------------------------------------------------------- /.gradle/checksums/checksums.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/.gradle/checksums/checksums.lock -------------------------------------------------------------------------------- /doc/image-20210419184914844.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/doc/image-20210419184914844.png -------------------------------------------------------------------------------- /doc/image-20210419193403782.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/doc/image-20210419193403782.png -------------------------------------------------------------------------------- /doc/image-20210419193440494.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/doc/image-20210419193440494.png -------------------------------------------------------------------------------- /doc/image-20210419193540196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/doc/image-20210419193540196.png -------------------------------------------------------------------------------- /.gradle/6.1.1/fileHashes/fileHashes.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/.gradle/6.1.1/fileHashes/fileHashes.lock -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanerx/dcomic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | /test.properties 9 | /test.properties 10 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/guide/chapter0/example2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test_manga", 3 | "title": "简单学会如何制作.manga文件", 4 | "alias": [ 5 | ".manga从入门到精通", 6 | ".manga Program design" 7 | ], 8 | "authors": [ 9 | "hanerx" 10 | ] 11 | } -------------------------------------------------------------------------------- /android/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | #Flutter Wrapper 2 | -keep class io.flutter.app.** { *; } 3 | -keep class io.flutter.plugin.** { *; } 4 | -keep class io.flutter.util.** { *; } 5 | -keep class io.flutter.view.** { *; } 6 | -keep class io.flutter.** { *; } 7 | -keep class io.flutter.plugins.** { *; } -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.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.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/protobuf/comic.pbenum.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: comic.proto 4 | // 5 | // @dart = 2.12 6 | // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields 7 | 8 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 0b8abb4724aa590dd0f429683339b1e045a1594d 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /lib/protobuf/novel_chapter.pbenum.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: novel_chapter.proto 4 | // 5 | // @dart = 2.12 6 | // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields 7 | 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/----.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能需求 3 | about: 新功能的需求工单 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **需求简述** 11 | 需求的大概描述 12 | 13 | **需求主要目的与对应问题** 14 | 阐述需求主要是为了解决什么问题 15 | 16 | **需求主要功能清单** 17 | [ ] 功能1 18 | [ ] 功能2 19 | [x] 功能3 20 | 21 | **预期实现完成样式** 22 | *可以贴个图来说明到底要啥样子* 23 | 24 | **附加信息** 25 | *这里加上如参考的项目等附加信息* 26 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /lib/model/baseModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:dcomic/utils/log_output.dart'; 3 | import 'package:logger/logger.dart'; 4 | 5 | class BaseModel extends ChangeNotifier { 6 | Logger logger; 7 | 8 | BaseModel() { 9 | logger = Logger( 10 | filter: ReleaseFilter(), 11 | printer: PrettyPrinter(), 12 | output: ConsoleLogOutput()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /lib/protobuf/comic.pbserver.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: comic.proto 4 | // 5 | // @dart = 2.12 6 | // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package 7 | 8 | export 'comic.pb.dart'; 9 | 10 | -------------------------------------------------------------------------------- /lib/protobuf/novel_chapter.pbserver.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: novel_chapter.proto 4 | // 5 | // @dart = 2.12 6 | // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package 7 | 8 | export 'novel_chapter.pb.dart'; 9 | 10 | -------------------------------------------------------------------------------- /lib/http/GithubRequestHandler.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/http/UniversalRequestModel.dart'; 2 | import 'package:dio/dio.dart'; 3 | 4 | class GithubRequestHandler extends SingleDomainRequestHandler { 5 | GithubRequestHandler() : super('https://api.github.com'); 6 | 7 | Future getReleases() { 8 | return dio.get('/repos/hanerx/dcomic/releases'); 9 | } 10 | 11 | Future getLatestRelease() { 12 | return dio.get('/repos/hanerx/flutter_dmzj/releases/latest'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /lib/utils/log_output.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | import 'package:logger_flutter/logger_flutter.dart'; 3 | 4 | class ConsoleLogOutput extends ConsoleOutput { 5 | @override 6 | void output(OutputEvent event) { 7 | super.output(event); 8 | LogConsole.add(event); 9 | } 10 | } 11 | 12 | class ReleaseFilter extends LogFilter { 13 | @override 14 | bool shouldLog(LogEvent event) { 15 | var shouldLog = false; 16 | if (event.level.index >= level.index) { 17 | shouldLog = true; 18 | } 19 | return shouldLog; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dcomic", 3 | "short_name": "dcomic", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /lib/model/mag_model/OutputMangaModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/database/localMangaDatabaseProvider.dart'; 2 | import 'package:dcomic/model/mag_model/baseMangaModel.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | 5 | import '../baseModel.dart'; 6 | 7 | class OutputMangaModel extends BaseModel{ 8 | List _data=[]; 9 | 10 | OutputMangaModel(); 11 | 12 | Future init()async{ 13 | try{ 14 | _data=await LocalMangaDatabaseProvider().getAll(); 15 | notifyListeners(); 16 | }catch(e,s){ 17 | FirebaseCrashlytics.instance 18 | .recordError(e, s, reason: 'localComicListInitFailed'); 19 | } 20 | } 21 | 22 | List get data=>_data; 23 | } -------------------------------------------------------------------------------- /lib/utils/soup.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:html/parser.dart' show parse; 3 | import 'package:html/dom.dart'; 4 | 5 | class BeautifulSoup { 6 | String htmlDoc; 7 | Document doc; 8 | BeautifulSoup(this.htmlDoc){ 9 | doc = parse(htmlDoc); 10 | } 11 | 12 | Element find({String id}) { 13 | return doc.querySelector(id); 14 | } 15 | 16 | Element call(String selector) => doc.querySelector(selector); 17 | 18 | List findAll(String selector){ 19 | return doc.querySelectorAll(selector); 20 | } 21 | 22 | String getText(){ 23 | return doc.querySelector("html").text; 24 | } 25 | 26 | String print(){ 27 | return doc.querySelector("html").outerHtml; 28 | } 29 | 30 | String attr(Element e,String attribute) => e.attributes[attribute]; 31 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Exceptions to above rules. 37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug--.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug报告 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Bug现象简述** 11 | A clear and concise description of what the bug is. 12 | 13 | **复现方法** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **预期动作** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **截图** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Smartphone (please complete the following information):** 27 | - Device: [e.g. iPhone6] 28 | - OS: [e.g. iOS8.1] 29 | - Browser [e.g. stock browser, safari] 30 | - Version [e.g. 22] 31 | 32 | **附加信息** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /lib/model/homepageModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/baseModel.dart'; 2 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | 5 | class HomepageModel extends BaseModel { 6 | final BaseSourceModel model; 7 | List _data = []; 8 | dynamic error; 9 | 10 | HomepageModel(this.model); 11 | 12 | Future init() async { 13 | try { 14 | _data = await model.homePageHandler.getHomePage(); 15 | error = null; 16 | } catch (e, s) { 17 | FirebaseCrashlytics.instance 18 | .recordError(e, s, reason: 'getHomepageFailed'); 19 | error = '未知错误:$e'; 20 | } 21 | notifyListeners(); 22 | } 23 | 24 | List get data => _data; 25 | 26 | int get length => _data.length; 27 | } 28 | -------------------------------------------------------------------------------- /lib/model/novel_source/baseNovelSourceModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | import '../baseModel.dart'; 4 | 5 | abstract class BaseNovelSourceModel extends BaseModel { 6 | Future> search(String keyword, {int page: 0}); 7 | 8 | Widget getSettingWidget(context); 9 | 10 | } 11 | 12 | abstract class NovelDetail extends BaseModel{ 13 | 14 | } 15 | 16 | abstract class Novel extends BaseModel{ 17 | 18 | } 19 | 20 | abstract class NovelSearchResult { 21 | String get title; 22 | 23 | String get novelId; 24 | 25 | String get cover; 26 | 27 | String get author; 28 | 29 | String get tag; 30 | 31 | String get latestChapter; 32 | 33 | @override 34 | String toString() { 35 | return 'SearchResult{title: $title, novelId: $novelId, cover: $cover, author: $author, tag: $tag}'; 36 | } 37 | } -------------------------------------------------------------------------------- /lib/model/localHistoryModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/baseModel.dart'; 2 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 3 | import 'package:dcomic/model/comic_source/sourceProvider.dart'; 4 | 5 | class LocalHistoryModel extends BaseModel { 6 | final SourceProvider provider; 7 | 8 | LocalHistoryModel(this.provider); 9 | 10 | List _list = []; 11 | 12 | Future getHistories() async { 13 | for (var item in provider.activeSources) { 14 | _list += await item.getLocalHistoryComics(); 15 | } 16 | _list.sort((left,right)=>right.timestamp.compareTo(left.timestamp)); 17 | notifyListeners(); 18 | } 19 | 20 | Future refresh() async { 21 | _list.clear(); 22 | await getHistories(); 23 | notifyListeners(); 24 | } 25 | 26 | List get list => _list; 27 | } 28 | -------------------------------------------------------------------------------- /lib/utils/HttpProxyAdapter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dio/adapter.dart'; 4 | 5 | class HttpProxyAdapter extends DefaultHttpClientAdapter { 6 | final String ipAddr; 7 | final int port; 8 | 9 | HttpProxyAdapter({this.ipAddr = 'localhost', this.port = 8888}) { 10 | onHttpClientCreate = (client) { 11 | String proxy = '$ipAddr:$port'; 12 | client.findProxy = (url) { 13 | return 'PROXY $proxy'; 14 | }; 15 | 16 | client.badCertificateCallback = 17 | (X509Certificate cert, String host, int port) => true; 18 | }; 19 | } 20 | } 21 | 22 | class BadCertificateAdapter extends DefaultHttpClientAdapter { 23 | BadCertificateAdapter() { 24 | onHttpClientCreate = (client) { 25 | client.badCertificateCallback = 26 | (X509Certificate cert, String host, int port) => true; 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/model/RouterModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/baseModel.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | 4 | enum RouterLayout{ 5 | RootRoute, 6 | HomeRoute, 7 | ViewerRoute 8 | } 9 | 10 | class RouterModel extends BaseModel{ 11 | final GlobalKey homeNavigator=GlobalKey(); 12 | final GlobalKey viewerNavigator=GlobalKey(); 13 | final BuildContext context; 14 | 15 | RouterModel(this.context); 16 | 17 | NavigatorState getNavigatorState(RouterLayout layout){ 18 | switch(layout){ 19 | case RouterLayout.RootRoute: 20 | return Navigator.of(context); 21 | case RouterLayout.HomeRoute: 22 | return homeNavigator.currentState; 23 | case RouterLayout.ViewerRoute: 24 | return viewerNavigator.currentState; 25 | } 26 | return Navigator.of(context); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | 10.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/model/server_controller/ComicListModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/baseModel.dart'; 2 | import 'package:dcomic/model/comic_source/IPFSSourceProivder.dart'; 3 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 4 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | 7 | class ComicListModel extends BaseModel { 8 | final IPFSSourceModel node; 9 | 10 | List _data = []; 11 | 12 | TextEditingController controller=TextEditingController(); 13 | 14 | ComicListModel(this.node); 15 | 16 | Future init() async { 17 | try { 18 | _data = await node.search(controller.text); 19 | notifyListeners(); 20 | } catch (e, s) { 21 | FirebaseCrashlytics.instance.recordError(e, s, 22 | reason: 'comicListLoadingFailed: ${node.address}'); 23 | } 24 | } 25 | 26 | List get data => _data; 27 | } 28 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.4' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.google.gms:google-services:4.3.4' 12 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1' 13 | classpath 'com.google.firebase:perf-plugin:1.3.2' 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | maven { url 'https://jitpack.io' } 22 | } 23 | } 24 | 25 | rootProject.buildDir = '../build' 26 | subprojects { 27 | project.buildDir = "${rootProject.buildDir}/${project.name}" 28 | } 29 | subprojects { 30 | project.evaluationDependsOn(':app') 31 | } 32 | 33 | task clean(type: Delete) { 34 | delete rootProject.buildDir 35 | } 36 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | import Firebase 4 | import flutter_downloader 5 | 6 | @UIApplicationMain 7 | @objc class AppDelegate: FlutterAppDelegate { 8 | override func application( 9 | _ application: UIApplication, 10 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 11 | ) -> Bool { 12 | GeneratedPluginRegistrant.register(with: self) 13 | FlutterDownloaderPlugin.setPluginRegistrantCallback(registerPlugins) 14 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 15 | } 16 | 17 | override func applicationDidFinishLaunching(_ application: UIApplication) { 18 | FirebaseApp.configure() 19 | } 20 | 21 | } 22 | 23 | private func registerPlugins(registry: FlutterPluginRegistry) { 24 | if (!registry.hasPlugin("FlutterDownloaderPlugin")) { 25 | FlutterDownloaderPlugin.register(with: registry.registrar(forPlugin: "FlutterDownloaderPlugin")!) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/model/download.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:dcomic/component/DownloadComicListTile.dart'; 3 | import 'package:dcomic/database/downloader.dart'; 4 | import 'package:dcomic/model/baseModel.dart'; 5 | 6 | class DownloadModel extends BaseModel { 7 | List data; 8 | 9 | 10 | DownloadModel() { 11 | logger.i('action: init'); 12 | getComic(); 13 | } 14 | 15 | Future getComic() async { 16 | DownloadProvider downloadProvider = DownloadProvider(); 17 | data = await downloadProvider.getAllComic(); 18 | logger.i('action: getComic, comicList: $data'); 19 | notifyListeners(); 20 | } 21 | 22 | Widget buildComicListTile(context, index) { 23 | if (index < 0 || index >= data.length) { 24 | return null; 25 | } 26 | return DownloadComicListTile( 27 | title: data[index].title, 28 | comicId: data[index].comicId, 29 | cover: data[index].cover, 30 | ); 31 | } 32 | 33 | int get length => data == null ? 0 : data.length; 34 | } 35 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/view/comic_source/comic_source_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 4 | import 'package:dcomic/generated/l10n.dart'; 5 | import 'package:dcomic/model/comic_source/sourceProvider.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class ComicSourcePage extends StatefulWidget { 9 | @override 10 | State createState() { 11 | // TODO: implement createState 12 | return _ComicSourcePage(); 13 | } 14 | } 15 | 16 | class _ComicSourcePage extends State { 17 | @override 18 | Widget build(BuildContext context) { 19 | // TODO: implement build 20 | return Scaffold( 21 | appBar: AppBar( 22 | title: Text(S.of(context).SettingPageSourcePageSourceTitle), 23 | ), 24 | body: EasyRefresh( 25 | child: ListView.builder( 26 | itemBuilder: Provider.of(context).getSourceConfigWidget, 27 | itemCount: Provider.of(context).sources.length, 28 | )), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/view/history_page.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:dcomic/component/history_tab/CloudHistoryTab.dart'; 5 | import 'package:dcomic/component/history_tab/LocalHistoryTab.dart'; 6 | 7 | class HistoryPage extends StatefulWidget { 8 | @override 9 | State createState() { 10 | // TODO: implement createState 11 | return _HistoryPage(); 12 | } 13 | } 14 | 15 | class _HistoryPage extends State { 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | // TODO: implement build 20 | return DefaultTabController( 21 | length: 2, 22 | child: Scaffold( 23 | appBar: AppBar( 24 | title: Text('历史记录'), 25 | bottom: TabBar( 26 | tabs: [ 27 | Tab(text:'云端记录'), 28 | Tab(text: '本地记录',) 29 | ], 30 | ), 31 | ), 32 | body:TabBarView( 33 | children: [ 34 | CloudHistoryTab(), 35 | LocalHistoryTab() 36 | ], 37 | ), 38 | ), 39 | ); 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "522563222655", 4 | "project_id": "dcomic-93209", 5 | "storage_bucket": "dcomic-93209.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:522563222655:android:e14780753062c4cf43d34a", 11 | "android_client_info": { 12 | "package_name": "top.hanerx.flutterdmzj" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "522563222655-vtsp4k0o2ef9gua3rgf01jl4u4hm791s.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyB8rCCOBN1x2sC5VBnAEE_EhOBiCaC518A" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "522563222655-vtsp4k0o2ef9gua3rgf01jl4u4hm791s.apps.googleusercontent.com", 31 | "client_type": 3 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | ], 38 | "configuration_version": "1" 39 | } -------------------------------------------------------------------------------- /lib/view/settings/user_setting_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:dcomic/generated/l10n.dart'; 4 | import 'package:dcomic/model/comic_source/sourceProvider.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class UserSettingPage extends StatefulWidget { 8 | @override 9 | State createState() { 10 | // TODO: implement createState 11 | return _UserSettingPage(); 12 | } 13 | } 14 | 15 | class _UserSettingPage extends State { 16 | @override 17 | Widget build(BuildContext context) { 18 | // TODO: implement build 19 | return Scaffold( 20 | appBar: AppBar( 21 | title: Text(S.of(context).SettingPageMainUserTitle), 22 | ), 23 | body: ListView.builder( 24 | itemBuilder: (context, index) { 25 | return Provider.of(context, listen: false) 26 | .activeSources[index] 27 | .userConfig 28 | .getSettingWidget(context); 29 | }, 30 | itemCount: Provider.of(context).activeSources.length, 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/component/ViewPointChip.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ViewPointChip extends StatelessWidget { 5 | final String content; 6 | final String id; 7 | final int num; 8 | 9 | ViewPointChip({this.content, this.id, this.num}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | // TODO: implement build 14 | return Container( 15 | margin: EdgeInsets.all(3), 16 | child: TextButton( 17 | child: Chip( 18 | label: Text('$content'), 19 | avatar: CircleAvatar( 20 | child: Text('${num % 100}'), 21 | backgroundColor: Colors.blue[ 22 | num ~/ 100 * 100 + 400 > 800 ? 800 : num ~/ 100 * 100 + 400], 23 | foregroundColor: Colors.white, 24 | )), 25 | style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)), 26 | onPressed: () {}, 27 | ), 28 | ); 29 | } 30 | 31 | @override 32 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 33 | // TODO: implement toString 34 | return '{content: $content, id: $id, num: $num, color: ${num ~/ 100 * 100 + 400}}'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:dcomic/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // // Build our app and trigger a frame. 16 | // await tester.pumpWidget(MyApp()); 17 | // 18 | // // Verify that our counter starts at 0. 19 | // expect(find.text('0'), findsOneWidget); 20 | // expect(find.text('1'), findsNothing); 21 | // 22 | // // Tap the '+' icon and trigger a frame. 23 | // await tester.tap(find.byIcon(Icons.add)); 24 | // await tester.pump(); 25 | // 26 | // // Verify that our counter has incremented. 27 | // expect(find.text('0'), findsNothing); 28 | // expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /ios/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 522563222655-lihtg3bcqnrea6k9em6etm6cga1h0h8n.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.522563222655-lihtg3bcqnrea6k9em6etm6cga1h0h8n 9 | API_KEY 10 | AIzaSyDzVZW34AMDmTRo4bfUSEoB_r9KWYm9aVg 11 | GCM_SENDER_ID 12 | 522563222655 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | online.connlost.dcomic 17 | PROJECT_ID 18 | dcomic-93209 19 | STORAGE_BUCKET 20 | dcomic-93209.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:522563222655:ios:7afebe3677ee371343d34a 33 | 34 | -------------------------------------------------------------------------------- /lib/model/comicSearchModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/baseModel.dart'; 2 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | 5 | class ComicSearchModel extends BaseModel { 6 | List _list = []; 7 | int page = 0; 8 | final BaseSourceModel model; 9 | final String keyword; 10 | 11 | ComicSearchModel(this.model, this.keyword); 12 | 13 | Future search(String keyword) async { 14 | if (keyword != null && keyword != '') { 15 | try { 16 | _list += await model.search(keyword, page: page); 17 | notifyListeners(); 18 | } catch (e, s) { 19 | FirebaseCrashlytics.instance 20 | .recordError(e, s, reason: 'searchFailed: ${model.type.name}'); 21 | logger.e('action: searchFailed, keyword: $keyword'); 22 | } 23 | } 24 | } 25 | 26 | Future refresh() async { 27 | _list.clear(); 28 | page = 0; 29 | await search(keyword); 30 | notifyListeners(); 31 | } 32 | 33 | Future next() async { 34 | page++; 35 | await search(keyword); 36 | notifyListeners(); 37 | } 38 | 39 | List get list => _list; 40 | 41 | int get length => _list.length; 42 | } 43 | -------------------------------------------------------------------------------- /lib/view/download_comic_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:dcomic/model/downloadChapters.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class DownloadComicPage extends StatefulWidget { 7 | final String comicId; 8 | final String title; 9 | 10 | const DownloadComicPage({Key key, this.comicId, this.title}) 11 | : super(key: key); 12 | 13 | @override 14 | State createState() { 15 | // TODO: implement createState 16 | return _DownloadComicPage(); 17 | } 18 | } 19 | 20 | class _DownloadComicPage extends State { 21 | @override 22 | Widget build(BuildContext context) { 23 | // TODO: implement build 24 | return ChangeNotifierProvider( 25 | create: (_) => DownloadChaptersModel(widget.comicId), 26 | builder: (context, child) { 27 | return Scaffold( 28 | appBar: AppBar( 29 | title: Text('下载详情-${widget.title}'), 30 | ), 31 | body: ListView.builder( 32 | itemCount: Provider.of(context).length, 33 | itemBuilder: Provider.of(context) 34 | .buildChapterListTile), 35 | ); 36 | }, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/model/comicLatestUpdateModel.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:dcomic/model/baseModel.dart'; 3 | import 'package:dcomic/model/comicRankingListModel.dart'; 4 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 5 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 6 | 7 | class ComicLatestUpdateModel extends BaseModel { 8 | final BaseSourceModel model; 9 | int page = 0; 10 | List _data = []; 11 | 12 | ComicLatestUpdateModel(this.model); 13 | 14 | getLatestList() async { 15 | try { 16 | _data += await model.homePageHandler.getLatestUpdate(page); 17 | notifyListeners(); 18 | logger.i( 19 | 'class: ComicLatestUpdateModel,action: getLatestList, page: $page'); 20 | } catch (e, s) { 21 | FirebaseCrashlytics.instance 22 | .recordError(e, s, reason: 'getLatestListFailed'); 23 | logger.e( 24 | 'class: ComicLatestUpdateModel, action: getLatestListFailed, exception: $e'); 25 | } 26 | } 27 | 28 | Future refresh() async { 29 | page = 0; 30 | _data.clear(); 31 | await getLatestList(); 32 | notifyListeners(); 33 | } 34 | 35 | Future next() async { 36 | page++; 37 | await getLatestList(); 38 | notifyListeners(); 39 | } 40 | 41 | int get length => _data.length; 42 | 43 | List get data => _data; 44 | } 45 | -------------------------------------------------------------------------------- /lib/model/server_controller/UserListModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/baseModel.dart'; 2 | import 'package:dcomic/model/comic_source/IPFSSourceProivder.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | 5 | class UserListModel extends BaseModel { 6 | final IPFSSourceModel node; 7 | List _data = []; 8 | 9 | UserListModel(this.node); 10 | 11 | Future init() async { 12 | try { 13 | var response = await node.handler.getUserList(); 14 | if (response.statusCode == 200) { 15 | _data = response.data['data'] 16 | .map((e) => 17 | User(e['username'], e['nickname'], e['rights'], e['avatar'])) 18 | .toList(); 19 | notifyListeners(); 20 | } 21 | } catch (e, s) { 22 | FirebaseCrashlytics.instance 23 | .recordError(e, s, reason: 'userLoadingFailed'); 24 | } 25 | } 26 | 27 | List get data => _data; 28 | } 29 | 30 | class User { 31 | final String username; 32 | final String nickname; 33 | final List rights; 34 | final String avatar; 35 | 36 | User(this.username, this.nickname, this.rights, this.avatar); 37 | 38 | bool get admin { 39 | for (var item in rights) { 40 | if (item['right_num'] == 1) { 41 | return true; 42 | } 43 | } 44 | return false; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/model/sourceSearchProvider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:dcomic/component/SearchDialog.dart'; 4 | import 'package:dcomic/database/sourceDatabaseProvider.dart'; 5 | import 'package:dcomic/model/baseModel.dart'; 6 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 7 | 8 | class SourceSearchProvider extends BaseModel { 9 | final BaseSourceModel model; 10 | final String comicId; 11 | String _keyword; 12 | List _list = []; 13 | 14 | SourceSearchProvider(this.model, this._keyword, this.comicId); 15 | 16 | Future refresh() async { 17 | _list = await model.search(keyword); 18 | print(_list); 19 | notifyListeners(); 20 | } 21 | 22 | Widget buildListTile(context, index) { 23 | if (index >= 0 && index < length) { 24 | var result=_list[index]; 25 | return SearchListTile(result.cover, result.title, result.tag, result.comicId, result.author); 26 | } 27 | return null; 28 | } 29 | 30 | Future boundComicId(String boundId)async{ 31 | await SourceDatabaseProvider.boundComic(model.type.name, comicId, boundId); 32 | } 33 | 34 | int get length => _list.length; 35 | 36 | String get keyword => _keyword; 37 | 38 | set keyword(String value) { 39 | _keyword = value; 40 | notifyListeners(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/model/comicCommentModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/baseModel.dart'; 2 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | 5 | class ComicCommentModel extends BaseModel { 6 | int page = 0; 7 | List _data = []; 8 | Map comments = {}; 9 | 10 | String error; 11 | 12 | final ComicDetail detail; 13 | 14 | ComicCommentModel(this.detail); 15 | 16 | Future getComment(int page) async { 17 | if (detail != null) { 18 | try { 19 | _data += await detail.getComments(page); 20 | notifyListeners(); 21 | } on UnimplementedError { 22 | error = "该源不支持评论查看"; 23 | } catch (e, s) { 24 | FirebaseCrashlytics.instance.recordError(e, s, 25 | reason: 26 | 'getComicCommentFailed: ${detail != null ? detail.comicId : null}'); 27 | error = "未知错误:$e"; 28 | } 29 | } else { 30 | error = '未获得漫画源'; 31 | } 32 | notifyListeners(); 33 | } 34 | 35 | Future refresh() async { 36 | _data.clear(); 37 | page = 0; 38 | await getComment(page); 39 | notifyListeners(); 40 | } 41 | 42 | Future next() async { 43 | page++; 44 | await getComment(page); 45 | notifyListeners(); 46 | } 47 | 48 | List get data => _data; 49 | 50 | int get length => _data.length; 51 | } 52 | -------------------------------------------------------------------------------- /lib/component/LoadingCube.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_spinkit/flutter_spinkit.dart'; 4 | 5 | class LoadingCube extends StatelessWidget { 6 | final Color textColor; 7 | final Color backgroundColor; 8 | final Color cubeColor; 9 | 10 | const LoadingCube({Key key, this.textColor, this.backgroundColor, this.cubeColor}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | // TODO: implement build 15 | return Container( 16 | width: double.infinity, 17 | height: double.infinity, 18 | child: Center( 19 | child: SizedBox( 20 | height: 200.0, 21 | width: 300.0, 22 | child: Card( 23 | color: backgroundColor, 24 | elevation: 0, 25 | child: Column( 26 | mainAxisAlignment: MainAxisAlignment.center, 27 | crossAxisAlignment: CrossAxisAlignment.center, 28 | children: [ 29 | Container( 30 | width: 50.0, 31 | height: 50.0, 32 | child: SpinKitFadingCube( 33 | color: cubeColor==null?Theme.of(context).primaryColor:cubeColor, 34 | size: 25.0, 35 | ), 36 | ), 37 | Container( 38 | child: Text('加载中',style: TextStyle(color: textColor),), 39 | ) 40 | ], 41 | ), 42 | ), 43 | )), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/component/comic_viewer/Common.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:dcomic/component/LoadingCube.dart'; 3 | import 'package:dcomic/component/comic_viewer/EndPage.dart'; 4 | 5 | typedef BoolCallback = Future Function(); 6 | typedef OnPageChangeCallback = void Function(int index); 7 | 8 | class Common { 9 | static Widget builder(BuildContext context, int index, int count, 10 | IndexedWidgetBuilder builder, bool left, bool right, 11 | {bool dense = false}) { 12 | print( 13 | "class: ComicCommon, action: buildPage, index: $index, count: $count"); 14 | if(count==2){ 15 | return LoadingCube(); 16 | } 17 | if (index == 0) { 18 | if (!left) { 19 | return LoadingCube(); 20 | } else { 21 | return EndPage(); 22 | } 23 | } else if (count != null && index == count-1) { 24 | if (!right) { 25 | return LoadingCube(); 26 | } else { 27 | return EndPage(); 28 | } 29 | } else if (index > 0 && index < count) { 30 | return builder(context, index - 1); 31 | } 32 | return null; 33 | } 34 | 35 | static Widget builderVertical(BuildContext context, int index, int count, 36 | IndexedWidgetBuilder builder, bool left, bool right, 37 | {bool dense = false}){ 38 | print( 39 | "class: ComicCommon, action: buildPage, index: $index, count: $count"); 40 | if (index >= 0 && index < count) { 41 | return builder(context, index); 42 | } 43 | return null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/view/favorite/tracker_favorite_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 3 | import 'package:dcomic/component/EmptyView.dart'; 4 | import 'package:dcomic/component/LoadingCube.dart'; 5 | import 'package:dcomic/model/trackerModel.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class TrackerFavoritePage extends StatefulWidget { 9 | @override 10 | State createState() { 11 | // TODO: implement createState 12 | return _TrackerFavoritePage(); 13 | } 14 | } 15 | 16 | class _TrackerFavoritePage extends State { 17 | @override 18 | Widget build(BuildContext context) { 19 | // TODO: implement build 20 | return EasyRefresh( 21 | scrollController: ScrollController(), 22 | onRefresh: () async { 23 | await Provider.of(context, listen: false).init(); 24 | }, 25 | firstRefresh: true, 26 | firstRefreshWidget: LoadingCube(), 27 | emptyWidget: Provider.of(context).empty?EmptyView():null, 28 | child: Container( 29 | padding: EdgeInsets.fromLTRB(2, 7, 2, 0), 30 | child: GridView.count( 31 | physics: NeverScrollableScrollPhysics(), 32 | shrinkWrap: true, 33 | crossAxisCount: 3, 34 | childAspectRatio: 0.6, 35 | children: 36 | Provider.of(context).getFavoriteWidget(context), 37 | ))); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/model/cloudHistoryModel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dcomic/database/sourceDatabaseProvider.dart'; 4 | import 'package:dcomic/http/UniversalRequestModel.dart'; 5 | import 'package:dcomic/model/baseModel.dart'; 6 | 7 | class CloudHistoryModel extends BaseModel { 8 | int _page = 0; 9 | List _data = []; 10 | String _uid = ''; 11 | bool _login = false; 12 | 13 | Future initLoginState() async { 14 | _login = 15 | await SourceDatabaseProvider.getSourceOption('dmzj', 'login'); 16 | if (_login) { 17 | _uid = await SourceDatabaseProvider.getSourceOption('dmzj', 'uid'); 18 | } 19 | } 20 | 21 | Future getHistory() async { 22 | try { 23 | var response = await UniversalRequestModel.dmzjInterfaceRequestHandler 24 | .getHistory(_uid, _page); 25 | if (response.statusCode == 200) { 26 | var data = jsonDecode(response.data); 27 | _data += data; 28 | notifyListeners(); 29 | } 30 | } catch (e) { 31 | logger.e( 32 | 'class: ${this.runtimeType}, action: getHistoryFailed, exception: $e'); 33 | } 34 | } 35 | 36 | Future refresh() async { 37 | _page = 0; 38 | _data.clear(); 39 | await initLoginState(); 40 | await getHistory(); 41 | notifyListeners(); 42 | } 43 | 44 | Future next() async { 45 | _page++; 46 | await getHistory(); 47 | notifyListeners(); 48 | } 49 | 50 | bool get login => _login; 51 | 52 | List get data => _data; 53 | 54 | int get length => _data.length; 55 | } 56 | -------------------------------------------------------------------------------- /lib/protobuf/novel_chapter.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package dmzj.novel; 4 | 5 | 6 | message NovelChapterResponse { 7 | int32 Errno = 1; 8 | string Errmsg = 2; 9 | repeated NovelChapterVolumeResponse Data= 3; 10 | } 11 | 12 | message NovelChapterVolumeResponse { 13 | int32 VolumeId = 1; 14 | string VolumeName = 2; 15 | int32 VolumeOrder=3; 16 | repeated NovelChapterItemResponse Chapters=4; 17 | } 18 | 19 | message NovelChapterItemResponse { 20 | int32 ChapterId = 1; 21 | string ChapterName=2; 22 | int32 ChapterOrder = 3; 23 | } 24 | 25 | 26 | message NovelDetailResponse { 27 | int32 Errno = 1; 28 | string Errmsg = 2; 29 | NovelDetailInfoResponse Data= 3; 30 | } 31 | 32 | message NovelDetailInfoResponse { 33 | int32 NovelId = 1; 34 | string Name = 2; 35 | string Zone=3; 36 | string Status=4; 37 | string LastUpdateVolumeName=5; 38 | string LastUpdateChapterName=6; 39 | int32 LastUpdateVolumeId=7; 40 | int32 LastUpdateChapterId=8; 41 | int64 LastUpdateTime=9; 42 | string Cover=10; 43 | int32 HotHits=11; 44 | string Introduction=12; 45 | repeated string Types=13; 46 | string Authors=14; 47 | string FirstLetter=15; 48 | int32 SubscribeNum=16; 49 | int64 RedisUpdateTime=17; 50 | repeated NovelDetailInfoVolumeResponse Volume=18; 51 | } 52 | 53 | message NovelDetailInfoVolumeResponse { 54 | int32 VolumeId = 1; 55 | int32 LnovelId = 2; 56 | string VolumeName=3; 57 | int32 VolumeOrder=4; 58 | int64 Addtime=5; 59 | int32 SumChapters=6; 60 | } -------------------------------------------------------------------------------- /lib/component/comic/BaseListTile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class BaseListTile extends StatelessWidget { 5 | final Widget leading; 6 | final List detail; 7 | final VoidCallback onPressed; 8 | 9 | const BaseListTile({Key key, this.leading, this.detail, this.onPressed}) 10 | : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | // TODO: implement build 15 | return InkWell( 16 | onTap: () { 17 | if (onPressed != null) { 18 | onPressed(); 19 | } 20 | }, 21 | child: Container( 22 | padding: EdgeInsets.fromLTRB(8, 8, 8, 0), 23 | child: Container( 24 | padding: EdgeInsets.only(bottom: 8), 25 | decoration: BoxDecoration( 26 | border: Border( 27 | bottom: BorderSide(color: Colors.grey.withOpacity(0.1)))), 28 | child: Row( 29 | crossAxisAlignment: CrossAxisAlignment.start, 30 | children: [ 31 | ClipRRect( 32 | borderRadius: BorderRadius.circular(4), 33 | child: Container(width: 100, child: leading)), 34 | SizedBox( 35 | width: 12, 36 | ), 37 | Expanded( 38 | child: Column( 39 | crossAxisAlignment: CrossAxisAlignment.start, 40 | children: detail), 41 | ) 42 | ], 43 | ), 44 | ), 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/top/hanerx/flutterdmzj/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package top.hanerx.flutterdmzj 2 | 3 | import android.view.KeyEvent 4 | import androidx.annotation.NonNull 5 | import io.flutter.embedding.android.FlutterActivity; 6 | import io.flutter.embedding.engine.FlutterEngine 7 | import io.flutter.plugin.common.EventChannel 8 | import io.flutter.plugins.GeneratedPluginRegistrant 9 | 10 | 11 | class MainActivity : FlutterActivity() { 12 | private val VOLUME_CHANNEL = "top.hanerx/volume"; 13 | private var event: EventChannel.EventSink? = null; 14 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { 15 | GeneratedPluginRegistrant.registerWith(flutterEngine); 16 | EventChannel(flutterEngine.dartExecutor.binaryMessenger, VOLUME_CHANNEL).setStreamHandler(object : EventChannel.StreamHandler { 17 | override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { 18 | event = events!!; 19 | } 20 | 21 | override fun onCancel(arguments: Any?) { 22 | event = null; 23 | } 24 | 25 | }); 26 | } 27 | 28 | 29 | override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { 30 | if (this.event != null) { 31 | if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 32 | this.event!!.success(0); 33 | return true; 34 | } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { 35 | this.event!!.success(1); 36 | return true; 37 | } 38 | } 39 | return super.onKeyDown(keyCode, event) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/component/DataBaseTable.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class DataBaseTable extends StatelessWidget { 5 | final List headers; 6 | final List> data; 7 | final String table; 8 | 9 | const DataBaseTable({Key key, this.headers, this.data, this.table}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | // TODO: implement build 14 | return SingleChildScrollView( 15 | child: Container( 16 | width: MediaQuery.of(context).size.width, 17 | child: SingleChildScrollView( 18 | physics: ClampingScrollPhysics(), 19 | child: DataTable( 20 | columns: _buildHeaders(context), rows: _buildRows(context)), 21 | scrollDirection: Axis.horizontal, 22 | ), 23 | ), 24 | ); 25 | } 26 | 27 | List _buildHeaders(context) { 28 | return headers 29 | .map((e) => DataColumn(label: Text('$e',style: TextStyle(fontSize: 17,fontWeight: FontWeight.bold),))) 30 | .toList(); 31 | } 32 | 33 | List _buildRows(context) { 34 | if (data == null) { 35 | return []; 36 | } 37 | return data 38 | .map((el) => DataRow( 39 | 40 | cells: headers 41 | .map((e) => DataCell(Container( 42 | child: Text('${el[e]}',style: TextStyle(fontWeight: e==headers.first?FontWeight.bold:null),), 43 | constraints: BoxConstraints(maxWidth: 200), 44 | ))) 45 | .toList())) 46 | .toList(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/model/subjectListModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/baseModel.dart'; 2 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | 5 | class SubjectListModel extends BaseModel { 6 | final BaseSourceModel model; 7 | int page = 0; 8 | List _data = []; 9 | String error; 10 | 11 | SubjectListModel(this.model); 12 | 13 | Future getSubjectList(int page) async { 14 | try { 15 | _data += await this.model.homePageHandler.getSubjectList(page); 16 | error = null; 17 | notifyListeners(); 18 | } on UnimplementedError { 19 | error = "该源不支持本功能"; 20 | notifyListeners(); 21 | } on LoginRequiredError { 22 | error = '专题页需要登录,请先登录'; 23 | notifyListeners(); 24 | } catch (e, s) { 25 | FirebaseCrashlytics.instance 26 | .recordError(e, s, reason: 'subjectListLoadingFail'); 27 | error = '未知错误:$e'; 28 | notifyListeners(); 29 | } 30 | } 31 | 32 | Future refresh() async { 33 | _data.clear(); 34 | page = 0; 35 | await getSubjectList(page); 36 | notifyListeners(); 37 | } 38 | 39 | Future next() async { 40 | page++; 41 | await getSubjectList(page); 42 | notifyListeners(); 43 | } 44 | 45 | List get data => _data; 46 | 47 | int get length => _data.length; 48 | } 49 | 50 | class SubjectItem { 51 | final String cover; 52 | final String title; 53 | final String subtitle; 54 | final String subjectId; 55 | final BaseSourceModel model; 56 | 57 | SubjectItem( 58 | {this.cover, this.title, this.subtitle, this.subjectId, this.model}); 59 | } 60 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | dcomic 30 | 31 | 32 | 33 | 36 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/model/comicCategoryModel.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:dcomic/component/CategoryCard.dart'; 6 | import 'package:dcomic/model/baseModel.dart'; 7 | 8 | class ComicCategoryModel extends BaseModel { 9 | final BaseSourceModel model; 10 | List category = []; 11 | bool _local = false; 12 | 13 | ComicCategoryModel(this.model); 14 | 15 | Future init() async { 16 | try { 17 | category = await model.homePageHandler 18 | .getCategory(type: _local ? CategoryType.local : CategoryType.cloud); 19 | } catch (e, s) { 20 | FirebaseCrashlytics.instance 21 | .recordError(e, s, reason: 'CategoryLoadingFailed'); 22 | } 23 | notifyListeners(); 24 | } 25 | 26 | List buildCategoryWidget(context) { 27 | return category 28 | .map((e) => CategoryCard( 29 | cover: e.cover, 30 | title: e.title, 31 | tagId: e.categoryId, 32 | model: e.model, 33 | )) 34 | .toList(); 35 | } 36 | 37 | bool get empty => category.length == 0; 38 | 39 | bool get local => _local; 40 | 41 | set local(bool value) { 42 | _local = value; 43 | notifyListeners(); 44 | } 45 | } 46 | 47 | class CategoryModel { 48 | final String cover; 49 | final Map headers; 50 | final String title; 51 | final String categoryId; 52 | final BaseSourceModel model; 53 | 54 | CategoryModel( 55 | {this.cover, this.headers, this.title, this.categoryId, this.model}); 56 | } 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/view/novel_pages/novel_main_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/view/novel_pages/novel_category_page.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:dcomic/component/search/SearchButton.dart'; 5 | 6 | import 'package:dcomic/view/category_page.dart'; 7 | import 'package:dcomic/view/novel_pages/novel_home_page.dart'; 8 | import 'package:dcomic/view/novel_pages/novel_latest_update_page.dart'; 9 | import 'package:dcomic/view/novel_pages/novel_ranking_page.dart'; 10 | 11 | class NovelMainPage extends StatefulWidget { 12 | @override 13 | State createState() { 14 | // TODO: implement createState 15 | return _NovelMainPage(); 16 | } 17 | } 18 | 19 | class _NovelMainPage extends State { 20 | @override 21 | Widget build(BuildContext context) { 22 | // TODO: implement build 23 | return DefaultTabController( 24 | length: 4, 25 | child: Scaffold( 26 | appBar: AppBar( 27 | title: Text('轻小说站'), 28 | actions: [ 29 | SearchButton() 30 | ], 31 | bottom: TabBar(tabs: [ 32 | Tab( 33 | text: '主页', 34 | ), 35 | Tab( 36 | text: '分类', 37 | ), 38 | Tab( 39 | text: '排行', 40 | ), 41 | Tab( 42 | text: '最新', 43 | ) 44 | ]), 45 | ), 46 | body: TabBarView( 47 | children: [ 48 | NovelHomePage(), 49 | NovelCategoryPage(), 50 | NovelRankingPage(), 51 | NovelLatestUpdatePage(), 52 | ], 53 | ), 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/component/comic_viewer/EndPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class EndPage extends StatefulWidget { 5 | @override 6 | State createState() { 7 | // TODO: implement createState 8 | return _EndPage(); 9 | } 10 | 11 | EndPage({Key key}) : super(key: key); 12 | } 13 | 14 | class _EndPage extends State { 15 | @override 16 | Widget build(BuildContext context) { 17 | // TODO: implement build 18 | return Container( 19 | child: Center( 20 | child: Container( 21 | height: 300, 22 | width: 400, 23 | child: Card( 24 | elevation: 0, 25 | child: Container( 26 | padding: EdgeInsets.all(10), 27 | child: Column( 28 | children: [ 29 | Row( 30 | children: [ 31 | Expanded( 32 | child: Center( 33 | child: Text( 34 | '没得了,你已经翻到头了', 35 | style: TextStyle(fontSize: 18), 36 | )), 37 | ) 38 | ], 39 | ), 40 | Expanded( 41 | child: Row( 42 | children: [ 43 | Expanded( 44 | child: Center( 45 | child: Text('这里以后或许有点功能,现在就先放着好了'), 46 | ), 47 | ) 48 | ], 49 | ), 50 | ), 51 | ], 52 | ), 53 | )), 54 | ), 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/model/comicAuthorModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/comicRankingListModel.dart'; 2 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:dcomic/component/AuthorCard.dart'; 6 | import 'package:dcomic/model/baseModel.dart'; 7 | 8 | class ComicAuthorModel extends BaseModel { 9 | final String authorId; 10 | final BaseSourceModel model; 11 | 12 | int page = 0; 13 | 14 | List comics = []; 15 | 16 | ComicAuthorModel(this.authorId, this.model); 17 | 18 | Future init() async { 19 | // CustomHttp http = CustomHttp(); 20 | // try{ 21 | // var response = await http.getAuthor(authorId); 22 | // if (response.statusCode == 200) { 23 | // comics=response.data['data']; 24 | // notifyListeners(); 25 | // } 26 | // }catch(e){ 27 | // logger.e('class: ComicAuthorModel, action: initFailed, authorId: $authorId exception: $e'); 28 | // } 29 | try { 30 | comics = 31 | await model.homePageHandler.getAuthorComics(authorId, page: page); 32 | } catch (e, s) { 33 | FirebaseCrashlytics.instance 34 | .recordError(e, s, reason: 'getAuthorFailed'); 35 | } 36 | notifyListeners(); 37 | } 38 | 39 | List buildAuthorWidget(context) { 40 | return comics 41 | .map((e) => AuthorCard( 42 | imageUrl: e.cover, 43 | title: e.title, 44 | subtitle: e.types, 45 | model: e.model, 46 | id: e.comicId, 47 | )) 48 | .toList(); 49 | } 50 | 51 | bool get empty => comics.length == 0; 52 | } 53 | -------------------------------------------------------------------------------- /lib/view/download_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 4 | import 'package:dcomic/component/EmptyView.dart'; 5 | import 'package:dcomic/component/LoadingCube.dart'; 6 | import 'package:dcomic/model/download.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class DownloadPage extends StatefulWidget { 10 | @override 11 | State createState() { 12 | // TODO: implement createState 13 | return _DownloadPage(); 14 | } 15 | } 16 | 17 | class _DownloadPage extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | // TODO: implement build 21 | return ChangeNotifierProvider( 22 | create: (_) => DownloadModel(), 23 | builder: (context, child) { 24 | return Scaffold( 25 | appBar: AppBar( 26 | title: Text('下载管理'), 27 | ), 28 | body: EasyRefresh( 29 | firstRefresh: true, 30 | firstRefreshWidget: LoadingCube(), 31 | onRefresh: () async { 32 | await Provider.of(context, listen: false) 33 | .getComic(); 34 | return true; 35 | }, 36 | emptyWidget: Provider.of(context).length == 0 37 | ? EmptyView() 38 | : null, 39 | child: ListView.builder( 40 | itemBuilder: 41 | Provider.of(context).buildComicListTile, 42 | itemCount: Provider.of(context).length, 43 | ), 44 | )); 45 | }, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/view/mag_maker/mag_example_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:code_editor/code_editor.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_json_widget/flutter_json_widget.dart'; 7 | import 'package:dcomic/component/CustomDrawer.dart'; 8 | 9 | class MagExamplePage extends StatelessWidget { 10 | final String example; 11 | 12 | const MagExamplePage({Key key, this.example}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | // TODO: implement build 17 | return Scaffold( 18 | appBar: AppBar( 19 | title: Text('标准实例'), 20 | ), 21 | body: Builder(builder: (context)=>Container( 22 | child: CodeEditor( 23 | disableNavigationbar: true, 24 | model: EditorModel( 25 | styleOptions: EditorModelStyleOptions(heightOfContainer: MediaQuery.of(context).size.height-Scaffold.of(context).appBarMaxHeight), 26 | files: [ 27 | FileEditor(code: example, language: 'json', name: 'meta.json') 28 | ]), 29 | edit: false, 30 | )), 31 | ), 32 | endDrawer: CustomDrawer( 33 | widthPercent: 0.9, 34 | child: Scaffold( 35 | appBar: AppBar( 36 | title: Text('标准实例'), 37 | ), 38 | body: Builder(builder: (context) { 39 | try { 40 | return SingleChildScrollView( 41 | child: JsonViewerWidget(jsonDecode(example)), 42 | ); 43 | } catch (e) { 44 | return Center( 45 | child: Text('你的json好像没写对啊,没法渲染啊'), 46 | ); 47 | } 48 | }), 49 | ), 50 | ), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/component/search/SearchButton.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:dcomic/model/systemSettingModel.dart'; 3 | import 'package:dcomic/view/search_page.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class SearchButton extends StatefulWidget { 7 | @override 8 | State createState() { 9 | // TODO: implement createState 10 | return _SearchButton(); 11 | } 12 | } 13 | 14 | class _SearchButton extends State { 15 | int _count = 0; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | // TODO: implement build 20 | return TextButton( 21 | child: Icon( 22 | Icons.search, 23 | color: Colors.white, 24 | ), 25 | onPressed: () { 26 | Navigator.push(context, MaterialPageRoute(builder: (context) { 27 | return SearchPage(); 28 | },settings: RouteSettings(name: 'search_page'))); 29 | }, 30 | onLongPress: () { 31 | showDialog( 32 | context: context, 33 | builder: (context) { 34 | return SimpleDialog( 35 | title: Text('小彩蛋~'), 36 | children: [ 37 | SimpleDialogOption( 38 | child: Text('我们耕耘黑暗,却守护光明'), 39 | onPressed: () { 40 | if (_count > 10) { 41 | Navigator.of(context).pop(); 42 | Provider.of(context,listen: false).labState=true; 43 | } else { 44 | setState(() { 45 | _count++; 46 | }); 47 | } 48 | }, 49 | ) 50 | ], 51 | ); 52 | }); 53 | }, 54 | ); 55 | } 56 | } -------------------------------------------------------------------------------- /lib/component/CustomDrawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class CustomDrawer extends StatelessWidget { 6 | final double elevation; 7 | final Widget child; 8 | final String semanticLabel; 9 | ///new start 10 | final double widthPercent; 11 | ///new end 12 | const CustomDrawer({ 13 | Key key, 14 | this.elevation = 16.0, 15 | this.child, 16 | this.semanticLabel, 17 | ///new start 18 | this.widthPercent = 0.7, 19 | ///new end 20 | }) : 21 | ///new start 22 | assert(widthPercent!=null&&widthPercent<1.0&&widthPercent>0.0) 23 | ///new end 24 | ,super(key: key); 25 | @override 26 | Widget build(BuildContext context) { 27 | assert(debugCheckHasMaterialLocalizations(context)); 28 | String label = semanticLabel; 29 | switch (defaultTargetPlatform) { 30 | case TargetPlatform.iOS: 31 | case TargetPlatform.macOS: 32 | label = semanticLabel; 33 | break; 34 | case TargetPlatform.android: 35 | case TargetPlatform.fuchsia: 36 | case TargetPlatform.linux: 37 | case TargetPlatform.windows: 38 | label = semanticLabel ?? MaterialLocalizations.of(context)?.drawerLabel; 39 | break; 40 | } 41 | ///new start 42 | final double _width=MediaQuery.of(context).size.width*widthPercent; 43 | ///new end 44 | return Semantics( 45 | scopesRoute: true, 46 | namesRoute: true, 47 | explicitChildNodes: true, 48 | label: label, 49 | child: ConstrainedBox( 50 | ///edit start 51 | constraints: BoxConstraints.expand(width: _width), 52 | ///edit end 53 | child: Material( 54 | elevation: elevation, 55 | child: child, 56 | ), 57 | ), 58 | ); 59 | } 60 | } -------------------------------------------------------------------------------- /lib/component/TypeTags.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/comicCategoryModel.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:dcomic/view/category_detail_page.dart'; 5 | 6 | class TypeTags extends StatelessWidget { 7 | final List tags; 8 | 9 | TypeTags(this.tags); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | // TODO: implement build 14 | return TextButton( 15 | style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)), 16 | child: Text( 17 | '${tags.map((value) { 18 | return value.title; 19 | }).toList().join('/')}', 20 | style: Theme.of(context).textTheme.bodyText1), 21 | onPressed: () { 22 | showDialog( 23 | context: context, 24 | builder: (context) { 25 | return SimpleDialog( 26 | title: Text('分类'), 27 | children: tags.map((value) { 28 | return SimpleDialogOption( 29 | child: Text('${value.title}'), 30 | onPressed: () { 31 | Navigator.pop(context); 32 | if (value.categoryId != null) { 33 | Navigator.push(context, 34 | MaterialPageRoute(builder: (context) { 35 | return CategoryDetailPage( 36 | categoryId: value.categoryId, 37 | title: value.title, 38 | model: value.model, 39 | ); 40 | })); 41 | } 42 | }, 43 | ); 44 | }).toList(), 45 | ); 46 | }); 47 | }, 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/view/settings/source_setting_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/view/comic_source/comic_source_provider_page.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:dcomic/generated/l10n.dart'; 5 | import 'package:dcomic/view/comic_source/comic_source_page.dart'; 6 | 7 | class SourceSettingPage extends StatefulWidget { 8 | @override 9 | State createState() { 10 | // TODO: implement createState 11 | return _SourceSettingPage(); 12 | } 13 | } 14 | 15 | class _SourceSettingPage extends State { 16 | @override 17 | Widget build(BuildContext context) { 18 | // TODO: implement build 19 | return Scaffold( 20 | appBar: AppBar( 21 | title: Text(S.of(context).SettingPageMainSourceTitle), 22 | ), 23 | body: ListView( 24 | children: [ 25 | ListTile( 26 | title: Text(S.of(context).SettingPageSourcePageSourceTitle), 27 | subtitle: Text(S.of(context).SettingPageSourcePageSourceSubtitle), 28 | onTap: () { 29 | Navigator.of(context).push(MaterialPageRoute( 30 | builder: (context) => ComicSourcePage(), 31 | settings: RouteSettings(name: 'comic_source_page'))); 32 | }, 33 | ), 34 | ListTile( 35 | title: Text(S.of(context).SettingPageSourcePageSourceProviderTitle), 36 | subtitle: 37 | Text(S.of(context).SettingPageSourcePageSourceProviderSubtitle), 38 | onTap: () { 39 | Navigator.of(context).push(MaterialPageRoute( 40 | builder: (context) => ComicSourceProviderPage(), 41 | settings: RouteSettings(name: 'comic_source_provider_page'))); 42 | }, 43 | ), 44 | ], 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/model/subjectDetailModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/baseModel.dart'; 2 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 3 | 4 | import 'comic_source/baseSourceModel.dart'; 5 | 6 | class SubjectDetailModel extends BaseModel { 7 | final String subjectId; 8 | final BaseSourceModel model; 9 | String error; 10 | SubjectModel detail; 11 | 12 | SubjectDetailModel(this.subjectId, this.model); 13 | 14 | Future getSubjectDetail() async { 15 | try { 16 | detail = await model.homePageHandler.getSubject(subjectId); 17 | error = null; 18 | } catch (e, s) { 19 | FirebaseCrashlytics.instance 20 | .recordError(e, s, reason: 'subjectDetailLoadingFail: $subjectId'); 21 | error = '未知错误:$e'; 22 | } 23 | notifyListeners(); 24 | } 25 | 26 | String get cover => detail == null 27 | ? 'http://manhua.dmzj.com/css/img/mh_logo_dmzj.png?t=20131122' 28 | : detail.cover; 29 | 30 | Map get headers => 31 | detail == null ? {"referer": "https://m.dmzj.com"} : detail.headers; 32 | 33 | String get title => detail == null ? '加载中' : detail.title; 34 | 35 | String get description => detail == null ? '加载中...' : detail.description; 36 | 37 | List get data => detail == null ? [] : detail.data; 38 | } 39 | 40 | class SubjectModel { 41 | final String cover; 42 | final String title; 43 | final String description; 44 | final Map headers; 45 | final List data; 46 | 47 | SubjectModel( 48 | {this.cover, this.title, this.description, this.headers, this.data}); 49 | } 50 | 51 | class RecommendComic { 52 | final String cover; 53 | final String title; 54 | final String comicId; 55 | final String brief; 56 | final String reason; 57 | 58 | RecommendComic( 59 | {this.cover, this.title, this.comicId, this.brief, this.reason}); 60 | } 61 | -------------------------------------------------------------------------------- /lib/model/server_controller/ServerListModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/baseModel.dart'; 2 | import 'package:dcomic/model/comic_source/IPFSSourceProivder.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | 5 | class ServerListModel extends BaseModel { 6 | final IPFSSourceModel node; 7 | 8 | List _data = []; 9 | 10 | ServerListModel(this.node); 11 | 12 | Future init() async { 13 | try { 14 | var response = await node.handler.getServerList(); 15 | if (response.statusCode == 200) { 16 | _data = response.data['data'] 17 | .map((e) => Node(e['address'], e['token'], e['name'], 18 | e['title'], e['description'], e['type'], e['version'])) 19 | .toList(); 20 | notifyListeners(); 21 | } 22 | } catch (e, s) { 23 | FirebaseCrashlytics.instance.recordError(e, s, 24 | reason: 'serverListLoadingFailed: ${node.address}'); 25 | } 26 | } 27 | 28 | Future delete(String address) async { 29 | try { 30 | var response = await node.handler.deleteServer(address); 31 | if (response.statusCode == 200) { 32 | await init(); 33 | } 34 | } catch (e, s) { 35 | FirebaseCrashlytics.instance 36 | .recordError(e, s, reason: 'serverListDeleteFailed: ${address}'); 37 | } 38 | } 39 | 40 | List get data => _data; 41 | } 42 | 43 | class Node { 44 | final String address; 45 | final String token; 46 | final String name; 47 | final String title; 48 | final String description; 49 | final int type; 50 | final String version; 51 | 52 | static List modeList = ['独立服务', '分发服务器', '从服务器', '双向共享服务器']; 53 | 54 | Node(this.address, this.token, this.name, this.title, this.description, 55 | this.type, this.version); 56 | 57 | String get mode => 58 | type >= 0 && type < modeList.length ? modeList[type] : '未知服务器模式'; 59 | } 60 | -------------------------------------------------------------------------------- /lib/model/mag_model/MangaComicGroupModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/baseModel.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | 4 | import 'MangaComicDetailModel.dart'; 5 | 6 | class MangaComicGroupModel extends BaseModel { 7 | GroupObject group; 8 | final EditMode mode; 9 | final String outputPath; 10 | 11 | TextEditingController groupIdController = TextEditingController(); 12 | TextEditingController titleController = TextEditingController(); 13 | List _data = []; 14 | 15 | MangaComicGroupModel( 16 | this.group, 17 | this.mode, this.outputPath, 18 | ); 19 | 20 | Future init() async { 21 | if (mode == EditMode.edit) { 22 | groupIdController.text = group.groupId; 23 | titleController.text = group.title; 24 | _data = group.chapters; 25 | notifyListeners(); 26 | } 27 | } 28 | 29 | GroupObject delete() { 30 | group.delete = true; 31 | return group; 32 | } 33 | 34 | GroupObject getGroup() { 35 | if (group == null) { 36 | group = GroupObject(); 37 | } 38 | group.title = titleController.text; 39 | group.groupId = groupIdController.text; 40 | group.chapters = _data; 41 | return group; 42 | } 43 | 44 | void reOrderGroup(oldIndex, newIndex) { 45 | var tmp = _data[oldIndex]; 46 | _data[oldIndex] = _data[newIndex]; 47 | _data[newIndex] = tmp; 48 | notifyListeners(); 49 | } 50 | 51 | List get data => _data; 52 | 53 | void addChapter(Chapter chapter) { 54 | _data.add(chapter); 55 | notifyListeners(); 56 | } 57 | 58 | void deleteChapter(int index) { 59 | if (index >= 0 && index < _data.length) { 60 | _data.removeAt(index); 61 | notifyListeners(); 62 | } 63 | } 64 | 65 | void updateChapter(int index, Chapter result) { 66 | if (index >= 0 && index < _data.length) { 67 | _data[index] = result; 68 | notifyListeners(); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/component/DataBaseDefineTile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:dcomic/database/databaseCommon.dart'; 4 | 5 | class DatabaseDefineTile extends StatelessWidget { 6 | final DatabaseStaticModel model; 7 | final String table; 8 | 9 | const DatabaseDefineTile({Key key, this.model, this.table}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | // TODO: implement build 14 | return ListTile( 15 | dense: true, 16 | title: Text.rich(TextSpan(children: [ 17 | TextSpan(text: '$table', style: TextStyle(fontSize: 20)), 18 | TextSpan(text: ' '), 19 | TextSpan( 20 | text: '最后更新版本:${model.version}', 21 | style: TextStyle( 22 | fontSize: 12, 23 | fontWeight: FontWeight.normal, 24 | color: ListTileTheme.of(context).textColor)), 25 | TextSpan(text: ' '), 26 | TextSpan( 27 | text: '重构版本:${model.rebuildVersion} 弃用版本:${model.dropVersion}', 28 | style: TextStyle( 29 | fontSize: 12, 30 | fontWeight: FontWeight.normal, 31 | color: ListTileTheme.of(context).textColor)), 32 | ])), 33 | subtitle: ListView( 34 | shrinkWrap: true, 35 | physics: NeverScrollableScrollPhysics(), 36 | children: model.tables.keys 37 | .map((e) => ListTile( 38 | dense: true, 39 | title: Text( 40 | '$e', 41 | style: TextStyle( 42 | color: Theme.of(context).textSelectionColor), 43 | ), 44 | subtitle: Text('${model.tables[e]}'), 45 | )) 46 | .toList() + 47 | [Divider()], 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/component/Authors.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/comicCategoryModel.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:dcomic/view/author_page.dart'; 5 | 6 | class Authors extends StatelessWidget { 7 | final List tags; 8 | 9 | Authors(this.tags); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | // TODO: implement build 14 | return TextButton( 15 | style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)), 16 | child: Text( 17 | '${tags.map((value) { 18 | return value.title; 19 | }).toList().join('/')}', 20 | style: Theme.of(context).textTheme.bodyText1), 21 | onPressed: () { 22 | showDialog( 23 | context: context, 24 | builder: (context) { 25 | return SimpleDialog( 26 | title: Text('作者'), 27 | children: tags.map((value) { 28 | return SimpleDialogOption( 29 | child: Text('${value.title}'), 30 | onPressed: () { 31 | Navigator.pop(context); 32 | if (value.categoryId != null) { 33 | Navigator.push( 34 | context, 35 | MaterialPageRoute( 36 | builder: (context) { 37 | return AuthorPage( 38 | author: value.title, 39 | authorId: value.categoryId, 40 | model: value.model); 41 | }, 42 | settings: RouteSettings(name: 'author_page'))); 43 | } 44 | }, 45 | ); 46 | }).toList(), 47 | ); 48 | }); 49 | }, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # Add the Firebase pod for Google Analytics 5 | pod 'Firebase/Analytics' 6 | 7 | # For Analytics without IDFA collection capability, use this pod instead 8 | # pod ‘Firebase/AnalyticsWithoutAdIdSupport’ 9 | 10 | # Add the pods for any other Firebase products you want to use in your app 11 | # For example, to use Firebase Authentication and Cloud Firestore 12 | pod 'Firebase/Auth' 13 | pod 'Firebase/Analytics' 14 | pod 'Firebase/Crashlytics' 15 | pod 'Firebase/Performance' 16 | 17 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 18 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 19 | 20 | project 'Runner', { 21 | 'Debug' => :debug, 22 | 'Profile' => :release, 23 | 'Release' => :release, 24 | } 25 | 26 | def flutter_root 27 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 28 | unless File.exist?(generated_xcode_build_settings_path) 29 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 30 | end 31 | 32 | File.foreach(generated_xcode_build_settings_path) do |line| 33 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 34 | return matches[1].strip if matches 35 | end 36 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 37 | end 38 | 39 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 40 | 41 | flutter_ios_podfile_setup 42 | 43 | target 'Runner' do 44 | use_frameworks! 45 | use_modular_headers! 46 | 47 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 48 | end 49 | 50 | post_install do |installer| 51 | installer.pods_project.targets.each do |target| 52 | flutter_additional_ios_build_settings(target) 53 | end 54 | end 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /lib/view/server_controllers/server_sellect_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 2 | import 'package:dcomic/model/comic_source/sourceProvider.dart'; 3 | import 'package:dcomic/view/server_controllers/server_main_page.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 7 | import 'package:fluttericon/font_awesome5_icons.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class ServerSelectPage extends StatefulWidget { 11 | @override 12 | State createState() { 13 | // TODO: implement createState 14 | return _ServerSelectPage(); 15 | } 16 | } 17 | 18 | class _ServerSelectPage extends State { 19 | @override 20 | Widget build(BuildContext context) { 21 | // TODO: implement build 22 | return Scaffold( 23 | appBar: AppBar( 24 | title: Text('选择操作服务器'), 25 | ), 26 | body: EasyRefresh( 27 | child: ListView.builder( 28 | itemCount: 29 | Provider.of(context).ipfsSourceModels.length, 30 | itemBuilder: (context, index) { 31 | var node = 32 | Provider.of(context).ipfsSourceModels[index]; 33 | return ListTile( 34 | enabled: node.userConfig.status == UserStatus.login, 35 | leading: Icon(FontAwesome5.server), 36 | title: Text('${node.title}'), 37 | subtitle: Text('服务器地址:${node.address} 简介:${node.description}'), 38 | onTap: () { 39 | Navigator.of(context).push(MaterialPageRoute( 40 | builder: (context) => ServerMainPage( 41 | node: node, 42 | ), 43 | settings: RouteSettings(name: 'server_main_page'))); 44 | }, 45 | ); 46 | }), 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/model/server_controller/NewComicGroupModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/baseModel.dart'; 2 | import 'package:dcomic/model/comic_source/IPFSSourceProivder.dart'; 3 | import 'package:dcomic/model/server_controller/NewComicDetailModel.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | 6 | class NewComicGroupModel extends BaseModel { 7 | final IPFSSourceModel node; 8 | GroupObject group; 9 | final EditMode mode; 10 | 11 | TextEditingController groupIdController = TextEditingController(); 12 | TextEditingController titleController = TextEditingController(); 13 | List _data = []; 14 | 15 | NewComicGroupModel( 16 | this.node, 17 | this.group, 18 | this.mode, 19 | ); 20 | 21 | Future init() async { 22 | if (mode == EditMode.edit) { 23 | groupIdController.text = group.groupId; 24 | titleController.text = group.title; 25 | _data = group.chapters; 26 | notifyListeners(); 27 | } 28 | } 29 | 30 | GroupObject delete() { 31 | group.delete = true; 32 | return group; 33 | } 34 | 35 | GroupObject getGroup() { 36 | if (group == null) { 37 | group = GroupObject(); 38 | } 39 | group.title = titleController.text; 40 | group.groupId = groupIdController.text; 41 | group.chapters = _data; 42 | return group; 43 | } 44 | 45 | void reOrderGroup(oldIndex, newIndex) { 46 | var tmp = _data[oldIndex]; 47 | _data[oldIndex] = _data[newIndex]; 48 | _data[newIndex] = tmp; 49 | notifyListeners(); 50 | } 51 | 52 | List get data => _data; 53 | 54 | void addChapter(Chapter chapter) { 55 | _data.add(chapter); 56 | notifyListeners(); 57 | } 58 | 59 | void deleteChapter(int index) { 60 | if (index >= 0 && index < _data.length) { 61 | _data.removeAt(index); 62 | notifyListeners(); 63 | } 64 | } 65 | 66 | void updateChapter(int index, Chapter result) { 67 | if (index >= 0 && index < _data.length) { 68 | _data[index] = result; 69 | notifyListeners(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/model/comicFavoriteModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:dcomic/component/SubscribeCard.dart'; 5 | import 'package:dcomic/model/baseModel.dart'; 6 | import 'package:dcomic/view/comic_detail_page.dart'; 7 | 8 | class ComicFavoriteModel extends BaseModel { 9 | final BaseSourceModel model; 10 | List comics = []; 11 | int page = 0; 12 | String error = '没有更多数据了'; 13 | 14 | ComicFavoriteModel(this.model); 15 | 16 | Future getSubscribe() async { 17 | try { 18 | comics += await model.getFavoriteComics(page); 19 | } on UnimplementedError catch (e) { 20 | error = '该订阅模块尚未实现'; 21 | } on FavoriteUnavailableError catch (e) { 22 | error = '该解析源不支持订阅'; 23 | } on LoginRequiredError catch (e) { 24 | error = '该解析源需要登录,请先登录'; 25 | } catch (e) { 26 | error = '未知错误: $e'; 27 | } 28 | notifyListeners(); 29 | } 30 | 31 | Future nextPage() async { 32 | page++; 33 | await getSubscribe(); 34 | notifyListeners(); 35 | } 36 | 37 | Future refresh() async { 38 | page = 0; 39 | comics.clear(); 40 | await getSubscribe(); 41 | notifyListeners(); 42 | } 43 | 44 | List getFavoriteWidget(context) { 45 | return comics 46 | .map((e) => SubscribeCard( 47 | cover: e.cover, 48 | title: e.title, 49 | subTitle: e.latestChapter, 50 | isUnread: e.update, 51 | type: e.type, 52 | onTap: () { 53 | Navigator.of(context).push(MaterialPageRoute( 54 | builder: (context) => ComicDetailPage( 55 | id: e.comicId, 56 | title: e.title, 57 | model: e.model, 58 | ))); 59 | }, 60 | )) 61 | .toList(); 62 | } 63 | 64 | bool get empty => comics.length == 0; 65 | } 66 | -------------------------------------------------------------------------------- /lib/http/KuKuRequestHandler.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:dcomic/http/UniversalRequestModel.dart'; 5 | import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; 6 | import 'package:gbk2utf8/gbk2utf8.dart'; 7 | 8 | class KuKuRequestHandler extends KKKKRequestHandler { 9 | KuKuRequestHandler() : super('https://manhua.kukudm.com/'); 10 | } 11 | 12 | class SoKuKuRequestHandler extends SingleDomainRequestHandler { 13 | SoKuKuRequestHandler() : super('https://so.kukudm.com/'); 14 | 15 | Future search(String keyword, {int page: 0}) async { 16 | return dio.get( 17 | '/search.asp?kw=${(gbk.encode(keyword)).map((e) => '%${e.toRadixString(16)}'.toUpperCase()).toList().join('')}&page=$page', 18 | options: Options(responseDecoder: gbkDecoder), 19 | ); 20 | } 21 | } 22 | 23 | class KKKKRequestHandler extends SingleDomainRequestHandler { 24 | KKKKRequestHandler(String baseUrl) : super(baseUrl,policy: CachePolicy.request); 25 | 26 | Future getComic(String comicId) { 27 | return dio.get('/comiclist/$comicId', 28 | options: Options(responseDecoder: gbkDecoder)); 29 | } 30 | 31 | Future> getChapter( 32 | String comicId, String chapterId, int page) async { 33 | return { 34 | 'data': await dio.get('/comiclist/$comicId/$chapterId/$page.htm', 35 | options: Options(responseDecoder: gbkDecoder)), 36 | 'page': page 37 | }; 38 | } 39 | 40 | Future> getImageHosts() async { 41 | try { 42 | var response = await dio.get('/js2/js4.js'); 43 | if (response.statusCode == 200) { 44 | Map map = {}; 45 | print(response.data.toString().split('\n')); 46 | for (var item in response.data.toString().split('\n')) { 47 | map[item.substring(item.indexOf('{') + 1, item.indexOf('='))] = 48 | item.substring(item.indexOf('=\'') + 2, item.indexOf('\';')); 49 | } 50 | return map; 51 | } 52 | } catch (e) { 53 | print(e); 54 | } 55 | return {}; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/view/novel_pages/novel_category_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/novelCategoryModel.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 5 | import 'package:dcomic/component/EmptyView.dart'; 6 | import 'package:dcomic/component/LoadingCube.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class NovelCategoryPage extends StatefulWidget { 10 | 11 | const NovelCategoryPage({Key key}) : super(key: key); 12 | 13 | @override 14 | State createState() { 15 | // TODO: implement createState 16 | return _NovelCategoryPage(); 17 | } 18 | } 19 | 20 | class _NovelCategoryPage extends State { 21 | @override 22 | void initState() { 23 | // TODO: implement initState 24 | super.initState(); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | // TODO: implement build 30 | return ChangeNotifierProvider( 31 | create: (_) => NovelCategoryModel(), 32 | builder: (context, child) { 33 | return Column( 34 | children: [ 35 | Expanded( 36 | child: EasyRefresh( 37 | scrollController: ScrollController(), 38 | onRefresh: () async { 39 | await Provider.of(context, listen: false) 40 | .init(); 41 | }, 42 | firstRefresh: true, 43 | firstRefreshWidget: LoadingCube(), 44 | emptyWidget: Provider.of(context).empty 45 | ? EmptyView() 46 | : null, 47 | child: GridView.count( 48 | physics: NeverScrollableScrollPhysics(), 49 | shrinkWrap: true, 50 | crossAxisCount: 3, 51 | childAspectRatio: 0.85, 52 | children: Provider.of(context) 53 | .buildCategoryWidget(context), 54 | ), 55 | )) 56 | ], 57 | ); 58 | }, 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Flutter 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | tags: 10 | - 'v*' 11 | 12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 13 | jobs: 14 | # This workflow contains a single job called "build" 15 | build: 16 | # The type of runner that the job will run on 17 | runs-on: ubuntu-latest 18 | 19 | # Steps represent a sequence of tasks that will be executed as part of the job 20 | steps: 21 | - uses: actions/checkout@v1 22 | - uses: actions/setup-java@v1 23 | with: 24 | java-version: '8.x' 25 | - uses: subosito/flutter-action@v1.4.0 26 | with: 27 | flutter-version: '2.0.4' 28 | - run: wget https://hanerx.top/key_store/${{ secrets.JKS_KEY_TOKEN }}.jks -O /home/runner/work/dcomic/dcomic/test.jks && echo -e "storePassword=${{ secrets.STORAGE_PASSWORD }}\nkeyPassword=${{ secrets.KEY_PASSWORD }}\nkeyAlias=key\nstoreFile=/home/runner/work/dcomic/dcomic/test.jks" > /home/runner/work/dcomic/dcomic/android/test.properties 29 | - run: cat /home/runner/work/dcomic/dcomic/android/test.properties 30 | - run: flutter pub get 31 | - run: flutter build apk 32 | - uses: actions/upload-artifact@v2 33 | with: 34 | name: app-release.apk 35 | path: build/app/outputs/flutter-apk/app-release.apk 36 | # build-ios: 37 | # runs-on: macos-latest 38 | # steps: 39 | # - uses: actions/checkout@v1 40 | # - uses: actions/setup-java@v1 41 | # with: 42 | # java-version: '12.x' 43 | # - uses: subosito/flutter-action@v1 44 | # with: 45 | # flutter-version: '1.20.4' 46 | # - run: flutter pub get 47 | # - run: flutter build ios --release --no-codesign 48 | # - uses: actions/upload-artifact@v2 49 | # with: 50 | # name: app-release.app 51 | # path: /Users/runner/work/flutter_dmzj/flutter_dmzj/build/ios/iphoneos/Runner.app 52 | -------------------------------------------------------------------------------- /lib/model/trackerModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:dcomic/component/SubscribeCard.dart'; 4 | import 'package:dcomic/database/tracker.dart'; 5 | import 'package:dcomic/model/baseModel.dart'; 6 | import 'package:dcomic/model/comicDetail.dart'; 7 | import 'package:dcomic/view/comic_detail_page.dart'; 8 | 9 | class TrackerModel extends BaseModel { 10 | List tracingComic = []; 11 | TrackerProvider _provider = TrackerProvider(); 12 | 13 | TrackerModel() { 14 | init(); 15 | } 16 | 17 | Future init() async { 18 | tracingComic = await _provider.getAllComic(); 19 | notifyListeners(); 20 | } 21 | 22 | List getFavoriteWidget(context) { 23 | return tracingComic 24 | .map((e) => SubscribeCard( 25 | cover: e.cover, 26 | title: e.title, 27 | subTitle: '无', 28 | isUnread: false, 29 | onTap: () { 30 | Navigator.of(context).push(MaterialPageRoute( 31 | builder: (context) => ComicDetailPage(id: e.comicId,title: e.title,))); 32 | }, 33 | )) 34 | .toList(); 35 | } 36 | 37 | Future add(ComicDetailModel model) async { 38 | var comic = await _provider.insertComic(model.model); 39 | tracingComic.add(comic); 40 | notifyListeners(); 41 | } 42 | 43 | Future delete(ComicDetailModel model) async { 44 | await _provider.deleteComic(model.rawComicId); 45 | await init(); 46 | notifyListeners(); 47 | } 48 | 49 | Future subscribe(ComicDetailModel model) async { 50 | if (await _provider.getComic(model.comicId) == null) { 51 | await add(model); 52 | notifyListeners(); 53 | return 1; 54 | } else { 55 | await delete(model); 56 | notifyListeners(); 57 | return 2; 58 | } 59 | } 60 | 61 | bool ifSubscribe(ComicDetailModel model) { 62 | for (var item in tracingComic) { 63 | if (item.comicId == model.comicId) { 64 | return true; 65 | } 66 | } 67 | return false; 68 | } 69 | 70 | bool get empty=>tracingComic.length==0; 71 | } 72 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | DComic 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | flutterdmzj 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsLocalNetworking 30 | 31 | NSAllowsArbitraryLoads 32 | 33 | NSAllowsArbitraryLoadsInWebContent 34 | 35 | 36 | UIBackgroundModes 37 | 38 | fetch 39 | remote-notification 40 | 41 | UILaunchStoryboardName 42 | LaunchScreen 43 | UIMainStoryboardFile 44 | Main 45 | UISupportedInterfaceOrientations 46 | 47 | UIInterfaceOrientationPortrait 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | UISupportedInterfaceOrientations~ipad 52 | 53 | UIInterfaceOrientationPortrait 54 | UIInterfaceOrientationPortraitUpsideDown 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UIViewControllerBasedStatusBarAppearance 59 | 60 | io.flutter.embed_views_preview 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /lib/model/novelFavoriteModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/http/UniversalRequestModel.dart'; 2 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:dcomic/component/SubscribeCard.dart'; 6 | import 'package:dcomic/model/baseModel.dart'; 7 | import 'package:dcomic/view/novel_pages/novel_detail_page.dart'; 8 | 9 | class NovelFavoriteModel extends BaseModel { 10 | final String uid; 11 | List novels = []; 12 | int page = 0; 13 | 14 | NovelFavoriteModel(this.uid); 15 | 16 | Future getSubscribe() async { 17 | try { 18 | var response = await UniversalRequestModel.dmzjRequestHandler 19 | .getSubscribe(int.parse(uid), page, type: 1); 20 | if ((response.statusCode == 200||response.statusCode == 304)) { 21 | for (var item in response.data) { 22 | bool unread = item['sub_readed'] == 0; 23 | item['unread'] = unread; 24 | } 25 | novels += response.data; 26 | } 27 | notifyListeners(); 28 | } catch (e, s) { 29 | FirebaseCrashlytics.instance 30 | .recordError(e, s, reason: 'getSubscribeFailed'); 31 | logger.e( 32 | 'class: NovelFavoriteModel, action: getSubscribeFailed, expection: $e'); 33 | } 34 | } 35 | 36 | Future nextPage() async { 37 | page++; 38 | await getSubscribe(); 39 | notifyListeners(); 40 | } 41 | 42 | Future refresh() async { 43 | page = 0; 44 | novels.clear(); 45 | await getSubscribe(); 46 | notifyListeners(); 47 | } 48 | 49 | List getFavoriteWidget(context) { 50 | return novels 51 | .map((e) => SubscribeCard( 52 | cover: e['sub_img'], 53 | title: e['name'], 54 | subTitle: e['sub_update'], 55 | isUnread: e['unread'], 56 | onTap: () { 57 | Navigator.of(context).push(MaterialPageRoute( 58 | builder: (context) => NovelDetailPage( 59 | id: e['id'], 60 | ))); 61 | }, 62 | )) 63 | .toList(); 64 | } 65 | 66 | bool get empty => novels.length == 0; 67 | } 68 | -------------------------------------------------------------------------------- /lib/view/favorite/novel_favorite_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 4 | import 'package:dcomic/component/EmptyView.dart'; 5 | import 'package:dcomic/component/LoadingCube.dart'; 6 | import 'package:dcomic/model/novelFavoriteModel.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class NovelFavoritePage extends StatefulWidget { 10 | final String uid; 11 | 12 | const NovelFavoritePage({Key key, this.uid}) : super(key: key); 13 | 14 | @override 15 | State createState() { 16 | // TODO: implement createState 17 | return _NovelFavoritePage(); 18 | } 19 | } 20 | 21 | class _NovelFavoritePage extends State { 22 | @override 23 | void initState() { 24 | // TODO: implement initState 25 | super.initState(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | // TODO: implement build 31 | return ChangeNotifierProvider( 32 | create: (_) => NovelFavoriteModel(widget.uid), 33 | builder: (context, child) { 34 | return EasyRefresh( 35 | scrollController: ScrollController(), 36 | onRefresh: () async { 37 | await Provider.of(context, listen: false) 38 | .refresh(); 39 | }, 40 | onLoad: () async { 41 | await Provider.of(context, listen: false) 42 | .nextPage(); 43 | }, 44 | emptyWidget: Provider.of(context).empty 45 | ? EmptyView() 46 | : null, 47 | firstRefresh: true, 48 | firstRefreshWidget: LoadingCube(), 49 | child: Container( 50 | padding: EdgeInsets.fromLTRB(2, 7, 2, 0), 51 | child: GridView.count( 52 | physics: NeverScrollableScrollPhysics(), 53 | shrinkWrap: true, 54 | crossAxisCount: 3, 55 | childAspectRatio: 0.6, 56 | children: Provider.of(context) 57 | .getFavoriteWidget(context), 58 | ), 59 | ), 60 | ); 61 | }, 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/database/tracker.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dcomic/database/databaseCommon.dart'; 4 | import 'package:sqflite/sqflite.dart'; 5 | 6 | class TracingComic { 7 | int id; 8 | String comicId; 9 | String title; 10 | String cover; 11 | Map data; 12 | 13 | Map toMap() { 14 | var map = { 15 | 'title': title, 16 | 'comicId': comicId, 17 | 'cover': cover, 18 | 'data': jsonEncode(data), 19 | }; 20 | if (id != null) { 21 | map['id'] = id; 22 | } 23 | return map; 24 | } 25 | 26 | TracingComic(); 27 | 28 | TracingComic.fromMap(Map map) { 29 | id = map['id']; 30 | title = map['title']; 31 | comicId = map['comicId']; 32 | cover = map['cover'].replaceAll('dmzj1','dmzj'); 33 | data = jsonDecode(map['data']); 34 | } 35 | 36 | @override 37 | String toString() { 38 | return 'Comic{id: $id, title: $title, comicId: $comicId, cover: $cover, data: $data}'; 39 | } 40 | } 41 | 42 | class TrackerProvider { 43 | Database _database; 44 | 45 | initDataBase() async { 46 | _database = await DatabaseCommon.initDatabase(); 47 | } 48 | 49 | Future insertComic(TracingComic comic) async { 50 | await initDataBase(); 51 | comic.id = await _database.insert('tracking_comic_info', comic.toMap()); 52 | return comic; 53 | } 54 | 55 | Future getComic(String comicId) async { 56 | await initDataBase(); 57 | List maps = await _database.query('tracking_comic_info', 58 | where: 'comicId = ?', whereArgs: [comicId]); 59 | if (maps.length > 0) { 60 | _database.close(); 61 | return TracingComic.fromMap(maps.first); 62 | } 63 | return null; 64 | } 65 | 66 | Future> getAllComic() async { 67 | await initDataBase(); 68 | List maps = await _database.query('tracking_comic_info'); 69 | List data = []; 70 | for (var item in maps) { 71 | data.add(TracingComic.fromMap(item)); 72 | } 73 | return data; 74 | } 75 | 76 | Future deleteComic(String comicId)async{ 77 | await initDataBase(); 78 | return await _database.delete('tracking_comic_info',where: 'comicId=?',whereArgs: [comicId]); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/view/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/http/UniversalRequestModel.dart'; 2 | import 'package:dcomic/model/comic_source/sourceProvider.dart'; 3 | import 'package:dcomic/model/homepageModel.dart'; 4 | import 'package:dcomic/view/comic_pages/subject_list_page.dart'; 5 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 6 | import 'package:flutter/cupertino.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 9 | import 'package:dcomic/component/CardView.dart'; 10 | import 'package:dcomic/component/LoadingCube.dart'; 11 | import 'package:dcomic/database/sourceDatabaseProvider.dart'; 12 | import 'package:dcomic/view/favorite_page.dart'; 13 | import 'package:provider/provider.dart'; 14 | 15 | class HomePage extends StatefulWidget { 16 | @override 17 | State createState() { 18 | // TODO: implement createState 19 | return new _HomePage(); 20 | } 21 | } 22 | 23 | class _HomePage extends State { 24 | @override 25 | Widget build(BuildContext context) { 26 | // TODO: implement build 27 | return ChangeNotifierProvider( 28 | create: (_) => 29 | HomepageModel(Provider.of(context).activeHomeModel), 30 | builder: (context, child) => EasyRefresh( 31 | firstRefreshWidget: LoadingCube(), 32 | firstRefresh: true, 33 | onRefresh: () async { 34 | await Provider.of(context, listen: false).init(); 35 | }, 36 | header: ClassicalHeader( 37 | refreshedText: '刷新完成', 38 | refreshFailedText: '刷新失败', 39 | refreshingText: '刷新中', 40 | refreshText: '下拉刷新', 41 | refreshReadyText: '释放刷新'), 42 | child: Column( 43 | children: Provider.of(context) 44 | .data 45 | .map((e) => CardView( 46 | title: e.title, 47 | list: e.detail, 48 | row: e.detail.length % 3 == 0 ? 3 : 2, 49 | ratio: e.detail.length % 3 == 0 ? 0.6 : 1.5, 50 | action: e.action == null ? null : e.action(context), 51 | )) 52 | .toList(), 53 | ), 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/view/author_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 5 | import 'package:dcomic/component/EmptyView.dart'; 6 | import 'package:dcomic/component/LoadingCube.dart'; 7 | import 'package:dcomic/generated/l10n.dart'; 8 | import 'package:dcomic/model/comicAuthorModel.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | class AuthorPage extends StatefulWidget { 12 | final String authorId; 13 | final String author; 14 | final BaseSourceModel model; 15 | 16 | AuthorPage({this.authorId, this.author, this.model}); 17 | 18 | @override 19 | State createState() { 20 | // TODO: implement createState 21 | return _AuthorPage(); 22 | } 23 | } 24 | 25 | class _AuthorPage extends State { 26 | _AuthorPage(); 27 | 28 | @override 29 | void initState() { 30 | // TODO: implement initState 31 | super.initState(); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | // TODO: implement build 37 | return ChangeNotifierProvider( 38 | create: (_) => ComicAuthorModel(widget.authorId, widget.model), 39 | builder: (context, child) { 40 | return Scaffold( 41 | appBar: AppBar( 42 | title: Text(S.of(context).AuthorPageTitle(widget.author)), 43 | ), 44 | body: EasyRefresh( 45 | onRefresh: () async { 46 | await Provider.of(context, listen: false) 47 | .init(); 48 | }, 49 | emptyWidget: Provider.of(context).empty 50 | ? EmptyView() 51 | : null, 52 | scrollController: ScrollController(), 53 | firstRefreshWidget: LoadingCube(), 54 | firstRefresh: true, 55 | child: GridView.count( 56 | physics: NeverScrollableScrollPhysics(), 57 | shrinkWrap: true, 58 | crossAxisCount: 3, 59 | childAspectRatio: 0.6, 60 | children: Provider.of(context) 61 | .buildAuthorWidget(context)), 62 | ), 63 | ); 64 | }, 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/model/comicCategoryDetailModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/baseModel.dart'; 2 | import 'package:dcomic/model/comicRankingListModel.dart'; 3 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 4 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 5 | 6 | class ComicCategoryDetailModel extends BaseModel { 7 | final String categoryId; 8 | final String title; 9 | final BaseSourceModel model; 10 | // int filterDate = 0; 11 | int filterType = 0; 12 | // int filterTag = 0; 13 | int page = 0; 14 | List _data = []; 15 | // Map dateTypeList = {}; 16 | List typeTypeList = ['按人气', '按更新']; 17 | // Map tagTypeList = {}; 18 | 19 | ComicCategoryDetailModel(this.categoryId, this.title, this.model); 20 | 21 | // init() async { 22 | // // await getCategoryFilter(); 23 | // } 24 | 25 | // Future getCategoryFilter() async { 26 | // CustomHttp http = CustomHttp(); 27 | // var response = await http.getCategoryFilter(); 28 | // if (response.statusCode == 200) { 29 | // response.data[2]['items'] 30 | // .forEach((item) => dateTypeList[item['tag_id']] = item['tag_name']); 31 | // response.data[3]['items'] 32 | // .forEach((item) => tagTypeList[item['tag_id']] = item['tag_name']); 33 | // } 34 | // notifyListeners(); 35 | // } 36 | 37 | getCategoryDetail() async { 38 | // var response = await UniversalRequestModel.dmzjRequestHandler.getCategoryDetail( 39 | // categoryId, filterDate, filterTag, filterType, page); 40 | // if (response.statusCode == 200) { 41 | // _data += response.data; 42 | // } 43 | try { 44 | _data += await model.homePageHandler 45 | .getCategoryDetail(categoryId, page: page, popular: filterType == 0); 46 | } catch (e, s) { 47 | FirebaseCrashlytics.instance 48 | .recordError(e, s, reason: 'comicGetCategoryFailed'); 49 | } 50 | notifyListeners(); 51 | } 52 | 53 | refresh() async { 54 | page = 0; 55 | _data.clear(); 56 | await getCategoryDetail(); 57 | notifyListeners(); 58 | } 59 | 60 | next() async { 61 | page++; 62 | await getCategoryDetail(); 63 | notifyListeners(); 64 | } 65 | 66 | int get length => _data.length; 67 | 68 | List get data => _data; 69 | } 70 | -------------------------------------------------------------------------------- /lib/view/favorite/comic_favorite_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 5 | import 'package:dcomic/component/EmptyView.dart'; 6 | import 'package:dcomic/component/LoadingCube.dart'; 7 | import 'package:dcomic/model/comicFavoriteModel.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class ComicFavoritePage extends StatefulWidget { 11 | final BaseSourceModel model; 12 | 13 | const ComicFavoritePage({Key key, this.model}) : super(key: key); 14 | 15 | @override 16 | State createState() { 17 | // TODO: implement createState 18 | return _ComicFavoritePage(); 19 | } 20 | } 21 | 22 | class _ComicFavoritePage extends State { 23 | @override 24 | void initState() { 25 | // TODO: implement initState 26 | super.initState(); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | // TODO: implement build 32 | return ChangeNotifierProvider( 33 | create: (_) => ComicFavoriteModel(widget.model), 34 | builder: (context, child) { 35 | return EasyRefresh( 36 | firstRefresh: true, 37 | firstRefreshWidget: LoadingCube(), 38 | scrollController: ScrollController(), 39 | onRefresh: () async { 40 | await Provider.of(context, listen: false) 41 | .refresh(); 42 | }, 43 | onLoad: () async { 44 | await Provider.of(context, listen: false) 45 | .nextPage(); 46 | }, 47 | emptyWidget: Provider.of(context).empty 48 | ? EmptyView(message: Provider.of(context).error,) 49 | : null, 50 | child: Container( 51 | padding: EdgeInsets.fromLTRB(2, 7, 2, 0), 52 | child: GridView.count( 53 | physics: NeverScrollableScrollPhysics(), 54 | shrinkWrap: true, 55 | crossAxisCount: 3, 56 | childAspectRatio: 0.6, 57 | children: Provider.of(context) 58 | .getFavoriteWidget(context), 59 | ), 60 | ), 61 | ); 62 | }, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/model/server_controller/NewComicChapterModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/baseModel.dart'; 2 | import 'package:dcomic/model/comic_source/IPFSSourceProivder.dart'; 3 | import 'package:dcomic/model/server_controller/NewComicDetailModel.dart'; 4 | import 'package:file_picker/file_picker.dart'; 5 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 6 | import 'package:flutter/cupertino.dart'; 7 | 8 | class NewComicChapterModel extends BaseModel { 9 | final IPFSSourceModel node; 10 | Chapter chapter; 11 | final EditMode mode; 12 | 13 | List _data=[]; 14 | TextEditingController chapterIdController = TextEditingController(); 15 | TextEditingController titleController = TextEditingController(); 16 | 17 | NewComicChapterModel(this.node, this.chapter, this.mode); 18 | 19 | Future init() async { 20 | if (mode == EditMode.edit) { 21 | chapterIdController.text = chapter.chapterId; 22 | titleController.text = chapter.title; 23 | _data = chapter.data; 24 | notifyListeners(); 25 | } 26 | } 27 | 28 | void reOrderGroup(int oldIndex, int newIndex) { 29 | var tmp = _data[oldIndex]; 30 | _data[oldIndex] = _data[newIndex]; 31 | _data[newIndex] = tmp; 32 | notifyListeners(); 33 | } 34 | 35 | Chapter delete() { 36 | chapter.delete = true; 37 | return chapter; 38 | } 39 | 40 | Chapter getChapter() { 41 | if (chapter == null) { 42 | chapter = Chapter(); 43 | } 44 | chapter.chapterId = chapterIdController.text; 45 | chapter.title = titleController.text; 46 | chapter.timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; 47 | chapter.data = _data; 48 | return chapter; 49 | } 50 | 51 | Future addPage()async{ 52 | var result = await FilePicker.platform.pickFiles(type: FileType.image); 53 | try { 54 | var response = await node.handler.uploadImage(result.files.single); 55 | if (response.statusCode == 200) { 56 | _data.add(response.data['data']['cid']); 57 | notifyListeners(); 58 | } 59 | } catch (e, s) { 60 | FirebaseCrashlytics.instance 61 | .recordError(e, s, reason: 'uploadCoverFailed'); 62 | } 63 | } 64 | 65 | void deletePage(index){ 66 | if (index >= 0 && index < _data.length) { 67 | _data.removeAt(index); 68 | notifyListeners(); 69 | } 70 | } 71 | 72 | List get data => _data; 73 | } 74 | -------------------------------------------------------------------------------- /lib/component/DownloadComicListTile.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:dcomic/view/download_comic_page.dart'; 5 | 6 | class DownloadComicListTile extends StatelessWidget { 7 | final String title; 8 | final String cover; 9 | final String comicId; 10 | 11 | const DownloadComicListTile({Key key, this.title, this.cover, this.comicId}) 12 | : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | // TODO: implement build 17 | return IntrinsicHeight( 18 | child: TextButton( 19 | style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.fromLTRB(1, 0, 1, 0))), 20 | onPressed: () { 21 | Navigator.push(context, MaterialPageRoute(builder: (context) { 22 | return DownloadComicPage(comicId: comicId,title: title,); 23 | },settings: RouteSettings(name: 'download_comic_page'))); 24 | }, 25 | child: Card( 26 | child: Row( 27 | children: [ 28 | CachedNetworkImage( 29 | imageUrl: '$cover', 30 | httpHeaders: {'referer': 'http://images.dmzj.com'}, 31 | progressIndicatorBuilder: (context, url, downloadProgress) => 32 | Center(child: CircularProgressIndicator( 33 | value: downloadProgress.progress),), 34 | errorWidget: (context, url, error) => Icon(Icons.error), 35 | width: 100, 36 | ), 37 | Expanded( 38 | child: Padding( 39 | padding: EdgeInsets.fromLTRB(10, 2, 0, 0), 40 | child: Column( 41 | children: [ 42 | Expanded( 43 | child: Align( 44 | alignment: Alignment.topLeft, 45 | child: Text( 46 | '$title', 47 | style: TextStyle(fontSize: 16), 48 | ), 49 | ), 50 | ), 51 | ], 52 | ), 53 | )) 54 | ], 55 | ), 56 | ), 57 | )); 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /lib/view/login_page.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:dcomic/view/settings/user_setting_page.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 6 | import 'package:dcomic/model/comic_source/sourceProvider.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class LoginPage extends StatefulWidget { 10 | @override 11 | State createState() { 12 | // TODO: implement createState 13 | return _LoginPage(); 14 | } 15 | } 16 | 17 | class _LoginPage extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | // TODO: implement build 21 | return Scaffold( 22 | appBar: AppBar( 23 | title: Text('选择登录账号'), 24 | actions: [ 25 | IconButton( 26 | icon: Icon(Icons.settings), 27 | onPressed: () { 28 | Navigator.of(context).push(MaterialPageRoute( 29 | builder: (context) => UserSettingPage(), 30 | settings: RouteSettings(name: 'user_setting_page'))); 31 | }) 32 | ], 33 | ), 34 | body: ListView.builder( 35 | itemCount: Provider.of(context).activeSources.length, 36 | itemBuilder: (context, index) { 37 | BaseSourceModel model = 38 | Provider.of(context, listen: false) 39 | .activeSources[index]; 40 | return Card( 41 | child: ListTile( 42 | title: Text('${model.type.title}登录'), 43 | subtitle: Text('${model.type.description}'), 44 | enabled: model.userConfig.status == UserStatus.logout, 45 | trailing: Icon(model.userConfig.status == UserStatus.login 46 | ? Icons.cloud_done 47 | : model.userConfig.status == UserStatus.logout 48 | ? Icons.cloud_off 49 | : Icons.cancel), 50 | onTap: () async { 51 | await Navigator.of(context).push(MaterialPageRoute( 52 | builder: (context) => 53 | model.userConfig.getLoginWidget(context), 54 | settings: 55 | RouteSettings(name: '${model.type.name}_login_page'))); 56 | Navigator.of(context).pop(); 57 | }, 58 | ), 59 | ); 60 | }, 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/generated/intl/messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | // Ignore issues from commonly used lints in this file. 6 | // ignore_for_file:implementation_imports, file_names, unnecessary_new 7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering 8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment 9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases 10 | // ignore_for_file:comment_references 11 | 12 | import 'dart:async'; 13 | 14 | import 'package:intl/intl.dart'; 15 | import 'package:intl/message_lookup_by_library.dart'; 16 | import 'package:intl/src/intl_helpers.dart'; 17 | 18 | import 'messages_en.dart' as messages_en; 19 | import 'messages_zh.dart' as messages_zh; 20 | 21 | typedef Future LibraryLoader(); 22 | Map _deferredLibraries = { 23 | 'en': () => new Future.value(null), 24 | 'zh': () => new Future.value(null), 25 | }; 26 | 27 | MessageLookupByLibrary _findExact(String localeName) { 28 | switch (localeName) { 29 | case 'en': 30 | return messages_en.messages; 31 | case 'zh': 32 | return messages_zh.messages; 33 | default: 34 | return null; 35 | } 36 | } 37 | 38 | /// User programs should call this before using [localeName] for messages. 39 | Future initializeMessages(String localeName) async { 40 | var availableLocale = Intl.verifiedLocale( 41 | localeName, 42 | (locale) => _deferredLibraries[locale] != null, 43 | onFailure: (_) => null); 44 | if (availableLocale == null) { 45 | return new Future.value(false); 46 | } 47 | var lib = _deferredLibraries[availableLocale]; 48 | await (lib == null ? new Future.value(false) : lib()); 49 | initializeInternalMessageLookup(() => new CompositeMessageLookup()); 50 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 51 | return new Future.value(true); 52 | } 53 | 54 | bool _messagesExistFor(String locale) { 55 | try { 56 | return _findExact(locale) != null; 57 | } catch (e) { 58 | return false; 59 | } 60 | } 61 | 62 | MessageLookupByLibrary _findGeneratedMessagesFor(String locale) { 63 | var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor, 64 | onFailure: (_) => null); 65 | if (actualLocale == null) return null; 66 | return _findExact(actualLocale); 67 | } 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/model/server_controller/ServerMainPageModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/http/IPFSSourceRequestHandler.dart'; 2 | import 'package:dcomic/model/baseModel.dart'; 3 | import 'package:dcomic/model/comic_source/IPFSSourceProivder.dart'; 4 | import 'package:dio/dio.dart'; 5 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 6 | 7 | class ServerMainPageModel extends BaseModel { 8 | final IPFSSourceModel node; 9 | IPFSSourceRequestHandler _handler; 10 | String _username; 11 | String _nickname; 12 | String _avatar; 13 | List _rights = []; 14 | 15 | String error; 16 | 17 | ServerMainPageModel(this.node) { 18 | _handler = node.handler; 19 | init(); 20 | } 21 | 22 | Future init() async { 23 | try { 24 | var response = await _handler.getUserState(); 25 | if (response.statusCode == 200) { 26 | _username = response.data['data']['username']; 27 | _nickname = response.data['data']['nickname']; 28 | _rights = response.data['data']['rights']; 29 | _avatar = response.data['data']['avatar']; 30 | error = null; 31 | notifyListeners(); 32 | } 33 | } on DioError catch (e, s) { 34 | FirebaseCrashlytics.instance 35 | .recordError(e, s, reason: 'serverLoadingFailed: $address'); 36 | error = e.response.data['msg']; 37 | } catch (e, s) { 38 | FirebaseCrashlytics.instance 39 | .recordError(e, s, reason: 'serverLoadingFailed: $address'); 40 | error = '未知错误:$e'; 41 | } 42 | notifyListeners(); 43 | } 44 | 45 | Future addServer(String address, String token) async { 46 | try { 47 | var response = await _handler.addServer(address, token); 48 | if (response.statusCode == 200) { 49 | return; 50 | } 51 | } catch (e, s) { 52 | FirebaseCrashlytics.instance 53 | .recordError(e, s, reason: 'serverAddFailed: $address'); 54 | } 55 | } 56 | 57 | String get username => _username; 58 | 59 | String get title => node.title; 60 | 61 | String get address => node.address; 62 | 63 | String get name => node.name; 64 | 65 | String get version => node.version; 66 | 67 | int get mode => node.mode; 68 | 69 | String get description => node.description; 70 | 71 | List get rights => _rights; 72 | 73 | String get nickname => _nickname; 74 | 75 | String get avatar => _avatar; 76 | 77 | bool get admin { 78 | for (var item in rights) { 79 | if (item['right_num'] == 1) { 80 | return true; 81 | } 82 | } 83 | return false; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/component/ComicSourceCard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class ComicSourceCard extends StatefulWidget { 7 | final BaseSourceModel model; 8 | 9 | const ComicSourceCard({Key key, this.model}) : super(key: key); 10 | 11 | @override 12 | State createState() { 13 | // TODO: implement createState 14 | return _ComicSourceCard(); 15 | } 16 | } 17 | 18 | class _ComicSourceCard extends State { 19 | @override 20 | Widget build(BuildContext context) { 21 | // TODO: implement build 22 | IconData icon; 23 | switch(widget.model.type.sourceType){ 24 | case SourceType.LocalSource: 25 | // TODO: Handle this case. 26 | icon=Icons.file_download; 27 | break; 28 | case SourceType.LocalDecoderSource: 29 | // TODO: Handle this case. 30 | icon=Icons.phone_android; 31 | break; 32 | case SourceType.CloudDecoderSource: 33 | // TODO: Handle this case. 34 | icon=Icons.cloud; 35 | break; 36 | case SourceType.DeprecatedSource: 37 | // TODO: Handle this case. 38 | icon=Icons.cancel; 39 | break; 40 | } 41 | return ChangeNotifierProvider( 42 | create: (_) => SourceOptionsProvider(widget.model.options), 43 | builder: (context, child) { 44 | return Card( 45 | margin: EdgeInsets.all(5), 46 | child: Wrap( 47 | children: [ 48 | ListTile( 49 | enabled: !widget.model.type.deprecated, 50 | title: Text('${widget.model.type.title}'), 51 | subtitle: Text('${widget.model.type.description}'), 52 | trailing: Switch( 53 | value: Provider.of(context).active, 54 | onChanged: widget.model.type.canDisable&&!widget.model.type.deprecated 55 | ? (value) { 56 | Provider.of(context, 57 | listen: false) 58 | .active = value; 59 | } 60 | : null, 61 | ), 62 | leading: Icon(icon), 63 | ), 64 | Divider(), 65 | widget.model.getSettingWidget(context) 66 | ], 67 | )); 68 | }, 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/database/cookieDatabaseProvider.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/utils/log_output.dart'; 2 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 3 | import 'package:logger/logger.dart'; 4 | import 'package:sqflite/sqflite.dart'; 5 | 6 | import 'databaseCommon.dart'; 7 | 8 | class CookieDatabaseProvider { 9 | final String name; 10 | 11 | 12 | Database _database; 13 | Logger logger = Logger( 14 | filter: ReleaseFilter(), 15 | printer: PrettyPrinter(), 16 | output: ConsoleLogOutput()); 17 | 18 | CookieDatabaseProvider(this.name); 19 | 20 | Future get db async { 21 | if (_database == null) { 22 | _database = await DatabaseCommon.initDatabase(); 23 | } 24 | return _database; 25 | } 26 | 27 | Future insert(String configName, dynamic configValue) async { 28 | var batch = (await db).batch(); 29 | batch.delete("cookies", where: "key='$configName' AND provider='$name'"); 30 | switch (T) { 31 | case String: 32 | batch.insert( 33 | "cookies", {'key': '$configName', 'value': configValue,'provider':name}); 34 | break; 35 | case bool: 36 | batch.insert("cookies", 37 | {'key': '$configName', 'value': configValue ? '1' : '0','provider':name}); 38 | break; 39 | default: 40 | batch.insert("cookies", 41 | {'key': '$configName', 'value': configValue.toString(),'provider':name}); 42 | break; 43 | } 44 | await batch.commit(); 45 | } 46 | 47 | Future get(String configName, {T defaultValue}) async { 48 | var batch = (await db).batch(); 49 | batch.query("cookies", where: "key='$configName' AND provider='$name'"); 50 | var result = await batch.commit()as List; 51 | try { 52 | switch (T) { 53 | case String: 54 | return result.first[0]['value'].toString(); 55 | case bool: 56 | return result.first[0]['value'] == '1'; 57 | case int: 58 | return int.parse(result.first[0]['value']); 59 | case double: 60 | return double.parse(result.first[0]['value']); 61 | default: 62 | return result.first[0]['value']; 63 | } 64 | } catch (e,s) { 65 | FirebaseCrashlytics.instance.recordError(e, s, reason: 'cookieGetFailed:$configName, provider: $name'); 66 | logger.w( 67 | 'action: cookieGetFailed, name: $name, cookieName: $configName, exception: $e'); 68 | } 69 | return defaultValue; 70 | } 71 | 72 | Future getAll()async{ 73 | var batch = (await db).batch(); 74 | batch.query("cookies",where: "provider='$name'"); 75 | return await batch.commit(); 76 | } 77 | } -------------------------------------------------------------------------------- /lib/view/comment_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 5 | import 'package:dcomic/component/EmptyView.dart'; 6 | import 'package:dcomic/component/LoadingCube.dart'; 7 | import 'package:dcomic/component/comic/CommentListTile.dart'; 8 | import 'package:provider/provider.dart'; 9 | import 'package:dcomic/model/comicCommentModel.dart'; 10 | 11 | class CommentPage extends StatefulWidget { 12 | final ComicDetail detail; 13 | 14 | CommentPage({this.detail}); 15 | 16 | @override 17 | State createState() { 18 | // TODO: implement createState 19 | return _CommentPage(); 20 | } 21 | } 22 | 23 | class _CommentPage extends State { 24 | _CommentPage(); 25 | 26 | @override 27 | void initState() { 28 | // TODO: implement initState 29 | super.initState(); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | // TODO: implement build 35 | return ChangeNotifierProvider( 36 | create: (_) => ComicCommentModel(widget.detail), 37 | builder: (context, child) => Scaffold( 38 | appBar: AppBar( 39 | title: Text('评论'), 40 | ), 41 | body: EasyRefresh( 42 | onRefresh: () async { 43 | await Provider.of(context, listen: false) 44 | .refresh(); 45 | return; 46 | }, 47 | firstRefresh: true, 48 | firstRefreshWidget: LoadingCube(), 49 | onLoad: () async { 50 | await Provider.of(context, listen: false).next(); 51 | }, 52 | emptyWidget: Provider.of(context).length == 0||Provider.of(context).error!=null 53 | ? EmptyView(message: Provider.of(context).error,) 54 | : null, 55 | child: ListView.builder( 56 | itemCount: Provider.of(context).length, 57 | itemBuilder: (context, index) { 58 | var comment = 59 | Provider.of(context).data[index]; 60 | return CommentListTile( 61 | avatar: comment.avatar, 62 | content: comment.content, 63 | nickname: comment.nickname, 64 | reply: comment.reply, 65 | timestamp: comment.timestamp, 66 | like: comment.like, 67 | ); 68 | }), 69 | ), 70 | ), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/component/CategoryCard.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:dcomic/view/category_detail_page.dart'; 5 | import 'package:dcomic/view/novel_pages/novel_category_detail_page.dart'; 6 | import 'package:fluttericon/font_awesome5_icons.dart'; 7 | 8 | class CategoryCard extends StatelessWidget { 9 | final String cover; 10 | final String title; 11 | final String tagId; 12 | final BaseSourceModel model; 13 | 14 | CategoryCard({this.cover, this.title, this.tagId, this.model}); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | // TODO: implement build 19 | return TextButton( 20 | style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)), 21 | child: Card( 22 | elevation: 0, 23 | child: new Container( 24 | child: Wrap( 25 | children: [ 26 | ClipRRect( 27 | child: AspectRatio( 28 | child: CachedNetworkImage( 29 | imageUrl: '$cover', 30 | fit: BoxFit.cover, 31 | httpHeaders: {'referer': 'http://images.dmzj.com'}, 32 | progressIndicatorBuilder: 33 | (context, url, downloadProgress) => Center( 34 | child: CircularProgressIndicator( 35 | value: downloadProgress.progress), 36 | ), 37 | errorWidget: (context, url, error) => 38 | Icon(FontAwesome5.folder_open), 39 | ), 40 | aspectRatio: 1, 41 | ), 42 | borderRadius: BorderRadius.circular(5), 43 | ), 44 | Row( 45 | children: [ 46 | Expanded( 47 | child: Text( 48 | '$title', 49 | textAlign: TextAlign.center, 50 | overflow: TextOverflow.ellipsis, 51 | ), 52 | ) 53 | ], 54 | ) 55 | ], 56 | ), 57 | )), 58 | onPressed: () { 59 | Navigator.of(context).push(MaterialPageRoute( 60 | builder: (context) { 61 | return CategoryDetailPage( 62 | categoryId: tagId, 63 | title: title, 64 | model: model, 65 | ); 66 | }, 67 | settings: RouteSettings(name: 'category_detail_page'))); 68 | }, 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/model/databaseDetailModel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dcomic/component/EmptyView.dart'; 4 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:dcomic/component/DataBaseDefineTile.dart'; 7 | import 'package:dcomic/component/DataBaseTable.dart'; 8 | import 'package:dcomic/database/databaseCommon.dart'; 9 | import 'package:dcomic/model/baseModel.dart'; 10 | import 'package:sqflite/sqflite.dart'; 11 | 12 | class DatabaseDetailModel extends BaseModel{ 13 | Database _database; 14 | String path; 15 | int version; 16 | Map>> data= {}; 17 | 18 | 19 | DatabaseDetailModel(){ 20 | init(); 21 | } 22 | 23 | Future init()async{ 24 | _database=await DatabaseCommon.initDatabase(); 25 | path=_database.path; 26 | version=await _database.getVersion(); 27 | for(var element in tabs){ 28 | if(DatabaseCommon.databases[element].dropVersion==null||DatabaseCommon.databases[element].dropVersion>version){ 29 | data[element]=await _database.query(element); 30 | } 31 | } 32 | notifyListeners(); 33 | } 34 | 35 | List getTabs(context){ 36 | return tabs.map((e) => Tab(text: '$e',)).toList(); 37 | } 38 | 39 | Future backupDatabase(String path)async{ 40 | try{ 41 | var file=File(this.path); 42 | var directory = new Directory('$path'); 43 | directory.createSync(recursive: true); 44 | String save='${directory.path}/dmzj.db'; 45 | await file.copy(save); 46 | return save; 47 | }catch(e,s){ 48 | FirebaseCrashlytics.instance.recordError(e, s, reason: 'backupDatabaseFailed'); 49 | logger.e('class: DatabaseDetailModel, action: moveFileFailed, exception: $e'); 50 | } 51 | return null; 52 | } 53 | 54 | Future recoverDatabase(String path)async{ 55 | try{ 56 | var file=File(path); 57 | if(await file.exists()){ 58 | await file.copy(this.path); 59 | return true; 60 | } 61 | }catch(e,s){ 62 | FirebaseCrashlytics.instance.recordError(e, s, reason: 'recoverDatabaseFailed'); 63 | logger.e('class: DatabaseDetailModel, action: moveFileFailed, exception: $e'); 64 | } 65 | return false; 66 | } 67 | 68 | List getTabViews(context){ 69 | return tabs.map((e) =>DatabaseCommon.databases[e].dropVersion==null||DatabaseCommon.databases[e].dropVersion>version?DataBaseTable(headers:DatabaseCommon.databases[e].tables.keys.toList(),data: data[e],table: e,):EmptyView(message: '该表已弃用,不做显示',)).toList(); 70 | } 71 | 72 | Widget getDatabaseDefine(context,index){ 73 | var e=tabs[index]; 74 | return DatabaseDefineTile(model: DatabaseCommon.databases[e],table: e,); 75 | } 76 | 77 | List get tabs=>DatabaseCommon.databases.keys.toList(); 78 | } -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | def keystorePropertiesFile = rootProject.file("test.properties") 28 | def keystoreProperties = new Properties() 29 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 30 | 31 | android { 32 | compileSdkVersion 31 33 | 34 | sourceSets { 35 | main.java.srcDirs += 'src/main/kotlin' 36 | } 37 | 38 | lintOptions { 39 | disable 'InvalidPackage' 40 | } 41 | 42 | defaultConfig { 43 | applicationId "top.hanerx.flutterdmzj" 44 | minSdkVersion 26 45 | targetSdkVersion 31 46 | versionCode flutterVersionCode.toInteger() 47 | versionName flutterVersionName 48 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 49 | } 50 | 51 | signingConfigs { 52 | release { 53 | keyAlias keystoreProperties['keyAlias'] 54 | keyPassword keystoreProperties['keyPassword'] 55 | storeFile file(keystoreProperties['storeFile']) 56 | storePassword keystoreProperties['storePassword'] 57 | } 58 | } 59 | buildTypes { 60 | release { 61 | signingConfig signingConfigs.release 62 | minifyEnabled true 63 | useProguard true 64 | 65 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 66 | 67 | } 68 | } 69 | } 70 | 71 | flutter { 72 | source '../..' 73 | } 74 | 75 | dependencies { 76 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 77 | testImplementation 'junit:junit:4.12' 78 | androidTestImplementation 'androidx.test:runner:1.1.1' 79 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 80 | } 81 | apply plugin: 'com.google.gms.google-services' 82 | apply plugin: 'com.google.firebase.crashlytics' 83 | apply plugin: 'com.google.firebase.firebase-perf' -------------------------------------------------------------------------------- /assets/js/lzString.js: -------------------------------------------------------------------------------- 1 | var LZString=(function(){var f=String.fromCharCode;var keyStrBase64="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";var baseReverseDic={};function getBaseValue(alphabet,character){if(!baseReverseDic[alphabet]){baseReverseDic[alphabet]={};for(var i=0;i>=1;if(data.position==0){data.position=resetValue;data.val=getNextValue(data.index++)}bits|=(resb>0?1:0)*power;power<<=1}switch(next=bits){case 0:bits=0;maxpower=Math.pow(2,8);power=1;while(power!=maxpower){resb=data.val&data.position;data.position>>=1;if(data.position==0){data.position=resetValue;data.val=getNextValue(data.index++)}bits|=(resb>0?1:0)*power;power<<=1}c=f(bits);break;case 1:bits=0;maxpower=Math.pow(2,16);power=1;while(power!=maxpower){resb=data.val&data.position;data.position>>=1;if(data.position==0){data.position=resetValue;data.val=getNextValue(data.index++)}bits|=(resb>0?1:0)*power;power<<=1}c=f(bits);break;case 2:return""}dictionary[3]=c;w=c;result.push(c);while(true){if(data.index>length){return""}bits=0;maxpower=Math.pow(2,numBits);power=1;while(power!=maxpower){resb=data.val&data.position;data.position>>=1;if(data.position==0){data.position=resetValue;data.val=getNextValue(data.index++)}bits|=(resb>0?1:0)*power;power<<=1}switch(c=bits){case 0:bits=0;maxpower=Math.pow(2,8);power=1;while(power!=maxpower){resb=data.val&data.position;data.position>>=1;if(data.position==0){data.position=resetValue;data.val=getNextValue(data.index++)}bits|=(resb>0?1:0)*power;power<<=1}dictionary[dictSize++]=f(bits);c=dictSize-1;enlargeIn--;break;case 1:bits=0;maxpower=Math.pow(2,16);power=1;while(power!=maxpower){resb=data.val&data.position;data.position>>=1;if(data.position==0){data.position=resetValue;data.val=getNextValue(data.index++)}bits|=(resb>0?1:0)*power;power<<=1}dictionary[dictSize++]=f(bits);c=dictSize-1;enlargeIn--;break;case 2:return result.join('')}if(enlargeIn==0){enlargeIn=Math.pow(2,numBits);numBits++}if(dictionary[c]){entry=dictionary[c]}else{if(c===dictSize){entry=w+w.charAt(0)}else{return null}}result.push(entry);dictionary[dictSize++]=w+entry.charAt(0);enlargeIn--;w=entry;if(enlargeIn==0){enlargeIn=Math.pow(2,numBits);numBits++}}}};return LZString})();String.prototype.splic=function(f){return LZString.decompressFromBase64(this).split(f)}; -------------------------------------------------------------------------------- /lib/view/server_controllers/user_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/component/EmptyView.dart'; 2 | import 'package:dcomic/component/LoadingCube.dart'; 3 | import 'package:dcomic/model/comic_source/IPFSSourceProivder.dart'; 4 | import 'package:dcomic/model/server_controller/NewComicDetailModel.dart'; 5 | import 'package:dcomic/model/server_controller/UserListModel.dart'; 6 | import 'package:dcomic/view/server_controllers/server_user_detail_page.dart'; 7 | import 'package:flutter/cupertino.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 10 | import 'package:provider/provider.dart'; 11 | 12 | class UserListPage extends StatefulWidget { 13 | final IPFSSourceModel node; 14 | 15 | const UserListPage({Key key, this.node}) : super(key: key); 16 | 17 | @override 18 | State createState() { 19 | // TODO: implement createState 20 | return _UserListPage(); 21 | } 22 | } 23 | 24 | class _UserListPage extends State { 25 | @override 26 | Widget build(BuildContext context) { 27 | // TODO: implement build 28 | return ChangeNotifierProvider( 29 | create: (_) => UserListModel(widget.node), 30 | builder: (context, child) => Scaffold( 31 | appBar: AppBar( 32 | title: Text('用户列表'), 33 | ), 34 | body: EasyRefresh( 35 | firstRefreshWidget: LoadingCube(), 36 | firstRefresh: true, 37 | onRefresh: () async { 38 | await Provider.of(context, listen: false).init(); 39 | }, 40 | emptyWidget: Provider.of(context).data.length == 0 41 | ? EmptyView() 42 | : null, 43 | child: ListView.builder( 44 | itemCount: Provider.of(context).data.length, 45 | itemBuilder: (context, index) { 46 | var item = Provider.of(context).data[index]; 47 | return ListTile( 48 | leading: Icon(Icons.account_circle), 49 | title: Text('${item.nickname}'), 50 | subtitle: Text( 51 | '用户名:${item.username} 权限:${item.admin ? '管理员' : "一般用户"}\n头像CID:${item.avatar}'), 52 | onTap: () { 53 | Navigator.of(context).push(MaterialPageRoute( 54 | builder: (context) => ServerUserDetailPage( 55 | node: widget.node, 56 | userId: item.username, 57 | mode: EditMode.edit, 58 | ), 59 | settings: 60 | RouteSettings(name: 'server_user_detail_page'))); 61 | }, 62 | ); 63 | }), 64 | ), 65 | ), 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/view/favorite_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/component/EmptyView.dart'; 2 | import 'package:dcomic/model/comic_source/sourceProvider.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:dcomic/model/systemSettingModel.dart'; 6 | import 'package:dcomic/view/favorite/comic_favorite_page.dart'; 7 | import 'package:dcomic/view/favorite/novel_favorite_page.dart'; 8 | import 'package:dcomic/view/favorite/tracker_favorite_page.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | class FavoritePage extends StatefulWidget { 12 | FavoritePage(); 13 | 14 | @override 15 | State createState() { 16 | // TODO: implement createState 17 | return _FavoritePage(); 18 | } 19 | } 20 | 21 | class _FavoritePage extends State { 22 | _FavoritePage(); 23 | 24 | @override 25 | initState() { 26 | super.initState(); 27 | } 28 | 29 | // @override 30 | // void deactivate() { 31 | // super.deactivate(); 32 | // var bool = ModalRoute.of(context).isCurrent; 33 | // if (bool) { 34 | // Future.delayed(Duration(milliseconds: 200)).then((e) { 35 | // if (mounted) { 36 | // setState(() { 37 | // page = 0; 38 | // refreshState = true; 39 | // }); 40 | // getSubscribe(); 41 | // } 42 | // }); 43 | // } 44 | // } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | List tabs = Provider.of(context, listen: false) 49 | .favoriteSources 50 | .map((e) => Tab( 51 | text: e.type.title, 52 | )) 53 | .toList(); 54 | List list = Provider.of(context, listen: false) 55 | .favoriteSources 56 | .map((e) => ComicFavoritePage( 57 | model: e, 58 | )) 59 | .toList(); 60 | 61 | if (Provider.of(context, listen: false).blackBox) { 62 | tabs.add(Tab( 63 | text: '黑匣子', 64 | )); 65 | list.add(TrackerFavoritePage()); 66 | } 67 | if (Provider.of(context, listen: false).novel) { 68 | tabs.add(Tab( 69 | text: '轻小说', 70 | )); 71 | list.add(NovelFavoritePage( 72 | uid: Provider.of(context) 73 | .activeSources 74 | .first 75 | .userConfig 76 | .userId)); 77 | } 78 | 79 | // TODO: implement build 80 | return DefaultTabController( 81 | length: tabs.length, 82 | child: Scaffold( 83 | appBar: AppBar( 84 | title: Text('我的订阅'), 85 | bottom: TabBar( 86 | isScrollable: true, 87 | tabs: tabs, 88 | ), 89 | ), 90 | body: TabBarView( 91 | children: list, 92 | ), 93 | ), 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/database/historyDatabaseProvider.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/database/databaseCommon.dart'; 2 | import 'package:dcomic/utils/log_output.dart'; 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | import 'package:logger/logger.dart'; 5 | import 'package:sqflite/sqflite.dart'; 6 | 7 | class HistoryDatabaseProvider { 8 | final String name; 9 | 10 | Database _database; 11 | Logger logger = Logger( 12 | filter: ReleaseFilter(), 13 | printer: PrettyPrinter(), 14 | output: ConsoleLogOutput()); 15 | 16 | HistoryDatabaseProvider(this.name); 17 | 18 | Future get db async { 19 | if (_database == null) { 20 | _database = await DatabaseCommon.initDatabase(); 21 | } 22 | return _database; 23 | } 24 | 25 | Future addReadHistory(String comicId, String title, String cover, 26 | String lastChapter, String lastChapterId, int timestamp) async { 27 | var batch = (await db).batch(); 28 | batch.delete('local_history', 29 | where: "comicId = ? AND provider = ?", whereArgs: [comicId, name]); 30 | batch.insert("local_history", { 31 | 'comicId': comicId, 32 | 'title': title, 33 | 'cover': cover, 34 | 'last_chapter': lastChapter, 35 | 'last_chapter_id': lastChapterId, 36 | 'timestamp': timestamp, 37 | 'provider': name 38 | }); 39 | await batch.commit(); 40 | } 41 | 42 | Future getReadHistories() async { 43 | var batch = (await db).batch(); 44 | batch.query("local_history", where: 'provider = ?', whereArgs: [name]); 45 | return await batch.commit(); 46 | } 47 | 48 | Future getReadHistory(String comicId) async { 49 | try{ 50 | var batch = (await db).batch(); 51 | batch.query("local_history", 52 | where: 'comicId = ? AND provider = ?', whereArgs: [comicId, name]); 53 | var data=(await batch.commit() as List).first; 54 | return data[0]; 55 | }catch(e,s){ 56 | FirebaseCrashlytics.instance.recordError(e, s, reason: 'readHistoryGetFailed: $comicId, provider: $name'); 57 | } 58 | return null; 59 | } 60 | 61 | Future addUnread(String comicId, int timestamp) async { 62 | var batch = (await db).batch(); 63 | batch.delete("unread", 64 | where: "comicId = ? AND provider = ?", whereArgs: [comicId, name]); 65 | batch.insert('unread', 66 | {'comicId': comicId, 'timestamp': timestamp, 'provider': name}); 67 | await batch.commit(); 68 | } 69 | 70 | Future getUnread(String comicId) async { 71 | var batch = (await db).batch(); 72 | batch.query('unread', 73 | where: 'comicId = ? AND provider = ?', whereArgs: [comicId, name]); 74 | return (await batch.commit()).first; 75 | } 76 | 77 | Future getAllUnread() async { 78 | var batch = (await db).batch(); 79 | batch.query('unread', where: 'provider = ?', whereArgs: [name]); 80 | return await batch.commit(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/component/history_tab/LocalHistoryTab.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 2 | import 'package:dcomic/model/comic_source/sourceProvider.dart'; 3 | import 'package:dcomic/model/localHistoryModel.dart'; 4 | import 'package:dcomic/view/comic_detail_page.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 8 | import 'package:dcomic/component/EmptyView.dart'; 9 | import 'package:dcomic/component/LoadingCube.dart'; 10 | import 'package:provider/provider.dart'; 11 | 12 | import '../comic/HistoryListTile.dart'; 13 | 14 | class LocalHistoryTab extends StatefulWidget { 15 | @override 16 | State createState() { 17 | // TODO: implement createState 18 | return _LocalHistoryTab(); 19 | } 20 | } 21 | 22 | class _LocalHistoryTab extends State { 23 | @override 24 | void deactivate() { 25 | super.deactivate(); 26 | } 27 | 28 | @override 29 | void initState() { 30 | // TODO: implement initState 31 | super.initState(); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | // TODO: implement build 37 | return ChangeNotifierProvider( 38 | create: (_) => LocalHistoryModel(Provider.of(context)), 39 | builder: (context, child) { 40 | return EasyRefresh( 41 | scrollController: ScrollController(), 42 | child: ListView.builder( 43 | itemCount: Provider.of(context).list.length, 44 | shrinkWrap: true, 45 | itemBuilder: (context, index) { 46 | HistoryComic comic = 47 | Provider.of(context).list[index]; 48 | return HistoryListTile( 49 | cover: comic.cover, 50 | chapterName: comic.latestChapter, 51 | date: comic.timestamp, 52 | title: comic.title, 53 | provider: comic.model.type.title, 54 | onPressed: () { 55 | Navigator.of(context).push(MaterialPageRoute( 56 | builder: (context) => ComicDetailPage( 57 | id: comic.comicId, 58 | title: comic.title, 59 | model: comic.model, 60 | ),settings: RouteSettings(name: 'comic_detail_page'))); 61 | }, 62 | ); 63 | }), 64 | onRefresh: () async { 65 | await Provider.of(context, listen: false) 66 | .refresh(); 67 | }, 68 | firstRefreshWidget: LoadingCube(), 69 | firstRefresh: true, 70 | emptyWidget: Provider.of(context).list.length == 0 71 | ? EmptyView() 72 | : null, 73 | ); 74 | }, 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/component/search/SearchTab.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/component/comic/SearchListTile.dart'; 2 | import 'package:dcomic/model/comicSearchModel.dart'; 3 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 4 | import 'package:dcomic/view/comic_detail_page.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | import '../EmptyView.dart'; 11 | import '../LoadingCube.dart'; 12 | 13 | class SearchTab extends StatefulWidget { 14 | final String keyword; 15 | final Key key; 16 | final BaseSourceModel model; 17 | 18 | SearchTab({this.key, @required this.keyword, this.model}); 19 | 20 | @override 21 | State createState() { 22 | // TODO: implement createState 23 | return _SearchTab(); 24 | } 25 | } 26 | 27 | class _SearchTab extends State { 28 | List list = []; 29 | int page = 0; 30 | bool refreshState = false; 31 | 32 | _SearchTab(); 33 | 34 | @override 35 | void initState() { 36 | // TODO: implement initState 37 | super.initState(); 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | // TODO: implement build 43 | return ChangeNotifierProvider( 44 | create: (_) => ComicSearchModel(widget.model, widget.keyword), 45 | builder: (context, child) => EasyRefresh( 46 | scrollController: ScrollController(), 47 | firstRefreshWidget: LoadingCube(), 48 | firstRefresh: true, 49 | onRefresh: () async { 50 | await Provider.of(context, listen: false).refresh(); 51 | }, 52 | onLoad: () async { 53 | await Provider.of(context, listen: false).next(); 54 | }, 55 | emptyWidget: Provider.of(context).length == 0 56 | ? EmptyView() 57 | : null, 58 | child: ListView.builder( 59 | itemCount: Provider.of(context).length, 60 | itemBuilder: (context, index) { 61 | var comic = Provider.of(context, listen: false) 62 | .list[index]; 63 | return SearchListTile( 64 | cover: comic.cover, 65 | title: comic.title, 66 | authors: comic.author, 67 | tag: comic.tag, 68 | latest: comic.latestChapter, 69 | onPressed: () { 70 | Navigator.of(context).push(MaterialPageRoute( 71 | builder: (context) => ComicDetailPage( 72 | id: comic.comicId, 73 | title: comic.title, 74 | model: widget.model, 75 | ), 76 | settings: RouteSettings(name: 'comic_detail_page'))); 77 | }, 78 | ); 79 | }, 80 | ), 81 | ), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/component/AuthorCard.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:dcomic/view/comic_detail_page.dart'; 6 | 7 | class AuthorCard extends StatelessWidget { 8 | final String imageUrl; 9 | final String title; 10 | final String subtitle; 11 | final String id; 12 | final BaseSourceModel model; 13 | 14 | AuthorCard({this.imageUrl, this.title, this.subtitle, this.id, this.model}); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | // TODO: implement build 19 | return TextButton( 20 | style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(1))), 21 | child: Card( 22 | elevation: 0, 23 | child: Container( 24 | child: Wrap( 25 | alignment: WrapAlignment.center, 26 | children: [ 27 | ClipRRect( 28 | child: AspectRatio( 29 | aspectRatio: 0.75, 30 | child: CachedNetworkImage( 31 | imageUrl: '$imageUrl', 32 | httpHeaders: {'referer': 'http://images.dmzj.com'}, 33 | progressIndicatorBuilder: 34 | (context, url, downloadProgress) => Center( 35 | child: CircularProgressIndicator( 36 | value: downloadProgress.progress), 37 | ), 38 | errorWidget: (context, url, error) => Icon(Icons.error), 39 | ), 40 | ), 41 | borderRadius: BorderRadius.circular(5), 42 | ), 43 | Row( 44 | children: [ 45 | Expanded( 46 | child: Text( 47 | '$title', 48 | maxLines: 1, 49 | overflow: TextOverflow.ellipsis, 50 | textAlign: TextAlign.center, 51 | ), 52 | ) 53 | ], 54 | ), 55 | Text( 56 | '$subtitle', 57 | style: TextStyle(color: Colors.grey, fontSize: 14), 58 | textDirection: TextDirection.ltr, 59 | textAlign: TextAlign.start, 60 | overflow: TextOverflow.ellipsis, 61 | ) 62 | ], 63 | ), 64 | ), 65 | ), 66 | onPressed: () { 67 | Navigator.push( 68 | context, 69 | MaterialPageRoute( 70 | builder: (context) { 71 | return ComicDetailPage( 72 | id: id.toString(), 73 | title: title, 74 | model: model, 75 | ); 76 | }, 77 | settings: RouteSettings(name: 'comic_detail_page'))); 78 | }, 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/http/ManHuaGuiRequestHandler.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:dcomic/http/UniversalRequestModel.dart'; 3 | 4 | class ManHuaGuiRequestHandler extends CookiesRequestHandler { 5 | ManHuaGuiRequestHandler() : super('manhuagui', 'https://m.manhuagui.com'); 6 | 7 | Future search(String keyword) { 8 | return dio.get('/s/${Uri.encodeComponent(keyword)}.html'); 9 | } 10 | 11 | Future getComic(String comicId) { 12 | return dio.get('/comic/$comicId/'); 13 | } 14 | 15 | Future getChapter(String comicId, String chapterId) { 16 | return dio.get('/comic/$comicId/$chapterId.html'); 17 | } 18 | 19 | Future getComicComments(String comicId, int page) { 20 | return dio.get( 21 | '/tools/submit_ajax.ashx?action=comment_list&book_id=$comicId&page_index=${page + 1}'); 22 | } 23 | 24 | Future addSubscribe(String comicId) async { 25 | var data = FormData.fromMap({'book_id': comicId}); 26 | return dio.post('/tools/submit_ajax.ashx?action=user_book_shelf_add', 27 | data: data, options: await setHeader()); 28 | } 29 | 30 | Future cancelSubscribe(String comicId) async { 31 | var data = FormData.fromMap({'book_id': comicId}); 32 | return dio.post('/tools/submit_ajax.ashx?action=user_book_shelf_delete', 33 | data: data, options: await setHeader()); 34 | } 35 | 36 | Future getIfSubscribe(String comicId) async { 37 | return dio.get( 38 | '/tools/submit_ajax.ashx?action=user_book_shelf_check&book_id=$comicId', 39 | options: await setHeader()); 40 | } 41 | 42 | Future logout() async { 43 | return dio.get('/user/center/exit', options: await setHeader()); 44 | } 45 | 46 | Future login(String username, String password) { 47 | var data = FormData.fromMap({ 48 | 'txtUserName': username, 49 | 'txtPassword': password, 50 | 'chkRemember': 'checked' 51 | }); 52 | return dio.post('/tools/submit_ajax.ashx?action=user_login', data: data); 53 | } 54 | 55 | Future getLoginInfo() async { 56 | return dio.get('/tools/submit_ajax.ashx?action=user_check_login', 57 | options: await setHeader()); 58 | } 59 | 60 | Future getSubscribe() async { 61 | return dio.get('/user/book/shelf', options: await setHeader()); 62 | } 63 | 64 | Future getCategoryDetail(String categoryId, {int page: 0,bool popular:true}) { 65 | return dio.get('/list/$categoryId/${popular?"view.html":"update.html"}?page=${page + 1}'); 66 | } 67 | 68 | Future getAuthor(String authorId) { 69 | return dio.get('/author/$authorId/'); 70 | } 71 | 72 | Future getLatestUpdate({int page: 0}) { 73 | return dio.get('/update/?page=${page + 1}'); 74 | } 75 | 76 | Future getRankingList({int page: 0}) { 77 | return dio.get('/rank/?page=${page + 1}'); 78 | } 79 | 80 | Future getHompage() { 81 | return dio.get('/'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/view/comic_pages/subject_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/component/Card.dart'; 2 | import 'package:dcomic/component/EmptyView.dart'; 3 | import 'package:dcomic/component/LoadingCube.dart'; 4 | import 'package:dcomic/model/comic_source/baseSourceModel.dart'; 5 | import 'package:dcomic/model/subjectListModel.dart'; 6 | import 'package:dcomic/view/subject_detail_page.dart'; 7 | import 'package:flutter/cupertino.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 10 | import 'package:provider/provider.dart'; 11 | 12 | class SubjectListPage extends StatefulWidget { 13 | final BaseSourceModel model; 14 | 15 | const SubjectListPage({Key key, this.model}) : super(key: key); 16 | 17 | @override 18 | State createState() { 19 | // TODO: implement createState 20 | return _SubjectListPage(); 21 | } 22 | } 23 | 24 | class _SubjectListPage extends State { 25 | @override 26 | Widget build(BuildContext context) { 27 | // TODO: implement build 28 | return ChangeNotifierProvider( 29 | create: (_) => SubjectListModel(widget.model), 30 | builder: (context, child) => Scaffold( 31 | appBar: AppBar( 32 | title: Text('专题'), 33 | ), 34 | body: EasyRefresh( 35 | emptyWidget: Provider.of(context).length == 0 || 36 | Provider.of(context).error != null 37 | ? EmptyView( 38 | message: Provider.of(context).error, 39 | ) 40 | : null, 41 | onLoad: () async { 42 | await Provider.of(context, listen: false).next(); 43 | }, 44 | onRefresh: () async { 45 | await Provider.of(context, listen: false) 46 | .refresh(); 47 | }, 48 | firstRefreshWidget: LoadingCube(), 49 | firstRefresh: true, 50 | child: ListView.builder( 51 | shrinkWrap: true, 52 | physics: NeverScrollableScrollPhysics(), 53 | itemCount: Provider.of(context).length, 54 | itemBuilder: (context, index) { 55 | var item = Provider.of(context, listen: false) 56 | .data[index]; 57 | return HomePageCard( 58 | imageUrl: item.cover, 59 | title: item.title, 60 | subtitle: item.subtitle, 61 | onPressed: (context) => Navigator.of(context).push( 62 | MaterialPageRoute( 63 | builder: (context) => SubjectDetailPage( 64 | subjectId: item.subjectId, 65 | model: item.model, 66 | ), 67 | settings: 68 | RouteSettings(name: 'subject_detail_page'))), 69 | ); 70 | }), 71 | ), 72 | ), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/component/comic_viewer/Tips.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:battery/battery.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:connectivity/connectivity.dart'; 7 | import 'package:dcomic/component/Battery.dart'; 8 | 9 | class Tips extends StatefulWidget{ 10 | final String title; 11 | final int index; 12 | final int length; 13 | 14 | const Tips({Key key, this.title, this.index, this.length}) : super(key: key); 15 | 16 | @override 17 | State createState() { 18 | // TODO: implement createState 19 | return _Tip(); 20 | } 21 | 22 | } 23 | 24 | class _Tip extends State{ 25 | 26 | StreamSubscription _subscription; 27 | String _network='未知网络'; 28 | double _batteryState=0.8; 29 | 30 | @override 31 | void initState() { 32 | // TODO: implement initState 33 | super.initState(); 34 | initNetwork(); 35 | initBattery(); 36 | } 37 | 38 | initNetwork()async{ 39 | var connectivityResult = await (Connectivity().checkConnectivity()); 40 | if(mounted){ 41 | setState(() { 42 | if (connectivityResult == ConnectivityResult.mobile) { 43 | _network='移动网络'; 44 | } else if (connectivityResult == ConnectivityResult.wifi) { 45 | _network='WIFI'; 46 | } 47 | }); 48 | } 49 | _subscription = Connectivity().onConnectivityChanged.listen((ConnectivityResult result) { 50 | if(mounted){ 51 | setState(() { 52 | if (result == ConnectivityResult.mobile) { 53 | _network='移动网络'; 54 | } else if (result == ConnectivityResult.wifi) { 55 | _network='WIFI'; 56 | } 57 | }); 58 | } 59 | }); 60 | } 61 | 62 | initBattery()async{ 63 | try{ 64 | var battery = Battery(); 65 | int state=await battery.batteryLevel; 66 | if(mounted){ 67 | setState((){ 68 | _batteryState=(state)/100; 69 | }); 70 | Future.delayed(Duration(seconds: 20)).then((value) => initBattery()); 71 | } 72 | }catch(e){ 73 | print(e); 74 | } 75 | } 76 | 77 | @override 78 | void dispose() { 79 | // TODO: implement dispose 80 | super.dispose(); 81 | _subscription.cancel(); 82 | } 83 | 84 | @override 85 | Widget build(BuildContext context) { 86 | // TODO: implement build 87 | return Container( 88 | decoration: BoxDecoration( 89 | borderRadius: BorderRadius.only(topLeft: Radius.circular(4.0)), 90 | //设置四周边框 91 | border: new Border.all(width: 1, color: Colors.black54), 92 | color: Colors.black54 93 | ), 94 | padding: EdgeInsets.fromLTRB(10, 2, 10, 2), 95 | child: Row( 96 | children: [ 97 | Text('${widget.title} ${widget.index}/${widget.length} $_network ',style: TextStyle(color: Colors.white),), 98 | BatteryView(electricQuantity:_batteryState,) 99 | ], 100 | ), 101 | ); 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/view/mag_maker/new_mag_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:dcomic/view/mag_maker/mag_guide_page.dart'; 5 | import 'package:fluttericon/font_awesome5_icons.dart'; 6 | 7 | class NewMagPage extends StatefulWidget { 8 | @override 9 | State createState() { 10 | // TODO: implement createState 11 | return _NewMagPage(); 12 | } 13 | } 14 | 15 | class _NewMagPage extends State { 16 | @override 17 | Widget build(BuildContext context) { 18 | // TODO: implement build 19 | return Scaffold( 20 | appBar: AppBar( 21 | title: Text('新建漫画'), 22 | ), 23 | body: ListView( 24 | children: [ 25 | // FlatButton( 26 | // padding: EdgeInsets.all(5), 27 | // child: Card( 28 | // child: ListTile( 29 | // leading: Icon(FontAwesome5.code), 30 | // trailing: Icon(Icons.chevron_right), 31 | // title: Text('内置编辑器'), 32 | // subtitle: Text( 33 | // '使用内置编辑器直接对code进行编程,功能齐全且不受encoder或decoder限制,但需要有一定的json编辑能力'), 34 | // ), 35 | // ), 36 | // onPressed: () {}, 37 | // ), 38 | FlatButton( 39 | padding: EdgeInsets.all(5), 40 | child: Card( 41 | child: ListTile( 42 | leading: Icon(FontAwesome5.tasks), 43 | trailing: Icon(Icons.chevron_right), 44 | title: Text('构建向导'), 45 | subtitle: Text( 46 | '使用构建向导进行构建'), 47 | ), 48 | ), 49 | onPressed: () { 50 | 51 | }, 52 | ), 53 | // FlatButton( 54 | // padding: EdgeInsets.all(5), 55 | // child: Card( 56 | // child: ListTile( 57 | // leading: Icon(FontAwesome5.folder_open), 58 | // trailing: Icon(Icons.chevron_right), 59 | // title: Text('从文件中打开'), 60 | // subtitle: Text('打开一个已存在的.manga文件并使用该文件进行编辑'), 61 | // ), 62 | // ), 63 | // onPressed: () {}, 64 | // ), 65 | // FlatButton( 66 | // padding: EdgeInsets.all(5), 67 | // child: Card( 68 | // child: ListTile( 69 | // leading: Icon(FontAwesome5.flag), 70 | // trailing: Icon(Icons.chevron_right), 71 | // title: Text('新手教程'), 72 | // subtitle: Text('打开一次新手教程,用最不清晰的流程带你看一遍怎么新建一个.manga文件'), 73 | // ), 74 | // ), 75 | // onPressed: ()async { 76 | // var chapter=GuideChapter(); 77 | // await chapter.initFromString(await rootBundle.loadString('assets/guide/chapter0/main.json')); 78 | // Navigator.of(context).push(MaterialPageRoute( 79 | // builder: (context) => MagGuidePage( 80 | // guide: chapter 81 | // ),settings: RouteSettings(name: 'mag_guide_page'))); 82 | // }, 83 | // ), 84 | ], 85 | ), 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /assets/guide/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test_manga", 3 | "version": "1.0.0-beta.1", 4 | "title": "测试用漫画", 5 | "alias": [ 6 | "简单学会怎么制作.manga", 7 | "how to make .manga file" 8 | ], 9 | "description": "简单学会怎么制作.manga格式的漫画", 10 | "authors": [ 11 | { 12 | "decoder": null, 13 | "data": { 14 | "name": "hanerx" 15 | } 16 | }, 17 | "someone" 18 | ], 19 | "data": [ 20 | { 21 | "name": "default", 22 | "title": "连载", 23 | "data": [ 24 | { 25 | "decoder": "url_decoder", 26 | "timestamp": 10205605364, 27 | "name": "chapter1", 28 | "order": 1, 29 | "title": "第一章-网络图片", 30 | "data": [ 31 | "https://something.com/1.jpg", 32 | "https://something.com/2.jpg" 33 | ], 34 | "headers": { 35 | "referer": "https://img.something.com" 36 | } 37 | }, 38 | { 39 | "decoder": "local_decoder", 40 | "encoder": "dmzj_downloader", 41 | "timestamp": 10205605364, 42 | "order": 2, 43 | "name": "chapter2", 44 | "title": "第二章-本地图片", 45 | "data": [ 46 | "./data/chapter2/1.webp" 47 | ], 48 | "comment_trackers": [ 49 | 50 | ] 51 | }, 52 | { 53 | "decoder": "bt_decoder", 54 | "timestamp": 10205605364, 55 | "order": 3, 56 | "name": "chapter3", 57 | "title": "第三章-bt文件", 58 | "data": { 59 | "type": "file", 60 | "path": "./data/chapter3/bt.torrent" 61 | } 62 | }, 63 | { 64 | "decoder": "bt_decoder", 65 | "timestamp": 10205605364, 66 | "order": 4, 67 | "name": "chapter4", 68 | "title": "第四章-磁力链接", 69 | "data": { 70 | "type": "magnet", 71 | "path": "magnet:?xt=urn:btih:somethingbabababa" 72 | } 73 | } 74 | ] 75 | }, 76 | { 77 | "name": "" 78 | } 79 | ], 80 | "translate": true, 81 | "language": "zh-cn", 82 | "origin_language": "zh-cn", 83 | "md5": "we4t23465yhtsgrfcsvchef", 84 | "translators": [ 85 | { 86 | "name": "someone", 87 | "url": "https://github.com/hanerx", 88 | "description": "someone is none" 89 | }, 90 | "hanerx" 91 | ], 92 | "trackers": [ 93 | { 94 | "decoder": "dmzj_decoder", 95 | "url": "https://something.com/trackers/test_manga" 96 | }, 97 | { 98 | "decoder": "default", 99 | "url": "https://something.com/tracker", 100 | "options": { 101 | "login": true, 102 | "token": "xoaeigjdfisauehglalfdk" 103 | } 104 | } 105 | ], 106 | "comment_trackers": [ 107 | { 108 | "decoder": "dmzj_comment_decoder", 109 | "url": "https://something.com/comment/test_manga", 110 | "headers": { 111 | "x-token": "something" 112 | } 113 | } 114 | ], 115 | "tags": [ 116 | { 117 | "name": "工具" 118 | }, 119 | { 120 | "decoder": "dmzj_tag_decoder", 121 | "name": "冒险", 122 | "tag_id": 12345 123 | } 124 | ] 125 | } -------------------------------------------------------------------------------- /lib/protobuf/comic.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package dmzj.comic; 4 | 5 | message ComicDetailResponse { 6 | int32 Errno = 1; 7 | string Errmsg = 2; 8 | ComicDetailInfoResponse Data = 3; 9 | } 10 | 11 | message ComicDetailInfoResponse { 12 | int32 Id = 1; 13 | string Title = 2; 14 | int32 Direction = 3; 15 | int32 Islong = 4; 16 | int32 IsDmzj = 5; 17 | string Cover = 6; 18 | string Description = 7; 19 | int64 LastUpdatetime = 8; 20 | string LastUpdateChapterName = 9; 21 | int32 Copyright = 10; 22 | string FirstLetter = 11; 23 | string ComicPy = 12; 24 | int32 Hidden = 13; 25 | int32 HotNum = 14; 26 | int32 HitNum = 15; 27 | int32 Uid = 16; 28 | int32 IsLock = 17; 29 | int32 LastUpdateChapterId = 18; 30 | repeated ComicDetailTypeItemResponse Types = 19; 31 | repeated ComicDetailTypeItemResponse Status = 20; 32 | repeated ComicDetailTypeItemResponse Authors = 21; 33 | int32 SubscribeNum = 22; 34 | repeated ComicDetailChapterResponse Chapters = 23; 35 | int32 IsNeedLogin = 24; 36 | //object UrlLinks=25; 37 | int32 IsHideChapter = 26; 38 | //object DhUrlLinks=27; 39 | 40 | } 41 | 42 | message ComicDetailTypeItemResponse { 43 | int32 TagId = 1; 44 | string TagName = 2; 45 | } 46 | 47 | message ComicDetailChapterResponse { 48 | string Title = 1; 49 | repeated ComicDetailChapterInfoResponse Data = 2; 50 | } 51 | message ComicDetailChapterInfoResponse { 52 | int32 ChapterId = 1; 53 | string ChapterTitle = 2; 54 | int64 Updatetime = 3; 55 | int32 Filesize = 4; 56 | int32 ChapterOrder = 5; 57 | } 58 | 59 | message ComicChapterDetailResponse { 60 | optional int32 Errno = 1; 61 | optional string Errmsg = 2; 62 | ComicChapterDetailInfoResponse Data = 3; 63 | } 64 | 65 | message ComicChapterDetailInfoResponse { 66 | int32 ChapterId = 1; 67 | int32 ComicId = 2; 68 | string Title = 3; 69 | int32 Order = 4; 70 | int32 Status = 5; 71 | repeated string SmallPages = 6; 72 | int32 Length = 7; 73 | repeated string RawPages = 8; 74 | int32 FileSize = 9; 75 | } 76 | 77 | message ComicUpdateListResponse { 78 | int32 Errno = 1; 79 | string Errmsg = 2; 80 | repeated ComicUpdateListItemResponse Data= 3; 81 | } 82 | 83 | message ComicUpdateListItemResponse { 84 | int32 ComicId = 1; 85 | string Title = 2; 86 | bool Islong = 3; 87 | string Authors=4; 88 | string Types=5; 89 | string Cover=6; 90 | string Status=7; 91 | string LastUpdateChapterName=8; 92 | int32 LastUpdateChapterId=9; 93 | int64 LastUpdatetime=10; 94 | } 95 | 96 | message ComicRankListResponse { 97 | int32 Errno = 1; 98 | string Errmsg = 2; 99 | repeated ComicRankListItemResponse Data= 3; 100 | } 101 | 102 | message ComicRankListItemResponse { 103 | int32 ComicId = 1; 104 | string Title = 2; 105 | string Authors=3; 106 | string Status=4; 107 | string Cover=5; 108 | string Types=6; 109 | int64 LastUpdatetime=7; 110 | string LastUpdateChapterName=8; 111 | string ComicPy=9; 112 | int32 Num=10; 113 | int32 TagId=11; 114 | string ChapterName=12; 115 | int32 ChapterId=13; 116 | } -------------------------------------------------------------------------------- /lib/component/comic/SubjectListTile.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:dcomic/component/comic/BaseListTile.dart'; 3 | import 'package:dcomic/utils/ProxyCacheManager.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class SubjectListTile extends StatelessWidget { 8 | final String cover; 9 | final VoidCallback onPressed; 10 | final Map headers; 11 | final String title; 12 | final String recommendBrief; 13 | final String recommendReason; 14 | 15 | const SubjectListTile( 16 | {Key key, 17 | this.cover, 18 | this.onPressed, 19 | this.headers, 20 | this.title, 21 | this.recommendBrief, 22 | this.recommendReason}) 23 | : super(key: key); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | // TODO: implement build 28 | return BaseListTile( 29 | onPressed: onPressed, 30 | leading: CachedNetworkImage( 31 | imageUrl: cover, 32 | httpHeaders: headers, 33 | progressIndicatorBuilder: (context, url, downloadProgress) => Container( 34 | child: Center( 35 | child: CircularProgressIndicator(value: downloadProgress.progress), 36 | ), 37 | ), 38 | errorWidget: (context, url, error) => CachedNetworkImage( 39 | imageUrl: cover, 40 | httpHeaders: headers, 41 | cacheManager: BadCertificateCacheManager(), 42 | progressIndicatorBuilder: (context, url, downloadProgress) => 43 | Container( 44 | child: Center( 45 | child: 46 | CircularProgressIndicator(value: downloadProgress.progress), 47 | ), 48 | ), 49 | errorWidget: (context, url, error) => Icon(Icons.error), 50 | ), 51 | ), 52 | detail: [ 53 | Text( 54 | title, 55 | maxLines: 1, 56 | overflow: TextOverflow.ellipsis, 57 | style: TextStyle(fontSize: 18), 58 | ), 59 | SizedBox( 60 | height: 12, 61 | ), 62 | Text.rich( 63 | TextSpan(children: [ 64 | WidgetSpan( 65 | child: Icon( 66 | Icons.tag, 67 | color: Colors.grey, 68 | size: 23, 69 | )), 70 | TextSpan( 71 | text: " ", 72 | ), 73 | TextSpan( 74 | text: recommendBrief, 75 | style: TextStyle(color: Colors.grey, fontSize: 16)) 76 | ]), 77 | ), 78 | SizedBox( 79 | height: 12, 80 | ), 81 | Text.rich( 82 | TextSpan(children: [ 83 | WidgetSpan( 84 | child: Icon( 85 | Icons.message, 86 | color: Colors.grey, 87 | size: 23, 88 | )), 89 | TextSpan( 90 | text: " ", 91 | ), 92 | TextSpan( 93 | text: recommendReason, 94 | style: TextStyle(color: Colors.grey, fontSize: 16)) 95 | ]), 96 | ), 97 | ], 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/model/downloadChapters.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:dcomic/database/downloader.dart'; 4 | import 'package:dcomic/model/baseModel.dart'; 5 | import 'package:dcomic/model/comic_source/sourceProvider.dart'; 6 | import 'package:dcomic/view/comic_viewer.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | import 'comic_source/baseSourceModel.dart'; 10 | 11 | class DownloadChaptersModel extends BaseModel { 12 | final String comicId; 13 | 14 | List data; 15 | List list = []; 16 | 17 | BuildContext context; 18 | 19 | DownloadChaptersModel(this.comicId) { 20 | getChapters(comicId); 21 | } 22 | 23 | Future getChapters(comicId) async { 24 | DownloadProvider downloadProvider = DownloadProvider(); 25 | data = await downloadProvider.getAllChapter(comicId); 26 | data.sort( 27 | (a, b) => int.parse(b.chapterId).compareTo(int.parse(a.chapterId))); 28 | await buildList(); 29 | notifyListeners(); 30 | } 31 | 32 | Widget buildChapterListTile(context, index) { 33 | this.context = context; 34 | if (index < 0 || index > list.length) { 35 | return null; 36 | } 37 | return list[index]; 38 | } 39 | 40 | Future buildList() async { 41 | for (var item in data) { 42 | list.add(ListTile( 43 | title: Text('${item.title}'), 44 | subtitle: LinearProgressIndicator( 45 | value: await item.progress / item.total, 46 | ), 47 | trailing: IconButton( 48 | icon: Icon( 49 | Icons.delete, 50 | color: Colors.red, 51 | ), 52 | onPressed: () async { 53 | await showDialog(context: context,builder: (context){ 54 | return AlertDialog( 55 | content: Text('是否删除?'), 56 | actions: [ 57 | FlatButton( 58 | child: Text('取消'), 59 | onPressed: (){ 60 | Navigator.of(context).pop(); 61 | }, 62 | ), 63 | FlatButton( 64 | child: Text('确认'), 65 | onPressed: ()async{ 66 | DownloadProvider downloadProvider = DownloadProvider(); 67 | await downloadProvider.deleteChapter(item); 68 | if (data.length <= 1) { 69 | await downloadProvider.deleteComic(comicId); 70 | } 71 | Navigator.of(context).pop(); 72 | }, 73 | ) 74 | ], 75 | ); 76 | }); 77 | Provider.of(context, listen: false) 78 | .getChapters(comicId); 79 | }, 80 | ), 81 | onTap: () async{ 82 | Comic comic= await Provider.of(context,listen: false).active.getChapter(chapterId: item.chapterId,comicId: item.comicId); 83 | Navigator.of(context).push(MaterialPageRoute(builder: (context) { 84 | return ComicViewPage( 85 | comic: comic, 86 | ); 87 | })); 88 | }, 89 | )); 90 | } 91 | } 92 | 93 | int get length => data == null ? 0 : data.length; 94 | } 95 | -------------------------------------------------------------------------------- /lib/view/mag_maker/output_mag_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dcomic/component/LoadingCube.dart'; 2 | import 'package:dcomic/component/comic/ComicListTile.dart'; 3 | import 'package:dcomic/model/mag_model/OutputMangaModel.dart'; 4 | import 'package:dcomic/model/mag_model/baseMangaModel.dart'; 5 | import 'package:file_picker/file_picker.dart'; 6 | import 'package:flutter/cupertino.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | class OutputMangaPage extends StatefulWidget { 12 | @override 13 | State createState() { 14 | // TODO: implement createState 15 | return _OutputMangaPage(); 16 | } 17 | } 18 | 19 | class _OutputMangaPage extends State { 20 | @override 21 | Widget build(BuildContext context) { 22 | // TODO: implement build 23 | return ChangeNotifierProvider( 24 | create: (_) => OutputMangaModel(), 25 | builder: (context, child) => Scaffold( 26 | appBar: AppBar( 27 | title: Text('导出漫画'), 28 | ), 29 | body: EasyRefresh( 30 | onRefresh: () async { 31 | await Provider.of(context, listen: false) 32 | .init(); 33 | }, 34 | firstRefreshWidget: LoadingCube(), 35 | firstRefresh: true, 36 | child: ListView.builder( 37 | shrinkWrap: true, 38 | physics: NeverScrollableScrollPhysics(), 39 | itemCount: Provider.of(context).data.length, 40 | itemBuilder: (context, index) { 41 | var item = Provider.of(context).data[index]; 42 | return ComicListTile( 43 | cover: item.cover, 44 | title: item.title, 45 | authors: item.authors 46 | .map((e) => e.name) 47 | .toList() 48 | .join('/'), 49 | tag: 50 | item.tags.map((e) => e.name).toList().join('/'), 51 | date: item.lastUpdateTimeStamp, 52 | onPressed: () async { 53 | var outputPath = 54 | await FilePicker.platform.getDirectoryPath(); 55 | if (outputPath != null) { 56 | try { 57 | await BaseMangaModel() 58 | .encodeFromDirectory(item.basePath, outputPath); 59 | ScaffoldMessenger.of(context) 60 | .showSnackBar(SnackBar(content: Text("导出成功"))); 61 | } catch (e) { 62 | ScaffoldMessenger.of(context) 63 | .showSnackBar(SnackBar(content: Text("输出错误:$e"))); 64 | } 65 | } else { 66 | ScaffoldMessenger.of(context) 67 | .showSnackBar(SnackBar(content: Text("请选择有效目录"))); 68 | } 69 | }, 70 | pageType: item.coverPageType, 71 | ); 72 | }), 73 | )), 74 | ); 75 | } 76 | } 77 | --------------------------------------------------------------------------------