├── deps
└── .gitkeep
├── assets
├── screenshot1.jpg
├── screenshot10.jpg
├── screenshot11.jpg
├── screenshot12.jpg
├── screenshot13.jpg
├── screenshot14.jpg
├── screenshot15.jpg
├── screenshot16.jpg
├── screenshot17.jpg
├── screenshot18.jpg
├── screenshot19.jpg
├── screenshot2.jpg
├── screenshot20.jpg
├── screenshot21.jpg
├── screenshot3.jpg
├── screenshot4.jpg
├── screenshot5.jpg
├── screenshot6.jpg
├── screenshot7.jpg
├── screenshot8.jpg
└── screenshot9.jpg
├── lib
├── assets
│ ├── logo_xxhdpi.png
│ ├── splash_logo.png
│ ├── splash_copyright.png
│ └── icons
│ │ ├── CustomIcons.ttf
│ │ ├── bookmark_minus.svg
│ │ ├── bookmark_plus.svg
│ │ ├── mdi_filled_notebook.svg
│ │ ├── eye_download.svg
│ │ ├── eye_menu.svg
│ │ ├── opened_blank_book.svg
│ │ ├── opened_both_blank_book.svg
│ │ ├── opened_right_star_book.svg
│ │ ├── opened_left_star_book.svg
│ │ ├── eye_sync.svg
│ │ ├── eye_public.svg
│ │ ├── opened_book_cog.svg
│ │ ├── application_star_cog.svg
│ │ └── download_cog.svg
├── service
│ ├── native
│ │ ├── clipboard.dart
│ │ ├── browser.dart
│ │ ├── system_ui.dart
│ │ └── android.dart
│ ├── evb
│ │ ├── evb_manager.dart
│ │ ├── auth_manager.dart
│ │ └── events.dart
│ ├── prefs
│ │ ├── search_history.dart
│ │ └── read_message.dart
│ ├── storage
│ │ ├── queue_manager.dart
│ │ └── storage.dart
│ ├── db
│ │ └── query_helper.dart
│ └── dio
│ │ └── dio_manager.dart
├── model
│ ├── result.dart
│ ├── category.g.dart
│ ├── result.g.dart
│ ├── user.g.dart
│ ├── message.dart
│ ├── chapter.g.dart
│ ├── order.dart
│ ├── comment.g.dart
│ ├── comment.dart
│ ├── author.dart
│ ├── user.dart
│ ├── message.g.dart
│ ├── category.dart
│ └── author.g.dart
├── page
│ ├── sep_recent.dart
│ ├── sep_ranking.dart
│ ├── view
│ │ ├── full_ripple.dart
│ │ ├── login_first.dart
│ │ ├── favorite_author_line.dart
│ │ ├── manga_history_line.dart
│ │ ├── small_author_line.dart
│ │ ├── small_manga_line.dart
│ │ ├── tiny_manga_line.dart
│ │ ├── genre_chip_list.dart
│ │ ├── custom_icons.dart
│ │ ├── shelf_manga_line.dart
│ │ ├── shelf_cache_line.dart
│ │ ├── favorite_manga_line.dart
│ │ ├── list_hint.dart
│ │ ├── favorite_reorder_line.dart
│ │ ├── category_grid.dart
│ │ ├── manga_ranking_line.dart
│ │ ├── manga_simple_toc.dart
│ │ ├── image_load.dart
│ │ └── detail_table.dart
│ ├── sep_genre.dart
│ ├── sep_shelf.dart
│ ├── sep_history.dart
│ ├── manga_group.dart
│ ├── manga_random.dart
│ ├── sep_favorite.dart
│ ├── author_detail.dart
│ ├── chapter_detail.dart
│ ├── manga_detail.dart
│ ├── manga_aud_ranking.dart
│ └── page
│ │ └── home.dart
├── main.dart
└── config.dart
├── android
├── gradle.properties
├── app
│ ├── src
│ │ ├── main
│ │ │ ├── res
│ │ │ │ ├── drawable
│ │ │ │ │ ├── background.png
│ │ │ │ │ ├── flutter_icon.png
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── drawable-hdpi
│ │ │ │ │ ├── branding.png
│ │ │ │ │ └── splash.png
│ │ │ │ ├── drawable-mdpi
│ │ │ │ │ ├── branding.png
│ │ │ │ │ └── splash.png
│ │ │ │ ├── drawable-xhdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── branding.png
│ │ │ │ ├── drawable-xxhdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── branding.png
│ │ │ │ ├── drawable-v21
│ │ │ │ │ ├── background.png
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── drawable-xxxhdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── branding.png
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── drawable-night-hdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── branding.png
│ │ │ │ ├── drawable-night-mdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── branding.png
│ │ │ │ ├── drawable-night-xhdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── branding.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── drawable-night-xxhdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── branding.png
│ │ │ │ ├── drawable-night-xxxhdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── branding.png
│ │ │ │ ├── xml
│ │ │ │ │ └── provider_paths.xml
│ │ │ │ ├── values-night
│ │ │ │ │ └── styles.xml
│ │ │ │ ├── values-v31
│ │ │ │ │ └── styles.xml
│ │ │ │ └── values
│ │ │ │ │ └── styles.xml
│ │ │ └── AndroidManifest.xml
│ │ ├── debug
│ │ │ └── AndroidManifest.xml
│ │ └── profile
│ │ │ └── AndroidManifest.xml
│ ├── proguard-rules.pro
│ ├── versions.md
│ └── build.gradle
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle
└── build.gradle
├── Makefile
├── .metadata
├── analysis_options.yaml
├── .gitignore
├── LICENSE
├── test
└── widget_test.dart
├── pubspec.yaml
├── README.md
└── process_deps.sh
/deps/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/screenshot1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot1.jpg
--------------------------------------------------------------------------------
/assets/screenshot10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot10.jpg
--------------------------------------------------------------------------------
/assets/screenshot11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot11.jpg
--------------------------------------------------------------------------------
/assets/screenshot12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot12.jpg
--------------------------------------------------------------------------------
/assets/screenshot13.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot13.jpg
--------------------------------------------------------------------------------
/assets/screenshot14.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot14.jpg
--------------------------------------------------------------------------------
/assets/screenshot15.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot15.jpg
--------------------------------------------------------------------------------
/assets/screenshot16.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot16.jpg
--------------------------------------------------------------------------------
/assets/screenshot17.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot17.jpg
--------------------------------------------------------------------------------
/assets/screenshot18.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot18.jpg
--------------------------------------------------------------------------------
/assets/screenshot19.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot19.jpg
--------------------------------------------------------------------------------
/assets/screenshot2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot2.jpg
--------------------------------------------------------------------------------
/assets/screenshot20.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot20.jpg
--------------------------------------------------------------------------------
/assets/screenshot21.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot21.jpg
--------------------------------------------------------------------------------
/assets/screenshot3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot3.jpg
--------------------------------------------------------------------------------
/assets/screenshot4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot4.jpg
--------------------------------------------------------------------------------
/assets/screenshot5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot5.jpg
--------------------------------------------------------------------------------
/assets/screenshot6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot6.jpg
--------------------------------------------------------------------------------
/assets/screenshot7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot7.jpg
--------------------------------------------------------------------------------
/assets/screenshot8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot8.jpg
--------------------------------------------------------------------------------
/assets/screenshot9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/assets/screenshot9.jpg
--------------------------------------------------------------------------------
/lib/assets/logo_xxhdpi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/lib/assets/logo_xxhdpi.png
--------------------------------------------------------------------------------
/lib/assets/splash_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/lib/assets/splash_logo.png
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/lib/assets/splash_copyright.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/lib/assets/splash_copyright.png
--------------------------------------------------------------------------------
/lib/assets/icons/CustomIcons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/lib/assets/icons/CustomIcons.ttf
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable/background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/branding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-hdpi/branding.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-hdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/branding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-mdpi/branding.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-mdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-xhdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-xxhdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/flutter_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable/flutter_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-v21/background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/branding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-xhdpi/branding.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/branding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-xxhdpi/branding.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-xxxhdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/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/Aoi-hosizora/manhuagui_flutter/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/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-hdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-night-hdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-mdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-night-mdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-xhdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-night-xhdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/branding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-xxxhdpi/branding.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/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/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-hdpi/branding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-night-hdpi/branding.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-mdpi/branding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-night-mdpi/branding.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-xhdpi/branding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-night-xhdpi/branding.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-xxhdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-night-xxhdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-xxxhdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-night-xxxhdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-xxhdpi/branding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-night-xxhdpi/branding.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-xxxhdpi/branding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aoi-hosizora/manhuagui_flutter/HEAD/android/app/src/main/res/drawable-night-xxxhdpi/branding.png
--------------------------------------------------------------------------------
/lib/assets/icons/bookmark_minus.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lib/assets/icons/bookmark_plus.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build
2 |
3 | build:
4 | flutter pub run build_runner build
5 |
6 | build_delete:
7 | flutter pub run build_runner build --delete-conflicting-outputs
8 |
9 | create_splash:
10 | flutter pub run flutter_native_splash:create
11 |
--------------------------------------------------------------------------------
/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-6.7-all.zip
7 |
--------------------------------------------------------------------------------
/android/app/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.** { *; }
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: 5464c5bac742001448fe4fc0597be939379f88ea
8 | channel: stable
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/lib/assets/icons/mdi_filled_notebook.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:flutter_lints/flutter.yaml
2 |
3 | linter:
4 | rules:
5 | avoid_print: false
6 | sized_box_for_whitespace: false
7 | prefer_single_quotes: true
8 | prefer_const_constructors: false
9 | prefer_const_literals_to_create_immutables: true
10 | avoid_function_literals_in_foreach_calls: false
11 | prefer_function_declarations_over_variables: false
12 |
--------------------------------------------------------------------------------
/android/app/src/main/res/xml/provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/app/versions.md:
--------------------------------------------------------------------------------
1 | ## Versions
2 |
3 | ### Version Codes and Version Names
4 |
5 | | versionCode | versionName |
6 | |-------------|-------------|
7 | | 1 | 1.0.0 |
8 | | 2 | 1.0.1 |
9 | | 3 | 1.0.2 |
10 | | 3 | 1.1.0 |
11 | | 4 | 1.2.0 |
12 | | 5 | 1.2.1 |
13 | | 6 | 1.2.2 |
14 | | 7 | 1.3.0 |
15 |
--------------------------------------------------------------------------------
/lib/assets/icons/eye_download.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lib/service/native/clipboard.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/services.dart';
2 | import 'package:fluttertoast/fluttertoast.dart';
3 |
4 | Future copyText(
5 | String text, {
6 | bool showToast = true,
7 | }) async {
8 | var data = ClipboardData(text: text);
9 | try {
10 | await Clipboard.setData(data);
11 | if (showToast) {
12 | await Fluttertoast.cancel();
13 | Fluttertoast.showToast(msg: '"$text" 已经复制到剪贴板');
14 | }
15 | } catch (_) {}
16 | }
17 |
--------------------------------------------------------------------------------
/lib/assets/icons/eye_menu.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lib/assets/icons/opened_blank_book.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 | -
7 |
8 |
9 | -
10 |
11 |
12 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 | -
7 |
8 |
9 | -
10 |
11 |
12 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
4 | def properties = new Properties()
5 |
6 | assert localPropertiesFile.exists()
7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
8 |
9 | def flutterSdkPath = properties.getProperty("flutter.sdk")
10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
12 |
--------------------------------------------------------------------------------
/lib/assets/icons/opened_both_blank_book.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lib/assets/icons/opened_right_star_book.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lib/assets/icons/opened_left_star_book.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lib/assets/icons/eye_sync.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lib/service/evb/evb_manager.dart:
--------------------------------------------------------------------------------
1 | import 'package:event_bus/event_bus.dart';
2 |
3 | class EventBusManager {
4 | EventBusManager._();
5 |
6 | static EventBusManager? _instance;
7 |
8 | static EventBusManager get instance {
9 | _instance ??= EventBusManager._();
10 | return _instance!;
11 | }
12 |
13 | EventBus? _eventBus; // global EventBus instance
14 |
15 | EventBus get eventBus {
16 | _eventBus ??= EventBus();
17 | return _eventBus!;
18 | }
19 |
20 | void Function() listen(void Function(T event) onData) {
21 | var stream = eventBus.on().listen(onData);
22 | return () => stream.cancel();
23 | }
24 |
25 | void fire(dynamic event) {
26 | eventBus.fire(event);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.6.10'
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:4.1.0'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | mavenCentral()
18 | }
19 | }
20 |
21 | rootProject.buildDir = '../build'
22 | subprojects {
23 | project.buildDir = "${rootProject.buildDir}/${project.name}"
24 | }
25 | subprojects {
26 | project.evaluationDependsOn(':app')
27 | }
28 |
29 | task clean(type: Delete) {
30 | delete rootProject.buildDir
31 | }
32 |
--------------------------------------------------------------------------------
/lib/assets/icons/eye_public.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/.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 | # VS Code related
19 | .vscode/
20 | .ionide/
21 | .history/
22 | *.rdb
23 |
24 | # Flutter/Dart/Pub related
25 | **/doc/api/
26 | **/ios/Flutter/.last_build_id
27 | .dart_tool/
28 | .flutter-plugins
29 | .flutter-plugins-dependencies
30 | .packages
31 | .pub-cache/
32 | .pub/
33 | /build/
34 |
35 | # Web related
36 | lib/generated_plugin_registrant.dart
37 |
38 | # Symbolication related
39 | app.*.symbols
40 |
41 | # Obfuscation related
42 | app.*.map.json
43 |
44 | # Android Studio will place build artifacts here
45 | /android/app/debug
46 | /android/app/profile
47 | /android/app/release
48 |
49 | ### Project ###
50 | _*.png
51 | _origin/
52 | deps/*
53 | !deps/.gitkeep
54 |
--------------------------------------------------------------------------------
/lib/service/native/browser.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_web_browser/flutter_web_browser.dart';
3 | import 'package:url_launcher/url_launcher_string.dart';
4 |
5 | Future launchInBrowser({
6 | required BuildContext context,
7 | required String url,
8 | bool useLaunch = false,
9 | LaunchMode launchMode = LaunchMode.externalApplication,
10 | }) async {
11 | if (useLaunch) {
12 | try {
13 | await launchUrlString(url, mode: launchMode);
14 | } catch (_) {}
15 | return;
16 | }
17 |
18 | try {
19 | await FlutterWebBrowser.openWebPage(
20 | url: url,
21 | customTabsOptions: CustomTabsOptions(
22 | defaultColorSchemeParams: CustomTabsColorSchemeParams(
23 | toolbarColor: Theme.of(context).primaryColor,
24 | ),
25 | shareState: CustomTabsShareState.on,
26 | instantAppsEnabled: false,
27 | showTitle: true,
28 | urlBarHidingEnabled: true,
29 | ),
30 | );
31 | } catch (_) {}
32 | }
33 |
--------------------------------------------------------------------------------
/lib/assets/icons/opened_book_cog.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lib/assets/icons/application_star_cog.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/lib/assets/icons/download_cog.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 AoiHosizora
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-v31/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
16 |
19 |
--------------------------------------------------------------------------------
/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:manhuagui_flutter/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 |
--------------------------------------------------------------------------------
/lib/model/result.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 |
3 | part 'result.g.dart';
4 |
5 | @JsonSerializable(fieldRename: FieldRename.snake, genericArgumentFactories: true)
6 | class Result {
7 | final int code;
8 | final String message;
9 | final T data;
10 |
11 | const Result({required this.code, required this.message, required this.data});
12 |
13 | factory Result.fromJson(Map json, T Function(Object? json) fromJsonT) => _$ResultFromJson(json, fromJsonT);
14 |
15 | Map toJson(Object? Function(T value) toJsonT) => _$ResultToJson(this, toJsonT);
16 | }
17 |
18 | @JsonSerializable(fieldRename: FieldRename.snake, genericArgumentFactories: true)
19 | class ResultPage {
20 | final int page;
21 | final int limit;
22 | final int total;
23 | final List data;
24 |
25 | const ResultPage({required this.page, required this.limit, required this.total, required this.data});
26 |
27 | factory ResultPage.fromJson(Map json, T Function(Object? json) fromJsonT) => _$ResultPageFromJson(json, fromJsonT);
28 |
29 | Map toJson(Object? Function(T value) toJsonT) => _$ResultPageToJson(this, toJsonT);
30 | }
31 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
17 |
21 |
--------------------------------------------------------------------------------
/lib/service/native/system_ui.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter/services.dart';
3 |
4 | void setDefaultSystemUIOverlayStyle() async {
5 | setSystemUIOverlayStyle();
6 | }
7 |
8 | void setSystemUIOverlayStyle({
9 | // status bar
10 | Color? statusBarColor,
11 | Brightness? statusBarBrightness,
12 | Brightness? statusBarIconBrightness,
13 | // navigation bar
14 | Color? navigationBarColor,
15 | Brightness? navigationBarIconBrightness,
16 | Color? navigationBarDividerColor,
17 | }) async {
18 | SystemChrome.setSystemUIOverlayStyle(
19 | SystemUiOverlayStyle(
20 | statusBarColor: statusBarColor,
21 | statusBarBrightness: statusBarBrightness ?? Brightness.dark,
22 | statusBarIconBrightness: statusBarIconBrightness ?? Brightness.light,
23 | systemNavigationBarColor: navigationBarColor ?? Color.fromRGBO(250, 250, 250, 1.0),
24 | systemNavigationBarIconBrightness: navigationBarIconBrightness ?? Brightness.dark,
25 | systemNavigationBarDividerColor: navigationBarDividerColor ?? Color.fromRGBO(250, 250, 250, 1.0),
26 | ),
27 | );
28 | }
29 |
30 | Future setEdgeToEdgeSystemUIMode() async {
31 | await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: SystemUiOverlay.values);
32 | }
33 |
34 | Future setManualSystemUIMode(List overlays) async {
35 | await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: overlays);
36 | }
37 |
--------------------------------------------------------------------------------
/lib/page/sep_recent.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/page/page/home_recent.dart';
4 | import 'package:manhuagui_flutter/page/search.dart';
5 | import 'package:manhuagui_flutter/page/view/app_drawer.dart';
6 |
7 | /// 最近更新页,即 Separate [RecentSubPage]
8 | class SepRecentPage extends StatefulWidget {
9 | const SepRecentPage({
10 | Key? key,
11 | }) : super(key: key);
12 |
13 | @override
14 | _SepRecentPageState createState() => _SepRecentPageState();
15 | }
16 |
17 | class _SepRecentPageState extends State {
18 | @override
19 | Widget build(BuildContext context) {
20 | return Scaffold(
21 | appBar: AppBar(
22 | title: Text('最近更新'),
23 | leading: AppBarActionButton.leading(context: context, allowDrawerButton: true),
24 | actions: [
25 | AppBarActionButton(
26 | icon: Icon(Icons.search),
27 | tooltip: '搜索漫画',
28 | onPressed: () => Navigator.of(context).push(
29 | CustomPageRoute(
30 | context: context,
31 | builder: (c) => SearchPage(),
32 | ),
33 | ),
34 | ),
35 | ],
36 | ),
37 | drawer: AppDrawer(
38 | currentSelection: DrawerSelection.recent,
39 | ),
40 | drawerEdgeDragWidth: MediaQuery.of(context).size.width,
41 | body: RecentSubPage(),
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/page/sep_ranking.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/page/page/home_ranking.dart';
4 | import 'package:manhuagui_flutter/page/search.dart';
5 | import 'package:manhuagui_flutter/page/view/app_drawer.dart';
6 |
7 | /// 漫画排行榜页,即 Separate [RankingSubPage]
8 | class SepRankingPage extends StatefulWidget {
9 | const SepRankingPage({
10 | Key? key,
11 | }) : super(key: key);
12 |
13 | @override
14 | _SepRankingPageState createState() => _SepRankingPageState();
15 | }
16 |
17 | class _SepRankingPageState extends State {
18 | @override
19 | Widget build(BuildContext context) {
20 | return Scaffold(
21 | appBar: AppBar(
22 | title: Text('漫画排行榜'),
23 | leading: AppBarActionButton.leading(context: context, allowDrawerButton: true),
24 | actions: [
25 | AppBarActionButton(
26 | icon: Icon(Icons.search),
27 | tooltip: '搜索漫画',
28 | onPressed: () => Navigator.of(context).push(
29 | CustomPageRoute(
30 | context: context,
31 | builder: (c) => SearchPage(),
32 | ),
33 | ),
34 | ),
35 | ],
36 | ),
37 | drawer: AppDrawer(
38 | currentSelection: DrawerSelection.ranking,
39 | ),
40 | drawerEdgeDragWidth: MediaQuery.of(context).size.width,
41 | body: RankingSubPage(),
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/model/category.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'category.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | Category _$CategoryFromJson(Map json) => Category(
10 | name: json['name'] as String,
11 | title: json['title'] as String,
12 | url: json['url'] as String,
13 | cover: json['cover'] as String,
14 | );
15 |
16 | Map _$CategoryToJson(Category instance) => {
17 | 'name': instance.name,
18 | 'title': instance.title,
19 | 'url': instance.url,
20 | 'cover': instance.cover,
21 | };
22 |
23 | CategoryList _$CategoryListFromJson(Map json) => CategoryList(
24 | genres: (json['genres'] as List)
25 | .map((e) => Category.fromJson(e as Map))
26 | .toList(),
27 | ages: (json['ages'] as List)
28 | .map((e) => Category.fromJson(e as Map))
29 | .toList(),
30 | zones: (json['zones'] as List)
31 | .map((e) => Category.fromJson(e as Map))
32 | .toList(),
33 | );
34 |
35 | Map _$CategoryListToJson(CategoryList instance) =>
36 | {
37 | 'genres': instance.genres,
38 | 'ages': instance.ages,
39 | 'zones': instance.zones,
40 | };
41 |
--------------------------------------------------------------------------------
/lib/model/result.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'result.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | Result _$ResultFromJson(
10 | Map json,
11 | T Function(Object? json) fromJsonT,
12 | ) =>
13 | Result(
14 | code: json['code'] as int,
15 | message: json['message'] as String,
16 | data: fromJsonT(json['data']),
17 | );
18 |
19 | Map _$ResultToJson(
20 | Result instance,
21 | Object? Function(T value) toJsonT,
22 | ) =>
23 | {
24 | 'code': instance.code,
25 | 'message': instance.message,
26 | 'data': toJsonT(instance.data),
27 | };
28 |
29 | ResultPage _$ResultPageFromJson(
30 | Map json,
31 | T Function(Object? json) fromJsonT,
32 | ) =>
33 | ResultPage(
34 | page: json['page'] as int,
35 | limit: json['limit'] as int,
36 | total: json['total'] as int,
37 | data: (json['data'] as List).map(fromJsonT).toList(),
38 | );
39 |
40 | Map _$ResultPageToJson(
41 | ResultPage instance,
42 | Object? Function(T value) toJsonT,
43 | ) =>
44 | {
45 | 'page': instance.page,
46 | 'limit': instance.limit,
47 | 'total': instance.total,
48 | 'data': instance.data.map(toJsonT).toList(),
49 | };
50 |
--------------------------------------------------------------------------------
/lib/page/view/full_ripple.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | /// 点击图像时的 Ripple 效果,在 [MineSubPage] / [MangaPage] / [AuthorPage] / [LargeDownloadLineView] 使用
4 | class FullRippleWidget extends StatelessWidget {
5 | const FullRippleWidget({
6 | Key? key,
7 | required this.child,
8 | this.radius,
9 | required this.onTap,
10 | this.onLongPress,
11 | this.highlightColor = Colors.black26,
12 | this.splashColor = Colors.black26,
13 | this.backgroundDecoration,
14 | }) : super(key: key);
15 |
16 | final Widget child;
17 | final BorderRadius? radius;
18 | final void Function()? onTap;
19 | final void Function()? onLongPress;
20 | final Color? highlightColor;
21 | final Color? splashColor;
22 | final Decoration? backgroundDecoration;
23 |
24 | @override
25 | Widget build(BuildContext context) {
26 | return ClipRRect(
27 | borderRadius: radius ?? BorderRadius.zero,
28 | child: Stack(
29 | children: [
30 | if (backgroundDecoration != null)
31 | Positioned.fill(
32 | child: Container(
33 | decoration: backgroundDecoration!,
34 | ),
35 | ),
36 | child,
37 | Positioned.fill(
38 | child: Material(
39 | color: Colors.transparent,
40 | child: InkWell(
41 | onTap: onTap,
42 | onLongPress: onLongPress,
43 | highlightColor: highlightColor,
44 | splashColor: splashColor,
45 | ),
46 | ),
47 | ),
48 | ],
49 | ),
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/lib/page/sep_genre.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/category.dart';
4 | import 'package:manhuagui_flutter/page/page/category_genre.dart';
5 | import 'package:manhuagui_flutter/page/search.dart';
6 | import 'package:manhuagui_flutter/page/view/app_drawer.dart';
7 |
8 | /// 漫画类别页,即 Separate [GenreSubPage]
9 | class SepGenrePage extends StatefulWidget {
10 | const SepGenrePage({
11 | Key? key,
12 | required this.genre,
13 | }) : super(key: key);
14 |
15 | final TinyCategory genre;
16 |
17 | @override
18 | _SepGenrePageState createState() => _SepGenrePageState();
19 | }
20 |
21 | class _SepGenrePageState extends State {
22 | @override
23 | Widget build(BuildContext context) {
24 | return Scaffold(
25 | appBar: AppBar(
26 | title: Text('漫画类别'),
27 | leading: AppBarActionButton.leading(context: context, allowDrawerButton: true),
28 | actions: [
29 | AppBarActionButton(
30 | icon: Icon(Icons.search),
31 | tooltip: '搜索漫画',
32 | onPressed: () => Navigator.of(context).push(
33 | CustomPageRoute(
34 | context: context,
35 | builder: (c) => SearchPage(),
36 | ),
37 | ),
38 | ),
39 | ],
40 | ),
41 | drawer: AppDrawer(
42 | currentSelection: DrawerSelection.none,
43 | ),
44 | drawerEdgeDragWidth: MediaQuery.of(context).size.width,
45 | body: GenreSubPage(
46 | defaultGenre: widget.genre,
47 | ),
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/service/prefs/search_history.dart:
--------------------------------------------------------------------------------
1 | import 'package:manhuagui_flutter/service/prefs/prefs_manager.dart';
2 | import 'package:shared_preferences/shared_preferences.dart';
3 |
4 | class SearchHistoryPrefs {
5 | SearchHistoryPrefs._();
6 |
7 | static const _searchHistoryKey = StringListKey('SearchHistoryPrefs_searchHistory');
8 |
9 | static List get keys => [_searchHistoryKey];
10 |
11 | static Future> getSearchHistories({SharedPreferences? prefs}) async {
12 | prefs ??= await PrefsManager.instance.loadPrefs();
13 | return prefs.safeGet>(_searchHistoryKey) ?? [];
14 | }
15 |
16 | static Future clearSearchHistories() async {
17 | final prefs = await PrefsManager.instance.loadPrefs();
18 | await prefs.safeSet>(_searchHistoryKey, []);
19 | }
20 |
21 | static Future> addSearchHistory(String s) async {
22 | final prefs = await PrefsManager.instance.loadPrefs();
23 | var data = await getSearchHistories();
24 | data.removeWhere((h) => h == s);
25 | data.insert(0, s); // 新 > 旧
26 | await prefs.safeSet>(_searchHistoryKey, data);
27 | return data;
28 | }
29 |
30 | static Future> removeSearchHistory(String s) async {
31 | final prefs = await PrefsManager.instance.loadPrefs();
32 | var data = await getSearchHistories();
33 | data.removeWhere((h) => h == s);
34 | await prefs.safeSet>(_searchHistoryKey, data);
35 | return data;
36 | }
37 |
38 | static Future upgradeFromVer1To2(SharedPreferences prefs) async {
39 | await prefs.safeMigrate>('SEARCH_HISTORY', _searchHistoryKey, defaultValue: []);
40 | }
41 |
42 | static Future upgradeFromVer2To3(SharedPreferences prefs) async {
43 | // pass
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lib/page/view/login_first.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:fluttertoast/fluttertoast.dart';
4 | import 'package:manhuagui_flutter/page/login.dart';
5 | import 'package:manhuagui_flutter/service/evb/auth_manager.dart';
6 |
7 | /// 登录提示,在 [ShelfSubPage] / [MineSubPage] 使用
8 | class LoginFirstView extends StatelessWidget {
9 | const LoginFirstView({
10 | Key? key,
11 | required this.checking,
12 | this.error = '',
13 | this.onErrorRetry,
14 | }) : super(key: key);
15 |
16 | final bool checking;
17 | final String error;
18 | final void Function()? onErrorRetry;
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | return PlaceholderText(
23 | state: checking ? PlaceholderState.loading : (error.isEmpty ? PlaceholderState.nothing : PlaceholderState.error),
24 | errorText: error.isEmpty ? '' : '无法检查登录状态\n$error',
25 | childBuilder: (c) => const SizedBox.shrink(),
26 | setting: PlaceholderSetting(
27 | nothingIcon: Icons.lock_open,
28 | ).copyWithChinese(
29 | loadingText: '检查登录状态中...',
30 | nothingText: '当前未登录,请先登录 Manhuagui',
31 | nothingRetryText: '登录',
32 | errorRetryText: '重试',
33 | ),
34 | onRetryForNothing: () {
35 | if (!AuthManager.instance.logined) {
36 | Navigator.of(context).push(
37 | CustomPageRoute.fromTheme(
38 | themeData: CustomPageRouteTheme.of(context),
39 | builder: (c) => LoginPage(),
40 | ),
41 | );
42 | } else {
43 | Fluttertoast.showToast(msg: '${AuthManager.instance.username} 登录成功');
44 | AuthManager.instance.notify(logined: true);
45 | }
46 | },
47 | onRetryForError: onErrorRetry,
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/page/view/favorite_author_line.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/entity.dart';
4 | import 'package:manhuagui_flutter/page/author.dart';
5 | import 'package:manhuagui_flutter/page/view/corner_icons.dart';
6 | import 'package:manhuagui_flutter/page/view/general_line.dart';
7 | import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
8 |
9 | /// 收藏作者行,在 [FavoriteAuthorPage] 使用
10 | class FavoriteAuthorLineView extends StatelessWidget {
11 | const FavoriteAuthorLineView({
12 | Key? key,
13 | required this.author,
14 | this.flags,
15 | this.twoColumns = false,
16 | required this.onLongPressed,
17 | }) : super(key: key);
18 |
19 | final FavoriteAuthor author;
20 | final AuthorCornerFlags? flags;
21 | final bool twoColumns;
22 | final void Function()? onLongPressed;
23 |
24 | @override
25 | Widget build(BuildContext context) {
26 | return GeneralLineView(
27 | imageUrl: author.authorCover,
28 | title: author.authorName,
29 | icon1: Icons.place,
30 | text1: author.authorZone,
31 | icon2: MdiIcons.commentBookmarkOutline,
32 | text2: author.remark.trim().isEmpty ? '暂无备注' : '备注 ${author.remark.trim()}',
33 | icon3: Icons.access_time,
34 | text3: '收藏于 ${author.formattedCreatedAtWithDuration}',
35 | cornerIcons: flags?.buildIcons(),
36 | twoColumns: twoColumns,
37 | onPressed: () => Navigator.of(context).push(
38 | CustomPageRoute(
39 | context: context,
40 | builder: (c) => AuthorPage(
41 | id: author.authorId,
42 | name: author.authorName,
43 | url: author.authorUrl,
44 | ),
45 | ),
46 | ),
47 | onLongPressed: onLongPressed,
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/page/view/manga_history_line.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/entity.dart';
4 | import 'package:manhuagui_flutter/page/manga.dart';
5 | import 'package:manhuagui_flutter/page/view/corner_icons.dart';
6 | import 'package:manhuagui_flutter/page/view/custom_icons.dart';
7 | import 'package:manhuagui_flutter/page/view/general_line.dart';
8 |
9 | /// 漫画阅读历史行,在 [HistorySubPage] 使用
10 | class MangaHistoryLineView extends StatelessWidget {
11 | const MangaHistoryLineView({
12 | Key? key,
13 | required this.history,
14 | this.flags,
15 | this.twoColumns = false,
16 | required this.onLongPressed,
17 | }) : super(key: key);
18 |
19 | final MangaHistory history;
20 | final MangaCornerFlags? flags;
21 | final bool twoColumns;
22 | final void Function()? onLongPressed;
23 |
24 | @override
25 | Widget build(BuildContext context) {
26 | return GeneralLineView(
27 | imageUrl: history.mangaCover,
28 | title: history.mangaTitle,
29 | icon1: null,
30 | text1: null,
31 | icon2: !history.read ? CustomIcons.opened_left_star_book : Icons.import_contacts,
32 | text2: !history.read ? '未开始阅读' : '阅读至 ${history.chapterTitle} 第${history.chapterPage}页',
33 | icon3: Icons.access_time,
34 | text3: (!history.read ? '浏览于 ' : '阅读于 ') + history.formattedLastTimeWithDuration,
35 | cornerIcons: flags?.buildIcons(),
36 | twoColumns: twoColumns,
37 | onPressed: () => Navigator.of(context).push(
38 | CustomPageRoute(
39 | context: context,
40 | builder: (c) => MangaPage(
41 | id: history.mangaId,
42 | title: history.mangaTitle,
43 | url: history.mangaUrl,
44 | ),
45 | ),
46 | ),
47 | onLongPressed: onLongPressed,
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/page/view/small_author_line.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/author.dart';
4 | import 'package:manhuagui_flutter/page/author.dart';
5 | import 'package:manhuagui_flutter/page/dlg/author_dialog.dart';
6 | import 'package:manhuagui_flutter/page/view/corner_icons.dart';
7 | import 'package:manhuagui_flutter/page/view/general_line.dart';
8 |
9 | /// 作者行,[SmallAuthor],在 [AuthorSubPage] 使用
10 | class SmallAuthorLineView extends StatelessWidget {
11 | const SmallAuthorLineView({
12 | Key? key,
13 | required this.author,
14 | this.flags,
15 | this.twoColumns = false,
16 | }) : super(key: key);
17 |
18 | final SmallAuthor author;
19 | final AuthorCornerFlags? flags;
20 | final bool twoColumns;
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | return GeneralLineView(
25 | imageUrl: author.cover,
26 | title: author.name,
27 | icon1: Icons.place,
28 | text1: author.zone,
29 | icon2: Icons.edit,
30 | text2: '共收录 ${author.mangaCount} 部漫画',
31 | icon3: Icons.update,
32 | text3: '更新于 ${author.formattedNewestDateWithDuration}',
33 | cornerIcons: flags?.buildIcons(),
34 | twoColumns: twoColumns,
35 | onPressed: () => Navigator.of(context).push(
36 | CustomPageRoute(
37 | context: context,
38 | builder: (c) => AuthorPage(
39 | id: author.aid,
40 | name: author.name,
41 | url: author.url,
42 | ),
43 | ),
44 | ),
45 | onLongPressed: () => showPopupMenuForAuthorList(
46 | context: context,
47 | authorId: author.aid,
48 | authorName: author.name,
49 | authorCover: author.cover,
50 | authorUrl: author.url,
51 | authorZone: author.zone,
52 | ),
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/lib/page/view/small_manga_line.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/manga.dart';
4 | import 'package:manhuagui_flutter/page/dlg/manga_dialog.dart';
5 | import 'package:manhuagui_flutter/page/manga.dart';
6 | import 'package:manhuagui_flutter/page/view/corner_icons.dart';
7 | import 'package:manhuagui_flutter/page/view/general_line.dart';
8 |
9 | /// 帶作者信息的漫画行,[SmallManga],在 [SearchPage] 使用
10 | class SmallMangaLineView extends StatelessWidget {
11 | const SmallMangaLineView({
12 | Key? key,
13 | required this.manga,
14 | this.flags,
15 | this.twoColumns = false,
16 | }) : super(key: key);
17 |
18 | final SmallManga manga;
19 | final MangaCornerFlags? flags;
20 | final bool twoColumns;
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | return GeneralLineView(
25 | imageUrl: manga.cover,
26 | title: manga.title,
27 | icon1: Icons.person,
28 | text1: manga.authors.map((a) => a.name).join('/'),
29 | icon2: Icons.notes,
30 | text2: '最新章节 ${manga.newestChapter}',
31 | icon3: Icons.update,
32 | text3: '${manga.finished ? '已完结' : '连载中'}・${manga.formattedNewestDateWithDuration}',
33 | cornerIcons: flags?.buildIcons(),
34 | twoColumns: twoColumns,
35 | onPressed: () => Navigator.of(context).push(
36 | CustomPageRoute(
37 | context: context,
38 | builder: (c) => MangaPage(
39 | id: manga.mid,
40 | title: manga.title,
41 | url: manga.url,
42 | ),
43 | ),
44 | ),
45 | onLongPressed: () => showPopupMenuForMangaList(
46 | context: context,
47 | mangaId: manga.mid,
48 | mangaTitle: manga.title,
49 | mangaCover: manga.cover,
50 | mangaUrl: manga.url,
51 | ),
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/page/view/tiny_manga_line.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/manga.dart';
4 | import 'package:manhuagui_flutter/page/dlg/manga_dialog.dart';
5 | import 'package:manhuagui_flutter/page/manga.dart';
6 | import 'package:manhuagui_flutter/page/view/corner_icons.dart';
7 | import 'package:manhuagui_flutter/page/view/general_line.dart';
8 |
9 | /// 漫画行,[TinyManga],在 [RecentSubPage] / [OverallSubPage] / [GenreSubPage] / [AuthorPage] 使用
10 | class TinyMangaLineView extends StatelessWidget {
11 | const TinyMangaLineView({
12 | Key? key,
13 | required this.manga,
14 | this.flags,
15 | this.twoColumns = false,
16 | }) : super(key: key);
17 |
18 | final TinyManga manga;
19 | final MangaCornerFlags? flags;
20 | final bool twoColumns;
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | return GeneralLineView(
25 | imageUrl: manga.cover,
26 | title: manga.title,
27 | icon1: Icons.edit,
28 | text1: manga.finished ? '已完结' : '连载中',
29 | icon2: Icons.notes,
30 | text2: '最新章节 ${manga.newestChapter}',
31 | icon3: Icons.update,
32 | text3: '更新于 ${manga.formattedNewestDateWithDuration}',
33 | cornerIcons: flags?.buildIcons(),
34 | twoColumns: twoColumns,
35 | onPressed: () => Navigator.of(context).push(
36 | CustomPageRoute(
37 | context: context,
38 | builder: (c) => MangaPage(
39 | id: manga.mid,
40 | title: manga.title,
41 | url: manga.url,
42 | ),
43 | ),
44 | ),
45 | onLongPressed: () => showPopupMenuForMangaList(
46 | context: context,
47 | mangaId: manga.mid,
48 | mangaTitle: manga.title,
49 | mangaCover: manga.cover,
50 | mangaUrl: manga.url,
51 | ),
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/service/prefs/read_message.dart:
--------------------------------------------------------------------------------
1 | import 'package:manhuagui_flutter/service/prefs/prefs_manager.dart';
2 | import 'package:shared_preferences/shared_preferences.dart';
3 |
4 | class ReadMessagePrefs {
5 | ReadMessagePrefs._();
6 |
7 | static const _readMessagesKey = StringListKey('ReadMessagePrefs_readMessageIds');
8 |
9 | static Future> getReadMessages() async {
10 | final prefs = await PrefsManager.instance.loadPrefs();
11 | return prefs.safeGet>(_readMessagesKey)?.map((e) => int.tryParse(e) ?? 0).toList() ?? [];
12 | }
13 |
14 | static Future clearReadMessages() async {
15 | final prefs = await PrefsManager.instance.loadPrefs();
16 | await prefs.safeSet>(_readMessagesKey, []);
17 | }
18 |
19 | static Future> addReadMessages(List mids) async {
20 | final prefs = await PrefsManager.instance.loadPrefs();
21 | var data = await getReadMessages();
22 | data.removeWhere((el) => mids.contains(el));
23 | data.addAll(mids);
24 | await prefs.safeSet>(_readMessagesKey, data.map((e) => e.toString()).toList());
25 | return data;
26 | }
27 |
28 | static Future> addReadMessage(int mid) async {
29 | return await addReadMessages([mid]);
30 | }
31 |
32 | static Future> removeReadMessage(int mid) async {
33 | final prefs = await PrefsManager.instance.loadPrefs();
34 | var data = await getReadMessages();
35 | data.removeWhere((h) => h == mid);
36 | await prefs.safeSet>(_readMessagesKey, data.map((e) => e.toString()).toList());
37 | return data;
38 | }
39 |
40 | static Future upgradeFromVer1To2(SharedPreferences prefs) async {
41 | // pass
42 | }
43 |
44 | static Future upgradeFromVer2To3(SharedPreferences prefs) async {
45 | await prefs.safeMigrate>('MessagePrefs_readMessageIds', _readMessagesKey, defaultValue: []);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lib/service/storage/queue_manager.dart:
--------------------------------------------------------------------------------
1 | import 'package:queue/queue.dart';
2 |
3 | class QueueManager {
4 | QueueManager._();
5 |
6 | static QueueManager? _instance;
7 |
8 | static QueueManager get instance {
9 | _instance ??= QueueManager._();
10 | return _instance!;
11 | }
12 |
13 | Queue? _queue;
14 | List>? _tasks;
15 |
16 | Queue get queue {
17 | if (_queue == null) {
18 | _queue = Queue(parallel: 1);
19 | _tasks = >[];
20 | }
21 | return _queue!;
22 | }
23 |
24 | List> get tasks {
25 | var _ = queue;
26 | return _tasks!;
27 | }
28 |
29 | Future addTask(QueueTask task) async {
30 | tasks.add(task);
31 | try {
32 | var result = await queue.add(() async {
33 | if (task.canceled) {
34 | return null; // canceled when not started
35 | }
36 | return await task.doTask();
37 | });
38 | return result;
39 | } catch (e, s) {
40 | if (e is QueueCancelledException) {
41 | return Future.value(null);
42 | }
43 | return Future.error(e, s);
44 | } finally {
45 | if (tasks.contains(task)) {
46 | tasks.remove(task);
47 | await task.doDefer(); // call doDefer only if contained
48 | }
49 | }
50 | }
51 | }
52 |
53 | abstract class QueueTask {
54 | var _canceled = false;
55 |
56 | bool get canceled => _canceled;
57 |
58 | void cancel() {
59 | _canceled = true;
60 | }
61 |
62 | Future doTask();
63 |
64 | Future doDefer() {
65 | return Future.value(null);
66 | }
67 | }
68 |
69 | class FuncQueueTask extends QueueTask {
70 | FuncQueueTask({
71 | required this.task,
72 | this.defer,
73 | });
74 |
75 | final Future Function() task;
76 | final Future Function()? defer;
77 |
78 | @override
79 | Future doTask() {
80 | return task.call();
81 | }
82 |
83 | @override
84 | Future doDefer() {
85 | return defer?.call() ?? Future.value(null);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/lib/page/view/genre_chip_list.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/category.dart';
4 | import 'package:manhuagui_flutter/page/sep_genre.dart';
5 | import 'package:manhuagui_flutter/page/view/full_ripple.dart';
6 |
7 | /// 漫画剧情类别列表,在 [RecommendSubPage] 使用
8 | class GenreChipListView extends StatelessWidget {
9 | const GenreChipListView({
10 | Key? key,
11 | required this.genres,
12 | }) : super(key: key);
13 |
14 | final List genres;
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | return Wrap(
19 | direction: Axis.horizontal,
20 | spacing: 10.0,
21 | runSpacing: 10.0,
22 | children: [
23 | for (var genre in genres)
24 | Container(
25 | decoration: ShapeDecoration(
26 | shape: StadiumBorder(),
27 | ),
28 | child: ClipPath.shape(
29 | shape: StadiumBorder(),
30 | child: FullRippleWidget(
31 | child: Chip(
32 | backgroundColor: Colors.deepOrange[50],
33 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
34 | label: Padding(
35 | padding: EdgeInsets.only(left: 1, right: 1, bottom: 1),
36 | child: Text('#${genre.title}'),
37 | ),
38 | shape: StadiumBorder(
39 | side: BorderSide(width: 1, color: Colors.transparent),
40 | ),
41 | ),
42 | highlightColor: null,
43 | splashColor: null,
44 | onTap: () => Navigator.of(context).push(
45 | CustomPageRoute(
46 | context: context,
47 | builder: (c) => SepGenrePage(
48 | genre: genre,
49 | ),
50 | ),
51 | ),
52 | ),
53 | ),
54 | ),
55 | ],
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/lib/page/view/custom_icons.dart:
--------------------------------------------------------------------------------
1 | /// Flutter icons CustomIcons
2 | /// Copyright (C) 2023 by original authors @ fluttericon.com, fontello.com
3 | /// This font was generated by FlutterIcon.com, which is derived from Fontello.
4 |
5 | import 'package:flutter/widgets.dart';
6 |
7 | // ignore_for_file: constant_identifier_names
8 |
9 | // https://www.fluttericon.com/
10 | // https://yqnn.github.io/svg-path-editor/
11 | // https://fonts.google.com/icons?selected=Material+Icons
12 | // https://pictogrammers.com/library/mdi/
13 |
14 | class CustomIcons {
15 | CustomIcons._();
16 |
17 | static const _kFontFam = 'CustomIcons';
18 | static const String? _kFontPkg = null;
19 |
20 | static const IconData opened_blank_book = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
21 | static const IconData opened_both_blank_book = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg);
22 | static const IconData opened_left_star_book = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg);
23 | static const IconData opened_right_star_book = IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg);
24 | static const IconData mdi_filled_notebook = IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg);
25 | static const IconData bookmark_plus = IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg);
26 | static const IconData bookmark_minus = IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg);
27 | static const IconData opened_book_cog = IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg);
28 | static const IconData download_cog = IconData(0xe808, fontFamily: _kFontFam, fontPackage: _kFontPkg);
29 | static const IconData application_star_cog = IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg);
30 | static const IconData eye_menu = IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
31 | static const IconData eye_download = IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
32 | static const IconData eye_sync = IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
33 | static const IconData eye_public = IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
34 | }
35 |
--------------------------------------------------------------------------------
/lib/model/user.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'user.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | Token _$TokenFromJson(Map json) => Token(
10 | token: json['token'] as String,
11 | );
12 |
13 | Map _$TokenToJson(Token instance) => {
14 | 'token': instance.token,
15 | };
16 |
17 | User _$UserFromJson(Map json) => User(
18 | username: json['username'] as String,
19 | avatar: json['avatar'] as String,
20 | className: json['class'] as String,
21 | score: json['score'] as int,
22 | accountPoint: json['account_point'] as int,
23 | unreadMessageCount: json['unread_message_count'] as int,
24 | loginIp: json['login_ip'] as String,
25 | lastLoginIp: json['last_login_ip'] as String,
26 | registerTime: json['register_time'] as String,
27 | lastLoginTime: json['last_login_time'] as String,
28 | cumulativeDayCount: json['cumulative_day_count'] as int,
29 | totalCommentCount: json['total_comment_count'] as int,
30 | );
31 |
32 | Map _$UserToJson(User instance) => {
33 | 'username': instance.username,
34 | 'avatar': instance.avatar,
35 | 'class': instance.className,
36 | 'score': instance.score,
37 | 'account_point': instance.accountPoint,
38 | 'unread_message_count': instance.unreadMessageCount,
39 | 'login_ip': instance.loginIp,
40 | 'last_login_ip': instance.lastLoginIp,
41 | 'register_time': instance.registerTime,
42 | 'last_login_time': instance.lastLoginTime,
43 | 'cumulative_day_count': instance.cumulativeDayCount,
44 | 'total_comment_count': instance.totalCommentCount,
45 | };
46 |
47 | LoginCheckResult _$LoginCheckResultFromJson(Map json) =>
48 | LoginCheckResult(
49 | username: json['username'] as String,
50 | );
51 |
52 | Map _$LoginCheckResultToJson(LoginCheckResult instance) =>
53 | {
54 | 'username': instance.username,
55 | };
56 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: manhuagui_flutter
2 | description: An unofficial android application for manhuagui, built in flutter.
3 |
4 | publish_to: 'none'
5 | version: 1.3.0
6 | environment:
7 | sdk: ">=2.16.2 <3.0.0"
8 |
9 | dependencies:
10 | flutter:
11 | sdk: flutter
12 |
13 | # basic
14 | material_design_icons_flutter: ^6.0.7096
15 | logger: ^1.2.2
16 | fluttertoast: 8.0.7
17 | url_launcher: ^6.1.5
18 | json_annotation: ^4.6.0
19 | dio: ^4.0.6
20 | retrofit: ^3.0.1+1
21 | intl: ^0.17.0
22 | basic_utils: ^5.2.2
23 |
24 | # platform assists
25 | permission_handler: ^10.0.0
26 | flutter_web_browser: ^0.17.1
27 | wakelock: ^0.6.2
28 | device_info_plus: 4.0.0
29 | battery_info: ^1.1.1
30 | connectivity_plus: 2.3.6+1
31 | flutter_local_notifications: 9.9.1
32 | flutter_native_splash: 2.1.6
33 |
34 | # widgets
35 | flutter_ahlib: ^1.3.0
36 | # flutter_ahlib:
37 | # path: ../flutter_ahlib
38 | # # git:
39 | # # url: https://github.com/Aoi-hosizora/flutter_ahlib
40 | # # ref: 090cbcf84bd45d3b113b99222fa67b1a4b277895
41 | flutter_cache_manager: ^3.3.0
42 | cached_network_image: ^3.2.0
43 | photo_view: ^0.14.0
44 | material_floating_search_bar: ^0.3.7
45 | flutter_rating_bar: ^4.0.1
46 | carousel_slider: ^4.1.1
47 | flutter_typeahead: 3.2.7
48 |
49 | # file system related and others
50 | path: 1.8.0
51 | path_provider: ^2.0.11
52 | external_path: ^1.0.1
53 | shared_preferences: ^2.0.15
54 | event_bus: ^2.0.0
55 | sqflite: ^2.0.2+1
56 | synchronized: ^3.0.0+3
57 | queue: ^3.1.0+1
58 |
59 | dependency_overrides:
60 | sqflite:
61 | path: ./deps/sqflite/sqflite
62 | photo_view:
63 | path: ./deps/photo_view
64 |
65 | dev_dependencies:
66 | flutter_test:
67 | sdk: flutter
68 | flutter_localizations:
69 | sdk: flutter
70 | flutter_lints: ^1.0.0
71 | build_runner: ^2.1.11
72 | json_serializable: ^6.2.0
73 | retrofit_generator: ^4.0.3+2
74 |
75 | flutter:
76 | uses-material-design: true
77 | assets:
78 | - lib/assets/
79 | fonts:
80 | - family: CustomIcons
81 | fonts:
82 | - asset: lib/assets/icons/CustomIcons.ttf
83 |
84 | flutter_native_splash:
85 | color: '#FFFFFF'
86 | image: lib/assets/splash_logo.png
87 | branding: lib/assets/splash_copyright.png
88 | web: false
89 |
--------------------------------------------------------------------------------
/lib/page/sep_shelf.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/page/page/subscribe_shelf.dart';
4 | import 'package:manhuagui_flutter/page/search.dart';
5 | import 'package:manhuagui_flutter/page/view/app_drawer.dart';
6 | import 'package:manhuagui_flutter/service/evb/evb_manager.dart';
7 | import 'package:manhuagui_flutter/service/evb/events.dart';
8 |
9 | /// 我的书架页,即 Separate [ShelfSubPage]
10 | class SepShelfPage extends StatefulWidget {
11 | const SepShelfPage({
12 | Key? key,
13 | }) : super(key: key);
14 |
15 | @override
16 | _SepShelfPageState createState() => _SepShelfPageState();
17 | }
18 |
19 | class _SepShelfPageState extends State {
20 | final _action = ActionController();
21 | final _cancelHandlers = [];
22 |
23 | @override
24 | void initState() {
25 | super.initState();
26 | _cancelHandlers.add(EventBusManager.instance.listen((_) => mountedSetState(() {})));
27 | }
28 |
29 | @override
30 | void dispose() {
31 | _action.dispose();
32 | _cancelHandlers.forEach((c) => c.call());
33 | super.dispose();
34 | }
35 |
36 | @override
37 | Widget build(BuildContext context) {
38 | return Scaffold(
39 | appBar: AppBar(
40 | title: Text('我的书架'),
41 | leading: AppBarActionButton.leading(context: context, allowDrawerButton: true),
42 | actions: [
43 | AppBarActionButton(
44 | icon: Icon(Icons.sync),
45 | tooltip: '同步我的书架',
46 | onPressed: () => _action.invoke('sync'),
47 | ),
48 | AppBarActionButton(
49 | icon: Icon(Icons.search),
50 | tooltip: '搜索漫画',
51 | onPressed: () => Navigator.of(context).push(
52 | CustomPageRoute(
53 | context: context,
54 | builder: (c) => SearchPage(),
55 | ),
56 | ),
57 | ),
58 | ],
59 | ),
60 | drawer: AppDrawer(
61 | currentSelection: DrawerSelection.shelf,
62 | ),
63 | drawerEdgeDragWidth: MediaQuery.of(context).size.width,
64 | body: ShelfSubPage(
65 | action: _action,
66 | isSepPage: true,
67 | ),
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lib/page/sep_history.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/page/page/subscribe_history.dart';
4 | import 'package:manhuagui_flutter/page/search.dart';
5 | import 'package:manhuagui_flutter/page/view/app_drawer.dart';
6 | import 'package:manhuagui_flutter/service/evb/evb_manager.dart';
7 | import 'package:manhuagui_flutter/service/evb/events.dart';
8 |
9 | /// 阅读历史页,即 Separate [HistorySubPage]
10 | class SepHistoryPage extends StatefulWidget {
11 | const SepHistoryPage({
12 | Key? key,
13 | }) : super(key: key);
14 |
15 | @override
16 | _SepHistoryPageState createState() => _SepHistoryPageState();
17 | }
18 |
19 | class _SepHistoryPageState extends State {
20 | final _action = ActionController();
21 | final _cancelHandlers = [];
22 |
23 | @override
24 | void initState() {
25 | super.initState();
26 | _cancelHandlers.add(EventBusManager.instance.listen((_) => mountedSetState(() {})));
27 | }
28 |
29 | @override
30 | void dispose() {
31 | _action.dispose();
32 | _cancelHandlers.forEach((c) => c.call());
33 | super.dispose();
34 | }
35 |
36 | @override
37 | Widget build(BuildContext context) {
38 | return Scaffold(
39 | appBar: AppBar(
40 | title: Text('阅读历史'),
41 | leading: AppBarActionButton.leading(context: context, allowDrawerButton: true),
42 | actions: [
43 | AppBarActionButton(
44 | icon: Icon(Icons.delete),
45 | tooltip: '清空阅读历史',
46 | onPressed: () => _action.invoke('clear'),
47 | ),
48 | AppBarActionButton(
49 | icon: Icon(Icons.search),
50 | tooltip: '搜索漫画',
51 | onPressed: () => Navigator.of(context).push(
52 | CustomPageRoute(
53 | context: context,
54 | builder: (c) => SearchPage(),
55 | ),
56 | ),
57 | ),
58 | ],
59 | ),
60 | drawer: AppDrawer(
61 | currentSelection: DrawerSelection.history,
62 | ),
63 | drawerEdgeDragWidth: MediaQuery.of(context).size.width,
64 | body: HistorySubPage(
65 | action: _action,
66 | isSepPage: true,
67 | ),
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lib/page/view/shelf_manga_line.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/entity.dart';
4 | import 'package:manhuagui_flutter/model/manga.dart';
5 | import 'package:manhuagui_flutter/page/manga.dart';
6 | import 'package:manhuagui_flutter/page/view/corner_icons.dart';
7 | import 'package:manhuagui_flutter/page/view/custom_icons.dart';
8 | import 'package:manhuagui_flutter/page/view/general_line.dart';
9 |
10 | /// 书架漫画行,在 [ShelfSubPage] 使用
11 | class ShelfMangaLineView extends StatelessWidget {
12 | const ShelfMangaLineView({
13 | Key? key,
14 | required this.manga,
15 | required this.history,
16 | this.useLocalHistory = false,
17 | this.flags,
18 | this.twoColumns = false,
19 | this.onLongPressed,
20 | }) : super(key: key);
21 |
22 | final ShelfManga manga;
23 | final MangaHistory? history;
24 | final bool useLocalHistory;
25 | final MangaCornerFlags? flags;
26 | final bool twoColumns;
27 | final VoidCallback? onLongPressed;
28 |
29 | @override
30 | Widget build(BuildContext context) {
31 | return GeneralLineView(
32 | imageUrl: manga.cover,
33 | title: manga.title,
34 | icon1: Icons.notes,
35 | text1: '最新章节 ' + manga.newestChapter,
36 | icon2: !useLocalHistory //
37 | ? CustomIcons.opened_blank_book
38 | : (history == null || !history!.read ? CustomIcons.opened_left_star_book : CustomIcons.opened_blank_book),
39 | text2: !useLocalHistory //
40 | ? '最近阅读至 ${manga.lastChapter.isEmpty ? '未知章节' : manga.lastChapter} (${manga.formattedLastDurationOrTime})'
41 | : ((history == null || !history!.read ? '未开始阅读' : '最近阅读至 ${history!.chapterTitle}') + ' (${history?.formattedLastTimeOrDuration ?? '未知时间'})'),
42 | icon3: Icons.update,
43 | text3: '更新于 ${manga.formattedNewestTimeWithDuration}',
44 | cornerIcons: flags?.buildIcons(),
45 | twoColumns: twoColumns,
46 | onPressed: () => Navigator.of(context).push(
47 | CustomPageRoute(
48 | context: context,
49 | builder: (c) => MangaPage(
50 | id: manga.mid,
51 | title: manga.title,
52 | url: manga.url,
53 | ),
54 | ),
55 | ),
56 | onLongPressed: onLongPressed,
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/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 | apply plugin: 'com.android.application'
15 | apply plugin: 'kotlin-android'
16 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
17 |
18 | def keystoreProperties = new Properties()
19 | def keystorePropertiesFile = rootProject.file("key.properties")
20 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
21 |
22 | android {
23 | compileSdkVersion 33 // flutter.compileSdkVersion
24 |
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_1_8
27 | targetCompatibility JavaVersion.VERSION_1_8
28 | }
29 |
30 | kotlinOptions {
31 | jvmTarget = '1.8'
32 | }
33 |
34 | sourceSets {
35 | main.java.srcDirs += 'src/main/kotlin'
36 | }
37 |
38 | defaultConfig {
39 | applicationId 'com.aoihosizora.manhuagui'
40 | // applicationId 'com.aoihosizora.manhuagui_test'
41 | // applicationId 'com.aoihosizora.manhuagui_beta'
42 | versionCode 7
43 | versionName '1.3.0'
44 |
45 | minSdkVersion 19 // flutter.minSdkVersion
46 | targetSdkVersion flutter.targetSdkVersion
47 | }
48 |
49 | signingConfigs {
50 | release {
51 | keyAlias keystoreProperties['keyAlias']
52 | keyPassword keystoreProperties['keyPassword']
53 | storeFile file(keystoreProperties['storeFile'])
54 | storePassword keystoreProperties['storePassword']
55 | }
56 | }
57 |
58 | buildTypes {
59 | release {
60 | signingConfig signingConfigs.release
61 |
62 | minifyEnabled true
63 | useProguard true
64 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
65 | }
66 | }
67 | }
68 |
69 | flutter {
70 | source '../..'
71 | }
72 |
73 | dependencies {
74 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
75 | }
76 |
--------------------------------------------------------------------------------
/lib/page/manga_group.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/manga.dart';
4 | import 'package:manhuagui_flutter/page/view/app_drawer.dart';
5 | import 'package:manhuagui_flutter/page/view/manga_group.dart';
6 |
7 | /// 漫画分组页,展示所给 [MangaGroupList] (三个 [MangaGroup]) 信息
8 | class MangaGroupPage extends StatefulWidget {
9 | const MangaGroupPage({
10 | Key? key,
11 | required this.groupList,
12 | }) : super(key: key);
13 |
14 | final MangaGroupList groupList;
15 |
16 | @override
17 | _MangaGroupPageState createState() => _MangaGroupPageState();
18 | }
19 |
20 | class _MangaGroupPageState extends State {
21 | final _controller = ScrollController();
22 | final _physicsController = CustomScrollPhysicsController();
23 |
24 | @override
25 | void dispose() {
26 | _controller.dispose();
27 | super.dispose();
28 | }
29 |
30 | @override
31 | Widget build(BuildContext context) {
32 | return DrawerScaffold(
33 | appBar: AppBar(
34 | title: Text('漫画分组'),
35 | leading: AppBarActionButton.leading(context: context, allowDrawerButton: false),
36 | ),
37 | drawer: AppDrawer(
38 | currentSelection: DrawerSelection.none,
39 | ),
40 | drawerEdgeDragWidth: null,
41 | physicsController: _physicsController,
42 | implicitlyOverscrollableScaffold: true,
43 | body: ExtendedScrollbar(
44 | controller: _controller,
45 | interactive: true,
46 | mainAxisMargin: 2,
47 | crossAxisMargin: 2,
48 | child: ListView(
49 | controller: _controller,
50 | padding: EdgeInsets.zero,
51 | physics: AlwaysScrollableScrollPhysics(),
52 | children: [
53 | DefaultScrollPhysics(
54 | physics: CustomScrollPhysics(controller: _physicsController),
55 | child: MangaGroupView(
56 | groupList: widget.groupList,
57 | style: MangaGroupViewStyle.normalFull,
58 | ),
59 | ),
60 | ],
61 | ),
62 | ),
63 | floatingActionButton: ScrollAnimatedFab(
64 | scrollController: _controller,
65 | condition: ScrollAnimatedCondition.direction,
66 | fab: FloatingActionButton(
67 | child: Icon(Icons.vertical_align_top),
68 | heroTag: null,
69 | onPressed: () => _controller.scrollToTop(),
70 | ),
71 | ),
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/page/manga_random.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/page/manga.dart';
4 | import 'package:manhuagui_flutter/service/dio/dio_manager.dart';
5 | import 'package:manhuagui_flutter/service/dio/retrofit.dart';
6 | import 'package:manhuagui_flutter/service/dio/wrap_error.dart';
7 |
8 | /// 随机漫画页,网络请求并展示 [RandomMangaInfo] 并跳转至 [MangaPage]
9 | class MangaRandomPage extends StatefulWidget {
10 | const MangaRandomPage({Key? key}) : super(key: key);
11 |
12 | @override
13 | State createState() => _MangaRandomPageState();
14 | }
15 |
16 | class _MangaRandomPageState extends State {
17 | @override
18 | void initState() {
19 | super.initState();
20 | WidgetsBinding.instance?.addPostFrameCallback((_) => _loadData());
21 | }
22 |
23 | Future _loadData() async {
24 | final client = RestClient(DioManager.instance.dio);
25 | try {
26 | var random = await client.getRandomManga();
27 | var mid = random.data.mid;
28 | var url = random.data.url;
29 | Navigator.of(context).pushReplacement(
30 | CustomPageRoute.fromTheme(
31 | themeData: CustomPageRouteTheme.of(context),
32 | builder: (c) => MangaPage(
33 | id: mid,
34 | title: '漫画 mid: $mid',
35 | url: url,
36 | ),
37 | ),
38 | );
39 | } catch (e, s) {
40 | var we = wrapError(e, s);
41 | showDialog(
42 | context: context,
43 | builder: (c) => AlertDialog(
44 | title: Text('随机漫画'),
45 | content: Text('无法获取随机漫画:${we.text}。'),
46 | actions: [
47 | TextButton(
48 | child: Text('确定'),
49 | onPressed: () {
50 | Navigator.of(c).pop(); // 本对话框
51 | Navigator.of(context).pop(); // 本页
52 | },
53 | ),
54 | ],
55 | ),
56 | );
57 | }
58 | }
59 |
60 | @override
61 | Widget build(BuildContext context) {
62 | return Scaffold(
63 | appBar: AppBar(
64 | title: Text('随机漫画'),
65 | leading: AppBarActionButton.leading(context: context),
66 | ),
67 | body: Center(
68 | child: SizedBox(
69 | height: 50,
70 | width: 50,
71 | child: Padding(
72 | padding: EdgeInsets.all(4.5 / 2),
73 | child: CircularProgressIndicator(
74 | strokeWidth: 4.5,
75 | ),
76 | ),
77 | ),
78 | ),
79 | );
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/lib/model/message.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 | import 'package:manhuagui_flutter/model/common.dart';
3 |
4 | part 'message.g.dart';
5 |
6 | @JsonSerializable(fieldRename: FieldRename.snake)
7 | class Message {
8 | final int mid;
9 | final String title;
10 | final NotificationContent? notification;
11 | final NewVersionContent? newVersion;
12 | final DateTime createdAt;
13 | final DateTime updatedAt;
14 |
15 | const Message({required this.mid, required this.title, required this.notification, required this.newVersion, required this.createdAt, required this.updatedAt});
16 |
17 | factory Message.fromJson(Map json) => _$MessageFromJson(json);
18 |
19 | Map toJson() => _$MessageToJson(this);
20 |
21 | String get createdAtString => //
22 | formatDatetimeAndDuration(createdAt.toLocal(), FormatPattern.datetime);
23 |
24 | String get updatedAtString => //
25 | formatDatetimeAndDuration(updatedAt.toLocal(), FormatPattern.datetime);
26 | }
27 |
28 | @JsonSerializable(fieldRename: FieldRename.snake)
29 | class NotificationContent {
30 | final String content;
31 | final bool dismissible;
32 | final String link;
33 |
34 | const NotificationContent({required this.content, required this.dismissible, required this.link});
35 |
36 | factory NotificationContent.fromJson(Map json) => _$NotificationContentFromJson(json);
37 |
38 | Map toJson() => _$NotificationContentToJson(this);
39 | }
40 |
41 | @JsonSerializable(fieldRename: FieldRename.snake)
42 | class NewVersionContent {
43 | final String version;
44 | final bool mustUpgrade;
45 | final String changeLogs;
46 | final String releasePage;
47 |
48 | const NewVersionContent({required this.version, required this.mustUpgrade, required this.changeLogs, required this.releasePage});
49 |
50 | factory NewVersionContent.fromJson(Map json) => _$NewVersionContentFromJson(json);
51 |
52 | Map toJson() => _$NewVersionContentToJson(this);
53 | }
54 |
55 | @JsonSerializable(fieldRename: FieldRename.snake)
56 | class LatestMessage {
57 | final Message? notification;
58 | final Message? newVersion;
59 | final Message? notDismissibleNotification;
60 | final Message? mustUpgradeNewVersion;
61 |
62 | const LatestMessage({required this.notification, required this.newVersion, required this.notDismissibleNotification, required this.mustUpgradeNewVersion});
63 |
64 | factory LatestMessage.fromJson(Map json) => _$LatestMessageFromJson(json);
65 |
66 | Map toJson() => _$LatestMessageToJson(this);
67 | }
68 |
--------------------------------------------------------------------------------
/lib/page/sep_favorite.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/page/page/subscribe_favorite.dart';
4 | import 'package:manhuagui_flutter/page/search.dart';
5 | import 'package:manhuagui_flutter/page/view/app_drawer.dart';
6 | import 'package:manhuagui_flutter/service/evb/evb_manager.dart';
7 | import 'package:manhuagui_flutter/service/evb/events.dart';
8 |
9 | /// 本地收藏页,即 Separate [FavoriteSubPage]
10 | class SepFavoritePage extends StatefulWidget {
11 | const SepFavoritePage({
12 | Key? key,
13 | }) : super(key: key);
14 |
15 | @override
16 | _SepFavoritePageState createState() => _SepFavoritePageState();
17 | }
18 |
19 | class _SepFavoritePageState extends State {
20 | final _action = ActionController();
21 | final _physicsController = CustomScrollPhysicsController();
22 | final _cancelHandlers = [];
23 |
24 | @override
25 | void initState() {
26 | super.initState();
27 | _cancelHandlers.add(EventBusManager.instance.listen((_) => mountedSetState(() {})));
28 | }
29 |
30 | @override
31 | void dispose() {
32 | _action.dispose();
33 | _cancelHandlers.forEach((c) => c.call());
34 | super.dispose();
35 | }
36 |
37 | @override
38 | Widget build(BuildContext context) {
39 | return DrawerScaffold(
40 | appBar: AppBar(
41 | title: Text('本地收藏'),
42 | leading: AppBarActionButton.leading(context: context, allowDrawerButton: true),
43 | actions: [
44 | AppBarActionButton(
45 | icon: Icon(Icons.bookmark_border),
46 | tooltip: '管理本地收藏',
47 | onPressed: () => _action.invoke('manage'),
48 | ),
49 | AppBarActionButton(
50 | icon: Icon(Icons.search),
51 | tooltip: '搜索漫画',
52 | onPressed: () => Navigator.of(context).push(
53 | CustomPageRoute(
54 | context: context,
55 | builder: (c) => SearchPage(),
56 | ),
57 | ),
58 | ),
59 | ],
60 | ),
61 | drawer: AppDrawer(
62 | currentSelection: DrawerSelection.favorite,
63 | ),
64 | drawerEdgeDragWidth: null,
65 | physicsController: _physicsController /* shared physics controller */,
66 | checkPhysicsControllerForOverscroll: true,
67 | implicitlyOverscrollableScaffold: true,
68 | implicitPageViewScrollPhysics: CustomScrollPhysics(controller: _physicsController),
69 | body: DefaultScrollPhysics(
70 | physics: CustomScrollPhysics(controller: _physicsController),
71 | child: FavoriteSubPage(
72 | action: _action,
73 | isSepPage: true,
74 | ),
75 | ),
76 | );
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/lib/model/chapter.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'chapter.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | MangaChapter _$MangaChapterFromJson(Map json) => MangaChapter(
10 | cid: json['cid'] as int,
11 | title: json['title'] as String,
12 | mid: json['mid'] as int,
13 | mangaTitle: json['manga_title'] as String,
14 | mangaCover: json['manga_cover'] as String,
15 | mangaUrl: json['manga_url'] as String,
16 | url: json['url'] as String,
17 | pages: (json['pages'] as List).map((e) => e as String).toList(),
18 | pageCount: json['page_count'] as int,
19 | nextCid: json['next_cid'] as int,
20 | prevCid: json['prev_cid'] as int,
21 | );
22 |
23 | Map _$MangaChapterToJson(MangaChapter instance) =>
24 | {
25 | 'cid': instance.cid,
26 | 'title': instance.title,
27 | 'mid': instance.mid,
28 | 'manga_title': instance.mangaTitle,
29 | 'manga_cover': instance.mangaCover,
30 | 'manga_url': instance.mangaUrl,
31 | 'url': instance.url,
32 | 'pages': instance.pages,
33 | 'page_count': instance.pageCount,
34 | 'next_cid': instance.nextCid,
35 | 'prev_cid': instance.prevCid,
36 | };
37 |
38 | TinyMangaChapter _$TinyMangaChapterFromJson(Map json) =>
39 | TinyMangaChapter(
40 | cid: json['cid'] as int,
41 | title: json['title'] as String,
42 | mid: json['mid'] as int,
43 | url: json['url'] as String,
44 | pageCount: json['page_count'] as int,
45 | isNew: json['is_new'] as bool,
46 | group: json['group'] as String,
47 | number: json['number'] as int,
48 | );
49 |
50 | Map _$TinyMangaChapterToJson(TinyMangaChapter instance) =>
51 | {
52 | 'cid': instance.cid,
53 | 'title': instance.title,
54 | 'mid': instance.mid,
55 | 'url': instance.url,
56 | 'page_count': instance.pageCount,
57 | 'is_new': instance.isNew,
58 | 'group': instance.group,
59 | 'number': instance.number,
60 | };
61 |
62 | MangaChapterGroup _$MangaChapterGroupFromJson(Map json) =>
63 | MangaChapterGroup(
64 | title: json['title'] as String,
65 | chapters: (json['chapters'] as List)
66 | .map((e) => TinyMangaChapter.fromJson(e as Map))
67 | .toList(),
68 | );
69 |
70 | Map _$MangaChapterGroupToJson(MangaChapterGroup instance) =>
71 | {
72 | 'title': instance.title,
73 | 'chapters': instance.chapters,
74 | };
75 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_ahlib/flutter_ahlib.dart';
2 | import 'package:flutter_localizations/flutter_localizations.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:logger/logger.dart';
5 | import 'package:manhuagui_flutter/config.dart';
6 | import 'package:manhuagui_flutter/page/index.dart';
7 | import 'package:manhuagui_flutter/page/splash.dart';
8 | import 'package:manhuagui_flutter/service/native/system_ui.dart';
9 |
10 | Future main() async {
11 | globalLogger = ExtendedLogger(filter: ProductionFilter(), printer: PreferredPrinter());
12 | CustomPageRouteTheme.allowThrowingUnsafeAncestorException = false;
13 | SplashPage.preserve(WidgetsFlutterBinding.ensureInitialized());
14 | await SplashPage.prepare();
15 |
16 | runApp(const MyApp());
17 | }
18 |
19 | class MyApp extends StatelessWidget {
20 | const MyApp({Key? key}) : super(key: key);
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | setDefaultSystemUIOverlayStyle();
25 | return MaterialApp(
26 | title: APP_NAME,
27 | theme: ThemeData(
28 | primarySwatch: Colors.deepOrange,
29 | appBarTheme: AppBarTheme(
30 | centerTitle: true,
31 | toolbarHeight: 45,
32 | ),
33 | scaffoldBackgroundColor: Color.fromRGBO(245, 245, 245, 1.0),
34 | splashFactory: CustomInkRipple.preferredSplashFactory,
35 | ).withPreferredButtonStyles(),
36 | debugShowCheckedModeBanner: false,
37 | localizationsDelegates: const [
38 | GlobalMaterialLocalizations.delegate,
39 | GlobalWidgetsLocalizations.delegate,
40 | GlobalCupertinoLocalizations.delegate,
41 | ],
42 | supportedLocales: const [
43 | Locale('ja', 'JP'),
44 | Locale('zh', 'CN'),
45 | ],
46 | home: SplashPage(home: IndexPage()),
47 | builder: (context, child) => CustomPageRouteTheme(
48 | data: CustomPageRouteThemeData(
49 | transitionDuration: Duration(milliseconds: 400),
50 | transitionsBuilder: NoPopGestureCupertinoPageTransitionsBuilder(),
51 | barrierColor: Colors.black38,
52 | barrierCurve: Curves.easeIn,
53 | disableCanTransitionTo: true,
54 | ),
55 | child: AppBarActionButtonTheme(
56 | data: AppBarActionButtonThemeData(
57 | splashRadius: 19,
58 | ),
59 | child: PlaceholderTextTheme(
60 | setting: PlaceholderSetting(
61 | useAnimatedSwitcher: true,
62 | switchDuration: Duration(milliseconds: 200),
63 | switchReverseDuration: Duration(milliseconds: 200),
64 | switchLayoutBuilder: switchLayoutBuilderWithSwitchedFlag,
65 | ).copyWithChinese(),
66 | child: child!,
67 | ),
68 | ),
69 | ),
70 | );
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/lib/page/view/shelf_cache_line.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/entity.dart';
4 | import 'package:manhuagui_flutter/page/view/full_ripple.dart';
5 | import 'package:manhuagui_flutter/page/view/network_image.dart';
6 |
7 | /// 缓存书架漫画行,在 [ShelfCacheSubPage] 使用
8 | class ShelfCacheLineView extends StatelessWidget {
9 | const ShelfCacheLineView({
10 | Key? key,
11 | required this.manga,
12 | required this.onPressed,
13 | this.onLongPressed,
14 | }) : super(key: key);
15 |
16 | final ShelfCache manga;
17 | final VoidCallback onPressed;
18 | final VoidCallback? onLongPressed;
19 |
20 | static double getChildAspectRatioForTwoColumns(BuildContext context) {
21 | // note: customRows (DownloadLineView) will never be used when calling getHeight
22 | var imageHeight = 48.0 + 10 * 2;
23 | var titleHeight = TextSpan(text: ' ', style: Theme.of(context).textTheme.subtitle1).layoutSize(context).height;
24 | var lineHeight = TextSpan(text: ' ', style: Theme.of(context).textTheme.bodyText2).layoutSize(context).height;
25 | var textHeight = titleHeight + lineHeight + 10 * 2;
26 |
27 | var height = imageHeight > textHeight ? imageHeight : textHeight;
28 | var width = MediaQuery.of(context).size.width / 2;
29 | return width / height;
30 | }
31 |
32 | @override
33 | Widget build(BuildContext context) {
34 | return FullRippleWidget(
35 | child: Padding(
36 | padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
37 | child: Row(
38 | children: [
39 | NetworkImageView(
40 | url: manga.mangaCover,
41 | height: 48,
42 | width: 48,
43 | radius: BorderRadius.circular(10),
44 | ),
45 | SizedBox(width: 16),
46 | Flexible(
47 | child: Column(
48 | crossAxisAlignment: CrossAxisAlignment.start,
49 | children: [
50 | Text(
51 | manga.mangaTitle,
52 | style: Theme.of(context).textTheme.subtitle1,
53 | maxLines: 1,
54 | overflow: TextOverflow.ellipsis,
55 | ),
56 | Text(
57 | '同步于 ${manga.formattedCachedAt}',
58 | style: Theme.of(context).textTheme.bodyText2?.copyWith(color: Colors.grey[600]),
59 | maxLines: 1,
60 | overflow: TextOverflow.ellipsis,
61 | ),
62 | ],
63 | ),
64 | ),
65 | ],
66 | ),
67 | ),
68 | highlightColor: null,
69 | splashColor: null,
70 | onTap: onPressed,
71 | onLongPress: onLongPressed,
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
22 |
23 |
32 |
33 |
37 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
50 |
53 |
54 |
59 |
60 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/lib/model/order.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 |
3 | enum MangaOrder {
4 | /// 人气最旺
5 | @JsonValue('popular')
6 | byPopular,
7 |
8 | /// 最新发布
9 | @JsonValue('new')
10 | byNew,
11 |
12 | /// 最近更新
13 | @JsonValue('update')
14 | byUpdate,
15 | }
16 |
17 | enum AuthorOrder {
18 | /// 人气最旺
19 | @JsonValue('popular')
20 | byPopular,
21 |
22 | /// 作品最多
23 | @JsonValue('comic')
24 | byComic,
25 |
26 | /// 最新收录
27 | @JsonValue('update') // "update" 表示 "最新收录" 为后端命名原因
28 | byNew,
29 | }
30 |
31 | extension MangaOrderExtension on MangaOrder {
32 | String toJson() {
33 | switch (this) {
34 | case MangaOrder.byPopular:
35 | return 'popular';
36 | case MangaOrder.byNew:
37 | return 'new';
38 | case MangaOrder.byUpdate:
39 | return 'update';
40 | }
41 | }
42 |
43 | String toTitle() {
44 | switch (this) {
45 | case MangaOrder.byPopular:
46 | return '人气最旺';
47 | case MangaOrder.byNew:
48 | return '最新发布';
49 | case MangaOrder.byUpdate:
50 | return '最近更新';
51 | }
52 | }
53 |
54 | int toInt() {
55 | switch (this) {
56 | case MangaOrder.byPopular:
57 | return 0;
58 | case MangaOrder.byNew:
59 | return 1;
60 | case MangaOrder.byUpdate:
61 | return 2;
62 | }
63 | }
64 |
65 | static MangaOrder fromInt(int i) {
66 | switch (i) {
67 | case 0:
68 | return MangaOrder.byPopular;
69 | case 1:
70 | return MangaOrder.byNew;
71 | case 2:
72 | return MangaOrder.byUpdate;
73 | }
74 | return MangaOrder.byPopular;
75 | }
76 | }
77 |
78 | extension AuthorOrderExtension on AuthorOrder {
79 | String toJson() {
80 | switch (this) {
81 | case AuthorOrder.byPopular:
82 | return 'popular';
83 | case AuthorOrder.byComic:
84 | return 'comic';
85 | case AuthorOrder.byNew:
86 | return 'update'; // "update" 表示 "最新收录" 为后端命名原因
87 | }
88 | }
89 |
90 | String toTitle() {
91 | switch (this) {
92 | case AuthorOrder.byPopular:
93 | return '人气最旺';
94 | case AuthorOrder.byComic:
95 | return '作品最多';
96 | case AuthorOrder.byNew:
97 | return '最新收录';
98 | }
99 | }
100 |
101 | int toInt() {
102 | switch (this) {
103 | case AuthorOrder.byPopular:
104 | return 0;
105 | case AuthorOrder.byComic:
106 | return 1;
107 | case AuthorOrder.byNew:
108 | return 2;
109 | }
110 | }
111 |
112 | static AuthorOrder fromInt(int i) {
113 | switch (i) {
114 | case 0:
115 | return AuthorOrder.byPopular;
116 | case 1:
117 | return AuthorOrder.byComic;
118 | case 2:
119 | return AuthorOrder.byNew;
120 | }
121 | return AuthorOrder.byPopular;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/lib/model/comment.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'comment.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | Comment _$CommentFromJson(Map json) => Comment(
10 | cid: json['cid'] as int,
11 | uid: json['uid'] as int,
12 | username: json['username'] as String,
13 | avatar: json['avatar'] as String,
14 | gender: json['gender'] as int,
15 | content: json['content'] as String,
16 | likeCount: json['like_count'] as int,
17 | replyCount: json['reply_count'] as int,
18 | commentTime: json['comment_time'] as String,
19 | replyTimeline: (json['reply_timeline'] as List)
20 | .map((e) => RepliedComment.fromJson(e as Map))
21 | .toList(),
22 | );
23 |
24 | Map _$CommentToJson(Comment instance) => {
25 | 'cid': instance.cid,
26 | 'uid': instance.uid,
27 | 'username': instance.username,
28 | 'avatar': instance.avatar,
29 | 'gender': instance.gender,
30 | 'content': instance.content,
31 | 'like_count': instance.likeCount,
32 | 'reply_count': instance.replyCount,
33 | 'comment_time': instance.commentTime,
34 | 'reply_timeline': instance.replyTimeline,
35 | };
36 |
37 | RepliedComment _$RepliedCommentFromJson(Map json) =>
38 | RepliedComment(
39 | cid: json['cid'] as int,
40 | uid: json['uid'] as int,
41 | username: json['username'] as String,
42 | avatar: json['avatar'] as String,
43 | gender: json['gender'] as int,
44 | content: json['content'] as String,
45 | likeCount: json['like_count'] as int,
46 | replyCount: json['reply_count'] as int,
47 | commentTime: json['comment_time'] as String,
48 | );
49 |
50 | Map _$RepliedCommentToJson(RepliedComment instance) =>
51 | {
52 | 'cid': instance.cid,
53 | 'uid': instance.uid,
54 | 'username': instance.username,
55 | 'avatar': instance.avatar,
56 | 'gender': instance.gender,
57 | 'content': instance.content,
58 | 'like_count': instance.likeCount,
59 | 'reply_count': instance.replyCount,
60 | 'comment_time': instance.commentTime,
61 | };
62 |
63 | AddedComment _$AddedCommentFromJson(Map json) => AddedComment(
64 | cid: json['cid'] as int,
65 | mid: json['mid'] as int,
66 | repliedCid: json['replied_cid'] as int,
67 | content: json['content'] as String,
68 | );
69 |
70 | Map _$AddedCommentToJson(AddedComment instance) =>
71 | {
72 | 'cid': instance.cid,
73 | 'mid': instance.mid,
74 | 'replied_cid': instance.repliedCid,
75 | 'content': instance.content,
76 | };
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # manhuagui_flutter
2 |
3 | [](https://github.com/Aoi-hosizora/manhuagui_flutter/releases)
4 | [](./LICENSE)
5 |
6 | + An unofficial android application for manhuagui (https://www.manhuagui.com/), powered by flutter.
7 | + Develop environment: `Flutter 2.10.5 channel stable / Dart 2.16.2`, visit [pubspec.yaml](./pubspec.yaml) for package dependencies.
8 |
9 | ### Related repositories
10 |
11 | + [Aoi-hosizora/manhuagui_api](https://github.com/Aoi-hosizora/manhuagui_api): The manhuagui backend used by this client.
12 | + [Aoi-hosizora/flutter_ahlib](https://github.com/Aoi-hosizora/flutter_ahlib): A personal flutter widgets and utilities library, which is used in this project.
13 |
14 | ### Install
15 |
16 | + Visit [Release](https://github.com/Aoi-hosizora/manhuagui_flutter/releases) for released versions.
17 |
18 | ### Build manually
19 |
20 | ```bash
21 | cd manhuagui_flutter
22 |
23 | # Process dependencies
24 | sh process_deps.sh
25 | flutter pub get
26 |
27 | # Build apk in release mode
28 | flutter build apk
29 | ```
30 |
31 | ### Tips
32 |
33 | + Manhuagui is a free and open-source software, and is released under the **MIT License**, but commercial use is **FORBIDDEN**.
34 | + Copyright (c) 2020-2023 AoiHosizora (青いほしぞら). Visit [LICENSE](./LICENSE) for details.
35 | + Note: Some UI in this application is referenced from [动漫之家](http://www.idmzj.com/) and [ガンマ!](https://ganma.jp/) android client.
36 | + Some disclaimers and statements in Simplified Chinese:
37 | 1. 本应用仅供学习使用,客户端和服务端代码完全开源,仅供非商业用途。
38 | 2. 本应用与漫画柜内容提供方无任何关系,若有问题,请发邮件或 Issue 联系。
39 |
40 | ### Screenshots
41 |
42 | |  |  |  |  |  |  |  |
43 | |--------------------------------------------|--------------------------------------------|--------------------------------------------|--------------------------------------------|--------------------------------------------|--------------------------------------------|--------------------------------------------|
44 | |  |  |  |  |  |  |  |
45 | |  |  |  |  |  |  |  |
46 |
--------------------------------------------------------------------------------
/lib/page/author_detail.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/author.dart';
4 | import 'package:manhuagui_flutter/page/view/detail_table.dart';
5 |
6 | /// 作者详情页,展示所给 [Author] 信息
7 | class AuthorDetailPage extends StatefulWidget {
8 | const AuthorDetailPage({
9 | Key? key,
10 | required this.data,
11 | }) : super(key: key);
12 |
13 | final Author data;
14 |
15 | @override
16 | _AuthorDetailPageState createState() => _AuthorDetailPageState();
17 | }
18 |
19 | class _AuthorDetailPageState extends State {
20 | final _controller = ScrollController();
21 |
22 | @override
23 | void dispose() {
24 | _controller.dispose();
25 | super.dispose();
26 | }
27 |
28 | @override
29 | Widget build(BuildContext context) {
30 | return Scaffold(
31 | appBar: AppBar(
32 | title: Text('作者详情'),
33 | leading: AppBarActionButton.leading(context: context),
34 | ),
35 | body: ExtendedScrollbar(
36 | controller: _controller,
37 | interactive: true,
38 | mainAxisMargin: 2,
39 | crossAxisMargin: 2,
40 | child: ListView(
41 | controller: _controller,
42 | padding: EdgeInsets.symmetric(horizontal: 20, vertical: 15),
43 | physics: AlwaysScrollableScrollPhysics(),
44 | children: [
45 | DetailTableView(
46 | rows: [
47 | DetailRow('aid', widget.data.aid.toString()),
48 | DetailRow('作者名', widget.data.name),
49 | DetailRow('作者别名', widget.data.alias.trim().isNotEmpty ? widget.data.alias.trim() : '暂无'),
50 | DetailRow('所属地区', widget.data.zone),
51 | DetailRow('网页链接', widget.data.url),
52 | DetailRow('人气指数', widget.data.popularity.toString()),
53 | DetailRow('收录漫画数', widget.data.mangaCount.toString()),
54 | DetailRow('收录更新时间', widget.data.formattedNewestDate),
55 | DetailRow('最新收录漫画', '《${widget.data.newestMangaTitle}》mid: ${widget.data.newestMangaId}'),
56 | DetailRow('评分最高漫画', '《${widget.data.highestMangaTitle}》mid: ${widget.data.highestMangaId}'),
57 | DetailRow('最高评分', widget.data.highestScore.toString()),
58 | DetailRow('平均评分', widget.data.averageScore.toString()),
59 | DetailRow('作者介绍', widget.data.introduction.trim().isNotEmpty ? widget.data.introduction.trim() : '暂无'),
60 | ],
61 | tableWidth: MediaQuery.of(context).size.width - MediaQuery.of(context).padding.horizontal - 40,
62 | ),
63 | ],
64 | ),
65 | ),
66 | floatingActionButton: ScrollAnimatedFab(
67 | scrollController: _controller,
68 | condition: ScrollAnimatedCondition.direction,
69 | fab: FloatingActionButton(
70 | child: Icon(Icons.vertical_align_top),
71 | heroTag: null,
72 | onPressed: () => _controller.scrollToTop(),
73 | ),
74 | ),
75 | );
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/lib/model/comment.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 | import 'package:manhuagui_flutter/config.dart';
3 | import 'package:manhuagui_flutter/model/common.dart';
4 |
5 | part 'comment.g.dart';
6 |
7 | @JsonSerializable(fieldRename: FieldRename.snake)
8 | class Comment {
9 | final int cid;
10 | final int uid;
11 | final String username;
12 | final String avatar;
13 | final int gender;
14 | final String content;
15 | final int likeCount;
16 | final int replyCount;
17 | final String commentTime;
18 | final List replyTimeline;
19 |
20 | const Comment({required this.cid, required this.uid, required this.username, required this.avatar, required this.gender, required this.content, required this.likeCount, required this.replyCount, required this.commentTime, required this.replyTimeline});
21 |
22 | factory Comment.fromJson(Map json) => _$CommentFromJson(json);
23 |
24 | Map toJson() => _$CommentToJson(this);
25 |
26 | String get formattedCommentTime => commentTime.replaceAll('-', '/');
27 | }
28 |
29 | @JsonSerializable(fieldRename: FieldRename.snake)
30 | class RepliedComment {
31 | final int cid;
32 | final int uid;
33 | final String username;
34 | final String avatar;
35 | final int gender;
36 | final String content;
37 | final int likeCount;
38 | final int replyCount;
39 | final String commentTime;
40 |
41 | const RepliedComment({required this.cid, required this.uid, required this.username, required this.avatar, required this.gender, required this.content, required this.likeCount, required this.replyCount, required this.commentTime});
42 |
43 | factory RepliedComment.fromJson(Map json) => _$RepliedCommentFromJson(json);
44 |
45 | Map toJson() => _$RepliedCommentToJson(this);
46 |
47 | String get formattedCommentTime => commentTime.replaceAll('-', '/');
48 |
49 | Comment toComment() {
50 | return Comment(cid: cid, uid: uid, username: username, avatar: avatar, gender: gender, content: content, likeCount: likeCount, replyCount: replyCount, commentTime: commentTime, replyTimeline: []);
51 | }
52 | }
53 |
54 | @JsonSerializable(fieldRename: FieldRename.snake)
55 | class AddedComment {
56 | final int cid;
57 | final int mid;
58 | final int repliedCid;
59 | final String content;
60 |
61 | const AddedComment({required this.cid, required this.mid, required this.repliedCid, required this.content});
62 |
63 | factory AddedComment.fromJson(Map json) => _$AddedCommentFromJson(json);
64 |
65 | Map toJson() => _$AddedCommentToJson(this);
66 |
67 | RepliedComment toRepliedComment({required String username, required DateTime time}) {
68 | return RepliedComment(
69 | cid: cid,
70 | uid: -1 /* <<< useless */,
71 | username: username,
72 | avatar: DEFAULT_USER_AVATAR_URL,
73 | gender: -1 /* <<< do not show */,
74 | content: content,
75 | likeCount: 0,
76 | replyCount: 0,
77 | commentTime: formatDatetimeAndDuration(time, FormatPattern.datetime),
78 | );
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/lib/config.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: constant_identifier_names, non_constant_identifier_names
2 |
3 | // app metadata
4 | const APP_NAME = 'Manhuagui';
5 | const APP_VERSION = '1.3.0';
6 | const APP_LEGALESE = 'Copyright © 2020-2023 AoiHosizora';
7 | const APP_DESCRIPTION = //
8 | '第三方漫画柜 ($WEB_HOMEPAGE_URL) 安卓客户端,使用 Flutter 开发,当前版本为 $APP_VERSION。\n'
9 | '\n'
10 | '开发者: GitHub @Aoi-hosizora (青いほしぞら) \n'
11 | '\n'
12 | '本应用仅供学习使用,客户端和服务端代码完全开源,仅供非商业用途。\n'
13 | '\n'
14 | '本应用与漫画柜内容提供方无任何关系,若有问题,请发邮件或 Issue 联系。';
15 |
16 | // data and related
17 | const ASSETS_PREFIX = 'lib/assets/';
18 | const DB_NAME = 'db_manhuagui';
19 | const DL_NTFC_ID = 'com.aoihosizora.manhuagui:download';
20 | const DL_NTFC_NAME = '漫画下载通知';
21 | const DL_NTFC_DESCRIPTION = '显示当前的漫画下载进度';
22 | const LOG_CONSOLE_BUFFER = 200;
23 |
24 | // network timeout
25 | const CONNECT_TIMEOUT = 8000; // 8.0s (local -> my server)
26 | const SEND_TIMEOUT = 8000; // 8.0s (local -> my server)
27 | const RECEIVE_TIMEOUT = 10000; // 10.0s (my server -> manhuagui server -> my server -> local)
28 | const DOWNLOAD_HEAD_TIMEOUT = 5000; // 5.0s (local -> manhuagui server -> local)
29 | const DOWNLOAD_IMAGE_TIMEOUT = 12000; // 12.0s (local -> manhuagui server -> local)
30 | const GALLERY_IMAGE_TIMEOUT = 15000; // 15.0s (local -> manhuagui server -> local)
31 | // => LTIMEOUT
32 | final CONNECT_LTIMEOUT = (CONNECT_TIMEOUT * 1.5).toInt();
33 | final SEND_LTIMEOUT = (SEND_TIMEOUT * 1.5).toInt();
34 | final RECEIVE_LTIMEOUT = (RECEIVE_TIMEOUT * 1.5).toInt();
35 | final DOWNLOAD_HEAD_LTIMEOUT = (DOWNLOAD_HEAD_TIMEOUT * 1.5).toInt();
36 | final DOWNLOAD_IMAGE_LTIMEOUT = (DOWNLOAD_IMAGE_TIMEOUT * 1.5).toInt();
37 | final GALLERY_IMAGE_LTIMEOUT = (GALLERY_IMAGE_TIMEOUT * 1.5).toInt();
38 | // => LLTIMEOUT
39 | const CONNECT_LLTIMEOUT = CONNECT_TIMEOUT * 2;
40 | const SEND_LLTIMEOUT = SEND_TIMEOUT * 2;
41 | const RECEIVE_LLTIMEOUT = RECEIVE_TIMEOUT * 2;
42 | const DOWNLOAD_HEAD_LLTIMEOUT = DOWNLOAD_HEAD_TIMEOUT * 2;
43 | const DOWNLOAD_IMAGE_LLTIMEOUT = DOWNLOAD_IMAGE_TIMEOUT * 2;
44 | const GALLERY_IMAGE_LLTIMEOUT = GALLERY_IMAGE_TIMEOUT * 2;
45 |
46 | // api related
47 | const BASE_API_URL = 'https://api-manhuagui.aoihosizora.top/v1/';
48 | const BASE_API_PURE_URL = 'https://api-manhuagui.aoihosizora.top/';
49 | const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36';
50 | const REFERER = 'https://www.manhuagui.com/';
51 | const DEFAULT_USER_AVATAR_URL = 'https://cf.hamreus.com/images/default.png';
52 | const DEFAULT_AUTHOR_COVER_URL = 'https://cf.hamreus.com/zpic/none.jpg';
53 |
54 | // website urls
55 | const WEB_HOMEPAGE_URL = 'https://www.manhuagui.com/';
56 | const USER_CENTER_URL = 'https://www.manhuagui.com/user/center/index';
57 | const MESSAGE_URL = 'https://www.manhuagui.com/user/message/system';
58 | const EDIT_PROFILE_URL = 'https://www.manhuagui.com/user/center/proinfo';
59 | const REGISTER_URL = 'https://www.manhuagui.com/user/register';
60 | const SOURCE_CODE_URL = 'https://github.com/Aoi-hosizora/manhuagui_flutter';
61 | const FEEDBACK_URL = 'https://github.com/Aoi-hosizora/manhuagui_flutter/issues';
62 | const RELEASE_URL = 'https://github.com/Aoi-hosizora/manhuagui_flutter/releases';
63 |
--------------------------------------------------------------------------------
/lib/page/view/favorite_manga_line.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/entity.dart';
4 | import 'package:manhuagui_flutter/page/manga.dart';
5 | import 'package:manhuagui_flutter/page/view/corner_icons.dart';
6 | import 'package:manhuagui_flutter/page/view/custom_icons.dart';
7 | import 'package:manhuagui_flutter/page/view/general_line.dart';
8 |
9 | /// 收藏漫画行,在 [FavoriteSubPage] 使用
10 | class FavoriteMangaLineView extends StatelessWidget {
11 | const FavoriteMangaLineView({
12 | Key? key,
13 | required this.manga,
14 | required this.index,
15 | required this.history,
16 | this.flags,
17 | this.twoColumns = false,
18 | required this.onLongPressed,
19 | }) : super(key: key);
20 |
21 | final FavoriteManga manga;
22 | final int? index;
23 | final MangaHistory? history;
24 | final MangaCornerFlags? flags;
25 | final bool twoColumns;
26 | final void Function()? onLongPressed;
27 |
28 | @override
29 | Widget build(BuildContext context) {
30 | return GeneralLineView(
31 | imageUrl: manga.mangaCover,
32 | title: manga.mangaTitle,
33 | icon1: Icons.folder_open,
34 | text1: '${manga.checkedGroupName}${manga.remark.trim().isEmpty ? '' : '・备注 ${manga.remark.trim()}'}',
35 | icon2: history == null || !history!.read ? CustomIcons.opened_left_star_book : Icons.import_contacts,
36 | text2: (history == null || !history!.read ? '未开始阅读' : '最近阅读至 ${history!.chapterTitle}') + ' (${history?.formattedLastTimeOrDuration ?? '未知时间'})',
37 | icon3: Icons.access_time,
38 | text3: '收藏于 ${manga.formattedCreatedAtWithDuration}',
39 | cornerIcons: flags?.buildIcons(),
40 | twoColumns: twoColumns,
41 | extraRightPaddingForTitle: index != null
42 | ? 28 - 14 + 5 // badge width - line horizontal padding + extra space
43 | : null /* no extra padding */,
44 | extrasInStack: [
45 | if (index != null)
46 | Positioned(
47 | top: 0,
48 | right: 0,
49 | child: Container(
50 | width: 28,
51 | height: 28,
52 | decoration: BoxDecoration(
53 | color: Colors.orange,
54 | borderRadius: BorderRadius.only(bottomLeft: Radius.circular(28)),
55 | ),
56 | alignment: Alignment.topRight,
57 | child: SizedBox(
58 | width: 28 * 0.8,
59 | height: 28 * 0.85,
60 | child: Center(
61 | child: Text(
62 | index!.toString(),
63 | // manga.order.toString(),
64 | style: Theme.of(context).textTheme.bodyText2?.copyWith(fontSize: index! < 100 ? null : 12.5, color: Colors.white),
65 | ),
66 | ),
67 | ),
68 | ),
69 | ),
70 | ],
71 | onPressed: () => Navigator.of(context).push(
72 | CustomPageRoute(
73 | context: context,
74 | builder: (c) => MangaPage(
75 | id: manga.mangaId,
76 | title: manga.mangaTitle,
77 | url: manga.mangaUrl,
78 | ),
79 | ),
80 | ),
81 | onLongPressed: onLongPressed,
82 | );
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/lib/page/view/list_hint.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | enum ListHintViewStyle {
4 | textText,
5 | textWidget,
6 | widgets,
7 | }
8 |
9 | class ListHintView extends StatelessWidget {
10 | const ListHintView.textText({
11 | Key? key,
12 | required String this.leftText,
13 | required String this.rightText,
14 | this.padding,
15 | }) : rightWidget = null,
16 | widgets = null,
17 | style = ListHintViewStyle.textText,
18 | super(key: key);
19 |
20 | const ListHintView.textWidget({
21 | Key? key,
22 | required String this.leftText,
23 | required Widget this.rightWidget,
24 | this.padding,
25 | }) : rightText = null,
26 | widgets = null,
27 | style = ListHintViewStyle.textWidget,
28 | super(key: key);
29 |
30 | const ListHintView.widgets({
31 | Key? key,
32 | required List this.widgets,
33 | this.padding,
34 | }) : leftText = null,
35 | rightText = null,
36 | rightWidget = null,
37 | style = ListHintViewStyle.widgets,
38 | super(key: key);
39 |
40 | final String? leftText;
41 | final String? rightText;
42 | final Widget? rightWidget;
43 | final List? widgets;
44 | final EdgeInsets? padding;
45 | final ListHintViewStyle style;
46 |
47 | @override
48 | Widget build(BuildContext context) {
49 | return Column(
50 | children: [
51 | Container(
52 | color: Colors.white,
53 | padding: padding ?? EdgeInsets.symmetric(horizontal: 10, vertical: 5),
54 | child: Row(
55 | mainAxisAlignment: style == ListHintViewStyle.widgets
56 | ? MainAxisAlignment.spaceAround /* | ▢ ▢ ▢ | */
57 | : MainAxisAlignment.spaceBetween /* | ▢ ▢ | */,
58 | crossAxisAlignment: CrossAxisAlignment.center,
59 | children: style == ListHintViewStyle.textText
60 | ? [
61 | Flexible(
62 | child: Padding(
63 | padding: EdgeInsets.only(left: 5),
64 | child: Text(
65 | leftText!,
66 | overflow: TextOverflow.ellipsis,
67 | ),
68 | ),
69 | ),
70 | SizedBox(height: 26, width: 20),
71 | Padding(
72 | padding: EdgeInsets.only(right: 5),
73 | child: Text(rightText!),
74 | ),
75 | ]
76 | : style == ListHintViewStyle.textWidget
77 | ? [
78 | Flexible(
79 | child: Padding(
80 | padding: EdgeInsets.only(left: 5),
81 | child: Text(
82 | leftText!,
83 | overflow: TextOverflow.ellipsis,
84 | ),
85 | ),
86 | ),
87 | SizedBox(height: 26, width: 15),
88 | rightWidget!,
89 | ]
90 | : widgets!,
91 | ),
92 | ),
93 | Divider(height: 0, thickness: 1),
94 | ],
95 | );
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/lib/service/db/query_helper.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_ahlib/flutter_ahlib.dart';
2 |
3 | class QueryHelper {
4 | static Tuple2>? buildLikeStatement(List columns, String? keyword, {bool includeWHERE = false, bool includeAND = false}) {
5 | if (keyword == null) {
6 | return null;
7 | }
8 | keyword = keyword.trim();
9 | if (keyword.isEmpty) {
10 | return null;
11 | }
12 |
13 | var keywords = keyword.replaceAll('\\', '\\\\').replaceAll('%', '\\%').replaceAll('_', '\\_').split(' ').toSet().toList();
14 | if (keywords.isEmpty) {
15 | return null;
16 | }
17 |
18 | var statements = [];
19 | var arguments = [];
20 | for (var column in columns) {
21 | statements.addAll([for (var _ in keywords) '`$column` LIKE ? ESCAPE \'\\\'']);
22 | arguments.addAll([for (var keyword in keywords) '%$keyword%']);
23 | }
24 | var statement = statements.join(' OR ');
25 | statement = (includeWHERE ? ' WHERE ' : ' ') + (includeAND ? ' AND ' : ' ') + '($statement)';
26 | return Tuple2(statement, arguments);
27 | }
28 |
29 | static String? buildOrderByStatement(SortMethod sortMethod, {String? idColumn, String? nameColumn, String? timeColumn, String? orderColumn, bool includeORDERBY = false}) {
30 | var desc = sortMethod.isDesc();
31 | var column = sortMethod.toAsc().let((sort) => //
32 | sort == SortMethod.byIdAsc
33 | ? idColumn
34 | : sort == SortMethod.byNameAsc
35 | ? nameColumn
36 | : sort == SortMethod.byTimeAsc
37 | ? timeColumn
38 | : orderColumn);
39 | var byName = sortMethod == SortMethod.byNameAsc || sortMethod == SortMethod.byNameDesc;
40 | if (column == null) {
41 | return null;
42 | }
43 |
44 | var statement = '`$column`' + (byName ? ' COLLATE LOCALIZED' : '') + (!desc ? ' ASC' : ' DESC');
45 | statement = (includeORDERBY ? ' ORDER BY ' : '') + statement;
46 | return statement;
47 | }
48 | }
49 |
50 | enum SortMethod {
51 | byIdAsc,
52 | byIdDesc,
53 | byNameAsc,
54 | byNameDesc,
55 | byTimeAsc,
56 | byTimeDesc,
57 | byOrderAsc,
58 | byOrderDesc,
59 | }
60 |
61 | extension SortMethodExtension on SortMethod {
62 | bool isAsc() {
63 | return this == SortMethod.byIdAsc || this == SortMethod.byNameAsc || this == SortMethod.byTimeAsc || this == SortMethod.byOrderAsc;
64 | }
65 |
66 | bool isDesc() {
67 | return this == SortMethod.byIdDesc || this == SortMethod.byNameDesc || this == SortMethod.byTimeDesc || this == SortMethod.byOrderDesc;
68 | }
69 |
70 | SortMethod toAsc() {
71 | if (isAsc()) {
72 | return this;
73 | }
74 | return this == SortMethod.byIdDesc
75 | ? SortMethod.byIdAsc
76 | : this == SortMethod.byNameDesc
77 | ? SortMethod.byNameAsc
78 | : this == SortMethod.byTimeDesc
79 | ? SortMethod.byTimeAsc
80 | : SortMethod.byOrderAsc;
81 | }
82 |
83 | SortMethod toDesc() {
84 | if (isDesc()) {
85 | return this;
86 | }
87 | return this == SortMethod.byIdAsc
88 | ? SortMethod.byIdDesc
89 | : this == SortMethod.byNameAsc
90 | ? SortMethod.byNameDesc
91 | : this == SortMethod.byTimeAsc
92 | ? SortMethod.byTimeDesc
93 | : SortMethod.byOrderDesc;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/lib/model/author.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 | import 'package:manhuagui_flutter/model/common.dart';
3 |
4 | part 'author.g.dart';
5 |
6 | @JsonSerializable(fieldRename: FieldRename.snake)
7 | class Author {
8 | final int aid;
9 | final String name;
10 | final String alias;
11 | final String zone;
12 | final String cover;
13 | final String url;
14 | final int mangaCount;
15 | final int newestMangaId;
16 | final String newestMangaTitle;
17 | final String newestMangaUrl;
18 | final String newestDate;
19 | final int highestMangaId;
20 | final String highestMangaTitle;
21 | final String highestMangaUrl;
22 | final double highestScore;
23 | final double averageScore;
24 | final int popularity;
25 | final String introduction;
26 | final List relatedAuthors;
27 |
28 | const Author({required this.aid, required this.name, required this.alias, required this.zone, required this.cover, required this.url, required this.mangaCount, required this.newestMangaId, required this.newestMangaTitle, required this.newestMangaUrl, required this.newestDate, required this.highestMangaId, required this.highestMangaTitle, required this.highestMangaUrl, required this.highestScore, required this.averageScore, required this.popularity, required this.introduction, required this.relatedAuthors});
29 |
30 | factory Author.fromJson(Map json) => _$AuthorFromJson(json);
31 |
32 | Map toJson() => _$AuthorToJson(this);
33 |
34 | String get formattedNewestDate => newestDate.replaceAll('-', '/');
35 | }
36 |
37 | @JsonSerializable(fieldRename: FieldRename.snake)
38 | class SmallAuthor {
39 | final int aid;
40 | final String name;
41 | final String zone;
42 | final String cover;
43 | final String url;
44 | final int mangaCount;
45 | final String newestDate;
46 |
47 | const SmallAuthor({required this.aid, required this.name, required this.zone, required this.cover, required this.url, required this.mangaCount, required this.newestDate});
48 |
49 | factory SmallAuthor.fromJson(Map json) => _$SmallAuthorFromJson(json);
50 |
51 | Map toJson() => _$SmallAuthorToJson(this);
52 |
53 | String get formattedNewestDate => newestDate.replaceAll('-', '/');
54 |
55 | String get formattedNewestDateWithDuration {
56 | var result = parseDurationOrDateString(formattedNewestDate);
57 | if (result.duration == null) {
58 | return result.date;
59 | }
60 | return '${result.duration} (${result.date})';
61 | }
62 | }
63 |
64 | @JsonSerializable(fieldRename: FieldRename.snake)
65 | class TinyAuthor {
66 | final int aid;
67 | final String name;
68 | final String url;
69 |
70 | const TinyAuthor({required this.aid, required this.name, required this.url});
71 |
72 | factory TinyAuthor.fromJson(Map json) => _$TinyAuthorFromJson(json);
73 |
74 | Map toJson() => _$TinyAuthorToJson(this);
75 | }
76 |
77 | @JsonSerializable(fieldRename: FieldRename.snake)
78 | class TinyZonedAuthor {
79 | final int aid;
80 | final String name;
81 | final String url;
82 | final String zone;
83 |
84 | const TinyZonedAuthor({required this.aid, required this.name, required this.url, required this.zone});
85 |
86 | factory TinyZonedAuthor.fromJson(Map json) => _$TinyZonedAuthorFromJson(json);
87 |
88 | Map toJson() => _$TinyZonedAuthorToJson(this);
89 | }
90 |
--------------------------------------------------------------------------------
/process_deps.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | mkdir -p deps
4 |
5 | # Process sqflite dependency
6 | if [ -d deps/sqflite ]; then
7 | echo "Ignore existed \"deps/sqflite/\""
8 | else
9 | git clone --depth 1 --branch v2.2.5-0 https://github.com/tekartik/sqflite deps/sqflite
10 | sed -i "s/sdk: '>=2.18.0 <3.0.0'/sdk: '>=2.16.2 <3.0.0' # sdk: '>=2.18.0 <3.0.0'/" deps/sqflite/sqflite/pubspec.yaml
11 | sed -i 's/flutter: ">=3.3.0"/# flutter: ">=3.3.0"/' deps/sqflite/sqflite/pubspec.yaml
12 | sed -i "s/sqflite_common: '>=2.4.2+2 <4.0.0'/sqflite_common:\n path: ..\/sqflite_common/" deps/sqflite/sqflite/pubspec.yaml
13 | sed -i "s/sdk: '>=2.18.0 <3.0.0'/sdk: '>=2.16.2 <3.0.0' # sdk: '>=2.18.0 <3.0.0'/" deps/sqflite/sqflite/example/pubspec.yaml
14 | sed -i "s/sdk: '>=2.18.0 <3.0.0'/sdk: '>=2.16.2 <3.0.0' # sdk: '>=2.18.0 <3.0.0'/" deps/sqflite/sqflite_common/pubspec.yaml
15 | sed -i "s/SqfliteBatchOperation(super.type, this.method, super.sql, super.arguments);/SqfliteBatchOperation(dynamic type, this.method, dynamic sql, dynamic arguments) : super(type, sql, arguments);/" deps/sqflite/sqflite_common/lib/src/batch.dart
16 | fi
17 |
18 | # Process photo_view dependency
19 | if [ -d deps/photo_view ]; then
20 | echo "Ignore existed \"deps/photo_view/\""
21 | else
22 | git clone --depth 1 --branch 0.14.0 https://github.com/bluefireteam/photo_view deps/photo_view
23 | sed -i "0,/required this.enablePanAlways,/s//required this.enablePanAlways,\n this.customBuilder,/" deps/photo_view/lib/src/photo_view_wrappers.dart
24 | sed -i "0,/final bool? enablePanAlways;/s//final bool? enablePanAlways;\n final Widget Function(BuildContext, Widget)? customBuilder;/" deps/photo_view/lib/src/photo_view_wrappers.dart
25 | sed -i "s/return PhotoViewCore(/var view = PhotoViewCore(/" deps/photo_view/lib/src/photo_view_wrappers.dart
26 | sed -z -i "s/widget.enablePanAlways ?? false,\n );/widget.enablePanAlways ?? false,\n );\n return widget.customBuilder?.call(context, view) ?? view;/" deps/photo_view/lib/src/photo_view_wrappers.dart
27 | sed -i "0,/this.enablePanAlways,/s//this.enablePanAlways,\n this.customBuilder,/" deps/photo_view/lib/photo_view.dart
28 | sed -i "s/loadingBuilder = null,/loadingBuilder = null,\n customBuilder = null,/" deps/photo_view/lib/photo_view.dart
29 | sed -i "s/final bool? enablePanAlways;/final bool? enablePanAlways;\n\n final Widget Function(BuildContext, Widget)? customBuilder;/" deps/photo_view/lib/photo_view.dart
30 | sed -z -i "s/enablePanAlways: widget.enablePanAlways,\n );/enablePanAlways: widget.enablePanAlways,\n customBuilder: widget.customBuilder,\n );/" deps/photo_view/lib/photo_view.dart
31 | fi
32 |
33 | # Process flutter_ahlib reloadable_photo_view.dart
34 | if [ -f deps/photo_view/lib/reloadable_photo_view.dart ]; then
35 | echo "Ignore existed \"deps/photo_view/lib/reloadable_photo_view.dart\""
36 | else
37 | curl -o deps/photo_view/lib/reloadable_photo_view.dart https://raw.githubusercontent.com/Aoi-hosizora/flutter_ahlib/v1.3.0/lib/src/image/reloadable_photo_view.dart
38 | sed -i "0,/this.errorBuilder,/s//this.errorBuilder,\n this.customBuilder,/" deps/photo_view/lib/reloadable_photo_view.dart
39 | sed -i "0,/final ErrorPlaceholderBuilder? errorBuilder;/s//final ErrorPlaceholderBuilder? errorBuilder;\n\n final Widget Function(BuildContext, Widget)? customBuilder;/" deps/photo_view/lib/reloadable_photo_view.dart
40 | sed -i "s/errorBuilder: widget.errorBuilder,/errorBuilder: widget.errorBuilder,\n customBuilder: widget.customBuilder,/" deps/photo_view/lib/reloadable_photo_view.dart
41 | fi
42 |
--------------------------------------------------------------------------------
/lib/model/user.dart:
--------------------------------------------------------------------------------
1 | import 'package:intl/intl.dart';
2 | import 'package:json_annotation/json_annotation.dart';
3 | import 'package:manhuagui_flutter/model/common.dart';
4 |
5 | part 'user.g.dart';
6 |
7 | @JsonSerializable(fieldRename: FieldRename.snake)
8 | class Token {
9 | final String token;
10 |
11 | const Token({required this.token});
12 |
13 | factory Token.fromJson(Map json) => _$TokenFromJson(json);
14 |
15 | Map toJson() => _$TokenToJson(this);
16 | }
17 |
18 | @JsonSerializable(fieldRename: FieldRename.snake)
19 | class User {
20 | final String username;
21 | final String avatar;
22 | @JsonKey(name: 'class')
23 | final String className;
24 | final int score;
25 | final int accountPoint;
26 | final int unreadMessageCount;
27 | final String loginIp;
28 | final String lastLoginIp;
29 | final String registerTime;
30 | final String lastLoginTime;
31 | final int cumulativeDayCount;
32 | final int totalCommentCount;
33 |
34 | const User({required this.username, required this.avatar, required this.className, required this.score, required this.accountPoint, required this.unreadMessageCount, required this.loginIp, required this.lastLoginIp, required this.registerTime, required this.lastLoginTime, required this.cumulativeDayCount, required this.totalCommentCount});
35 |
36 | factory User.fromJson(Map json) => _$UserFromJson(json);
37 |
38 | Map toJson() => _$UserToJson(this);
39 |
40 | static DateTime? _parseDateTime(String s) {
41 | try {
42 | return DateFormat('yyyy/M/d HH:mm:ss').parse(s);
43 | } catch (_) {
44 | return null;
45 | }
46 | }
47 |
48 | static String _formatDateTime(DateTime dt) => formatDatetimeAndDuration(dt, FormatPattern.datetime);
49 |
50 | static String _formatDuration(DateTime dt) => formatDatetimeAndDuration(dt, FormatPattern.durationOnlyDate);
51 |
52 | String get formattedRegisterTime {
53 | var dt = _parseDateTime(registerTime);
54 | if (dt == null) {
55 | return registerTime;
56 | }
57 | return _formatDateTime(dt); // "2023/02/02 22:24:59"
58 | }
59 |
60 | String get formattedLastLoginTimeWithDuration {
61 | var dt = _parseDateTime(lastLoginTime);
62 | if (dt == null) {
63 | return lastLoginTime;
64 | }
65 | return '${_formatDateTime(dt)} (${_formatDuration(dt)})'; // "2023/02/02 22:24:59 (xxx天前)" or "2023/02/02 22:24:59 (今天)"
66 | }
67 |
68 | String formattedCurrLoginTimeWithDuration(DateTime? currLoginTime) {
69 | var dt = currLoginTime;
70 | if (dt == null) {
71 | return '未知时间';
72 | }
73 | return '${_formatDateTime(dt)} (${_formatDuration(dt)})'; // "2023/02/02 22:24:59 (xxx天前)" or "2023/02/02 22:24:59 (今天)"
74 | }
75 |
76 | bool isTodayLogined(DateTime? currLoginTime) {
77 | var last = _parseDateTime(lastLoginTime);
78 | var curr = currLoginTime;
79 | var now = DateTime.now();
80 | var logined = false;
81 | logined = logined || (last != null && last.year == now.year && last.month == now.month && last.day == now.day);
82 | logined = logined || (curr != null && curr.year == now.year && curr.month == now.month && curr.day == now.day);
83 | return logined;
84 | }
85 | }
86 |
87 | @JsonSerializable(fieldRename: FieldRename.snake)
88 | class LoginCheckResult {
89 | final String username;
90 |
91 | const LoginCheckResult({required this.username});
92 |
93 | factory LoginCheckResult.fromJson(Map json) => _$LoginCheckResultFromJson(json);
94 |
95 | Map toJson() => _$LoginCheckResultToJson(this);
96 | }
97 |
--------------------------------------------------------------------------------
/lib/model/message.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'message.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | Message _$MessageFromJson(Map json) => Message(
10 | mid: json['mid'] as int,
11 | title: json['title'] as String,
12 | notification: json['notification'] == null
13 | ? null
14 | : NotificationContent.fromJson(
15 | json['notification'] as Map),
16 | newVersion: json['new_version'] == null
17 | ? null
18 | : NewVersionContent.fromJson(
19 | json['new_version'] as Map),
20 | createdAt: DateTime.parse(json['created_at'] as String),
21 | updatedAt: DateTime.parse(json['updated_at'] as String),
22 | );
23 |
24 | Map _$MessageToJson(Message instance) => {
25 | 'mid': instance.mid,
26 | 'title': instance.title,
27 | 'notification': instance.notification,
28 | 'new_version': instance.newVersion,
29 | 'created_at': instance.createdAt.toIso8601String(),
30 | 'updated_at': instance.updatedAt.toIso8601String(),
31 | };
32 |
33 | NotificationContent _$NotificationContentFromJson(Map json) =>
34 | NotificationContent(
35 | content: json['content'] as String,
36 | dismissible: json['dismissible'] as bool,
37 | link: json['link'] as String,
38 | );
39 |
40 | Map _$NotificationContentToJson(
41 | NotificationContent instance) =>
42 | {
43 | 'content': instance.content,
44 | 'dismissible': instance.dismissible,
45 | 'link': instance.link,
46 | };
47 |
48 | NewVersionContent _$NewVersionContentFromJson(Map json) =>
49 | NewVersionContent(
50 | version: json['version'] as String,
51 | mustUpgrade: json['must_upgrade'] as bool,
52 | changeLogs: json['change_logs'] as String,
53 | releasePage: json['release_page'] as String,
54 | );
55 |
56 | Map _$NewVersionContentToJson(NewVersionContent instance) =>
57 | {
58 | 'version': instance.version,
59 | 'must_upgrade': instance.mustUpgrade,
60 | 'change_logs': instance.changeLogs,
61 | 'release_page': instance.releasePage,
62 | };
63 |
64 | LatestMessage _$LatestMessageFromJson(Map json) =>
65 | LatestMessage(
66 | notification: json['notification'] == null
67 | ? null
68 | : Message.fromJson(json['notification'] as Map),
69 | newVersion: json['new_version'] == null
70 | ? null
71 | : Message.fromJson(json['new_version'] as Map),
72 | notDismissibleNotification: json['not_dismissible_notification'] == null
73 | ? null
74 | : Message.fromJson(
75 | json['not_dismissible_notification'] as Map),
76 | mustUpgradeNewVersion: json['must_upgrade_new_version'] == null
77 | ? null
78 | : Message.fromJson(
79 | json['must_upgrade_new_version'] as Map),
80 | );
81 |
82 | Map _$LatestMessageToJson(LatestMessage instance) =>
83 | {
84 | 'notification': instance.notification,
85 | 'new_version': instance.newVersion,
86 | 'not_dismissible_notification': instance.notDismissibleNotification,
87 | 'must_upgrade_new_version': instance.mustUpgradeNewVersion,
88 | };
89 |
--------------------------------------------------------------------------------
/lib/page/view/favorite_reorder_line.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:manhuagui_flutter/model/entity.dart';
3 | import 'package:manhuagui_flutter/page/view/network_image.dart';
4 |
5 | /// 用于排序的收藏漫画和收藏分组行,在 [FavoriteReorderPage] / [FavoriteGroupPage] 使用
6 |
7 | class FavoriteMangaReorderLineView extends StatelessWidget {
8 | const FavoriteMangaReorderLineView({
9 | Key? key,
10 | required this.favorite,
11 | required this.originIndex,
12 | required this.dragger,
13 | required this.onLinePressed,
14 | }) : super(key: key);
15 |
16 | final FavoriteManga favorite;
17 | final int originIndex;
18 | final Widget? dragger;
19 | final VoidCallback onLinePressed;
20 |
21 | @override
22 | Widget build(BuildContext context) {
23 | return ListTile(
24 | title: Text(favorite.mangaTitle, maxLines: 1, overflow: TextOverflow.ellipsis),
25 | subtitle: Text(
26 | '${favorite.checkedGroupName}・备注 ${favorite.remark.trim().isEmpty ? '暂无' : favorite.remark.trim()}\n'
27 | '#${originIndex + 1}・收藏于 ${favorite.formattedCreatedAt}',
28 | maxLines: 2,
29 | overflow: TextOverflow.ellipsis,
30 | ),
31 | isThreeLine: true,
32 | leading: Column(
33 | mainAxisAlignment: MainAxisAlignment.center,
34 | children: [
35 | NetworkImageView(
36 | url: favorite.mangaCover,
37 | height: 48,
38 | width: 48,
39 | radius: BorderRadius.circular(12),
40 | ),
41 | ],
42 | ),
43 | trailing: dragger == null
44 | ? null
45 | : Column(
46 | mainAxisAlignment: MainAxisAlignment.center,
47 | children: [dragger!],
48 | ),
49 | onTap: onLinePressed,
50 | );
51 | }
52 | }
53 |
54 | class FavoriteGroupReorderLineView extends StatelessWidget {
55 | const FavoriteGroupReorderLineView({
56 | Key? key,
57 | required this.group,
58 | required this.originGroup,
59 | required this.dragger,
60 | this.canDelete = true,
61 | required this.onDeletePressed,
62 | required this.onLinePressed,
63 | }) : super(key: key);
64 |
65 | final FavoriteGroup group;
66 | final FavoriteGroup? originGroup;
67 | final Widget? dragger;
68 | final bool canDelete;
69 | final VoidCallback? onDeletePressed;
70 | final VoidCallback onLinePressed;
71 |
72 | @override
73 | Widget build(BuildContext context) {
74 | return ListTile(
75 | title: Text(group.checkedGroupName, maxLines: 1, overflow: TextOverflow.ellipsis),
76 | subtitle: Text(
77 | (group.groupName == ''
78 | ? '不可修改'
79 | : originGroup == null
80 | ? '新增的分组'
81 | : (originGroup!.groupName == group.groupName ? '未变更' : '原为 "${originGroup!.checkedGroupName}"')) + //
82 | '・创建于 ${group.formattedCreatedAt}',
83 | maxLines: 1,
84 | overflow: TextOverflow.ellipsis,
85 | ),
86 | leading: Column(
87 | mainAxisAlignment: MainAxisAlignment.center,
88 | children: [
89 | InkResponse(
90 | child: Icon(!canDelete ? Icons.disabled_by_default_rounded : Icons.delete),
91 | onTap: !canDelete ? null : onDeletePressed,
92 | radius: 22,
93 | ),
94 | ],
95 | ),
96 | trailing: dragger == null
97 | ? null
98 | : Column(
99 | mainAxisAlignment: MainAxisAlignment.center,
100 | children: [dragger!],
101 | ),
102 | onTap: onLinePressed,
103 | );
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/lib/service/evb/auth_manager.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter_ahlib/flutter_ahlib.dart';
4 | import 'package:manhuagui_flutter/service/dio/dio_manager.dart';
5 | import 'package:manhuagui_flutter/service/dio/retrofit.dart';
6 | import 'package:manhuagui_flutter/service/dio/wrap_error.dart';
7 | import 'package:manhuagui_flutter/service/evb/evb_manager.dart';
8 | import 'package:manhuagui_flutter/service/prefs/auth.dart';
9 | import 'package:synchronized/synchronized.dart';
10 |
11 | class AuthData {
12 | const AuthData({
13 | required this.username,
14 | required this.token,
15 | });
16 |
17 | final String username;
18 | final String token;
19 |
20 | bool get logined => token.isNotEmpty;
21 |
22 | bool equals(AuthData? o) {
23 | return o != null && username == o.username && token == o.token;
24 | }
25 | }
26 |
27 | class AuthManager {
28 | AuthManager._();
29 |
30 | static AuthManager? _instance;
31 |
32 | static AuthManager get instance {
33 | _instance ??= AuthManager._();
34 | return _instance!;
35 | }
36 |
37 | var _data = AuthData(username: '', token: ''); // global auth data
38 | var _loading = false; // global loading flag
39 |
40 | AuthData get authData => _data;
41 |
42 | String get username => _data.username;
43 |
44 | String get token => _data.token;
45 |
46 | bool get logined => _data.logined;
47 |
48 | bool get loading => _loading;
49 |
50 | void record({required String username, required String token}) {
51 | _data = AuthData(username: username, token: token);
52 | }
53 |
54 | void Function() listen(void Function(AuthChangedEvent) onData) {
55 | return EventBusManager.instance.listen(onData);
56 | }
57 |
58 | void Function() listenOnlyWhen(Tuple1 authData /* mutable */, void Function(AuthChangedEvent) onData) {
59 | return EventBusManager.instance.listen((ev) {
60 | if (!AuthManager.instance.authData.equals(authData.item)) {
61 | authData.item = AuthManager.instance.authData;
62 | onData.call(ev);
63 | }
64 | });
65 | }
66 |
67 | AuthChangedEvent notify({required bool logined, ErrorMessage? error}) {
68 | final ev = AuthChangedEvent(logined: logined, error: error);
69 | EventBusManager.instance.fire(ev);
70 | return ev;
71 | }
72 |
73 | final _lock = Lock();
74 |
75 | Future check() async {
76 | return _lock.synchronized(() async {
77 | // check if currently logined
78 | if (AuthManager.instance.logined) {
79 | return AuthManager.instance.notify(logined: true);
80 | }
81 |
82 | // no token stored in prefs
83 | var token = await AuthPrefs.getToken();
84 | if (token.isEmpty) {
85 | return AuthManager.instance.notify(logined: false);
86 | }
87 |
88 | // check stored token
89 | _loading = true;
90 | final client = RestClient(DioManager.instance.dio);
91 | try {
92 | var r = await client.checkUserLogin(token: token);
93 | AuthManager.instance.record(username: r.data.username, token: token);
94 | return AuthManager.instance.notify(logined: true);
95 | } catch (e, s) {
96 | var we = wrapError(e, s);
97 | if (we.type == ErrorType.resultError && we.response!.statusCode == 401) {
98 | await AuthPrefs.setToken('');
99 | }
100 | return AuthManager.instance.notify(logined: false, error: we);
101 | } finally {
102 | _loading = false;
103 | }
104 | });
105 | }
106 | }
107 |
108 | class AuthChangedEvent {
109 | const AuthChangedEvent({required this.logined, this.error});
110 |
111 | final bool logined;
112 | final ErrorMessage? error;
113 | }
114 |
--------------------------------------------------------------------------------
/lib/service/evb/events.dart:
--------------------------------------------------------------------------------
1 | // ======================
2 | // To XXX Requested Event
3 | // ======================
4 |
5 | class ToRecommendRequestedEvent {
6 | const ToRecommendRequestedEvent();
7 | }
8 |
9 | class ToShelfRequestedEvent {
10 | const ToShelfRequestedEvent();
11 | }
12 |
13 | class ToFavoriteRequestedEvent {
14 | const ToFavoriteRequestedEvent();
15 | }
16 |
17 | class ToHistoryRequestedEvent {
18 | const ToHistoryRequestedEvent();
19 | }
20 |
21 | class ToRecentRequestedEvent {
22 | const ToRecentRequestedEvent();
23 | }
24 |
25 | class ToRankingRequestedEvent {
26 | const ToRankingRequestedEvent();
27 | }
28 |
29 | // =================
30 | // XXX Updated Event
31 | // =================
32 |
33 | enum UpdateReason { added, updated, deleted }
34 |
35 | class HistoryUpdatedEvent {
36 | const HistoryUpdatedEvent({required this.mangaId, required this.reason, this.fromHistoryPage = false, this.fromSepHistoryPage = false, this.fromMangaPage = false});
37 |
38 | final int mangaId;
39 | final UpdateReason reason;
40 | final bool fromHistoryPage;
41 | final bool fromSepHistoryPage;
42 | final bool fromMangaPage;
43 | }
44 |
45 | class ShelfUpdatedEvent {
46 | const ShelfUpdatedEvent({required this.mangaId, required this.added, this.fromShelfPage = false, this.fromSepShelfPage = false, this.fromMangaPage = false});
47 |
48 | final int mangaId;
49 | final bool added;
50 | final bool fromShelfPage;
51 | final bool fromSepShelfPage;
52 | final bool fromMangaPage;
53 | }
54 |
55 | class FavoriteUpdatedEvent {
56 | const FavoriteUpdatedEvent({required this.mangaId, required this.group, this.oldGroup, required this.reason, this.fromFavoritePage = false, this.fromSepFavoritePage = false, this.fromMangaPage = false});
57 |
58 | final int mangaId;
59 | final String group;
60 | final String? oldGroup; // means move to group
61 | final UpdateReason reason;
62 | final bool fromFavoritePage;
63 | final bool fromSepFavoritePage;
64 | final bool fromMangaPage;
65 | }
66 |
67 | class DownloadUpdatedEvent {
68 | const DownloadUpdatedEvent({required this.mangaId, this.fromDownloadPage = false, this.fromMangaPage = false, this.fromMangaViewerPage = false, this.fromDownloadMangaPage = false});
69 |
70 | final int mangaId;
71 | final bool fromDownloadPage;
72 | final bool fromMangaPage;
73 | final bool fromMangaViewerPage;
74 | final bool fromDownloadMangaPage;
75 | }
76 |
77 | class ShelfCacheUpdatedEvent {
78 | const ShelfCacheUpdatedEvent({required this.mangaId, required this.added, this.fromShelfCachePage = false});
79 |
80 | final int mangaId;
81 | final bool added;
82 | final bool fromShelfCachePage;
83 | }
84 |
85 | class FavoriteOrderUpdatedEvent {
86 | const FavoriteOrderUpdatedEvent({required this.groupName});
87 |
88 | final String groupName;
89 | }
90 |
91 | class FavoriteGroupUpdatedEvent {
92 | const FavoriteGroupUpdatedEvent({required this.changedGroups, required this.newGroups});
93 |
94 | final Map changedGroups;
95 | final List newGroups;
96 | }
97 |
98 | class FavoriteAuthorUpdatedEvent {
99 | const FavoriteAuthorUpdatedEvent({required this.authorId, required this.reason, this.fromFavoritePage = false, this.fromAuthorPage = false});
100 |
101 | final int authorId;
102 | final UpdateReason reason;
103 | final bool fromFavoritePage;
104 | final bool fromAuthorPage;
105 | }
106 |
107 | // ============
108 | // Other Events
109 | // ============
110 |
111 | class DownloadProgressChangedEvent {
112 | const DownloadProgressChangedEvent({required this.mangaId, required this.finished});
113 |
114 | final int mangaId;
115 | final bool finished;
116 | }
117 |
118 | class AppSettingChangedEvent {
119 | const AppSettingChangedEvent();
120 | }
121 |
--------------------------------------------------------------------------------
/lib/page/chapter_detail.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/chapter.dart';
4 | import 'package:manhuagui_flutter/page/view/detail_table.dart';
5 |
6 | /// 漫画章节详情页,展示所给 [TinyMangaChapter] 信息
7 | class ChapterDetailPage extends StatefulWidget {
8 | const ChapterDetailPage({
9 | Key? key,
10 | required this.data,
11 | required this.chapterCover,
12 | required this.groupLength,
13 | required this.mangaTitle,
14 | required this.mangaCover,
15 | required this.mangaUrl,
16 | required this.isTocLoaded,
17 | }) : super(key: key);
18 |
19 | final TinyMangaChapter data;
20 | final String? chapterCover;
21 | final int? groupLength;
22 | final String mangaTitle;
23 | final String mangaCover;
24 | final String mangaUrl;
25 | final bool isTocLoaded;
26 |
27 | @override
28 | _ChapterDetailPageState createState() => _ChapterDetailPageState();
29 | }
30 |
31 | class _ChapterDetailPageState extends State {
32 | final _controller = ScrollController();
33 |
34 | @override
35 | void dispose() {
36 | _controller.dispose();
37 | super.dispose();
38 | }
39 |
40 | @override
41 | Widget build(BuildContext context) {
42 | return Scaffold(
43 | appBar: AppBar(
44 | title: Text('漫画章节详情'),
45 | leading: AppBarActionButton.leading(context: context),
46 | ),
47 | body: ExtendedScrollbar(
48 | controller: _controller,
49 | interactive: true,
50 | mainAxisMargin: 2,
51 | crossAxisMargin: 2,
52 | child: ListView(
53 | controller: _controller,
54 | padding: EdgeInsets.symmetric(horizontal: 20, vertical: 15),
55 | physics: AlwaysScrollableScrollPhysics(),
56 | children: [
57 | DetailTableView(
58 | rows: [
59 | DetailRow('cid', widget.data.cid.toString()),
60 | DetailRow('章节标题', '《${widget.data.title}》', textForCopy: widget.data.title),
61 | DetailRow('章节封面', widget.chapterCover == null ? '未知' : widget.chapterCover!, canCopy: widget.chapterCover != null),
62 | DetailRow('章节网页链接', widget.data.url),
63 | DetailRow('章节页数', widget.data.pageCount.toString()),
64 | DetailRow('(mid)', widget.data.mid.toString()),
65 | DetailRow('(漫画标题)', '《${widget.mangaTitle}》', textForCopy: widget.mangaTitle),
66 | DetailRow('(漫画封面)', widget.mangaCover),
67 | DetailRow('(漫画网页链接)', widget.mangaUrl),
68 | if (widget.isTocLoaded) ...[
69 | DetailRow('最近上传', widget.data.isNew ? '是' : '否', canCopy: false),
70 | DetailRow('章节所属分组', widget.data.group.isEmpty ? '未知' : widget.data.group, canCopy: widget.data.group.isNotEmpty),
71 | DetailRow('分组内顺序', '正序 ${widget.data.number <= 0 ? '未知' : widget.data.number} (总数 ${widget.groupLength ?? '未知'})'),
72 | ],
73 | if (!widget.isTocLoaded) ...[
74 | DetailRow('最近上传', '未知', canCopy: false),
75 | DetailRow('章节所属分组', '未知', canCopy: false),
76 | DetailRow('分组内顺序', '未知', canCopy: false),
77 | ],
78 | ],
79 | tableWidth: MediaQuery.of(context).size.width - MediaQuery.of(context).padding.horizontal - 40,
80 | ),
81 | ],
82 | ),
83 | ),
84 | floatingActionButton: ScrollAnimatedFab(
85 | scrollController: _controller,
86 | condition: ScrollAnimatedCondition.direction,
87 | fab: FloatingActionButton(
88 | child: Icon(Icons.vertical_align_top),
89 | heroTag: null,
90 | onPressed: () => _controller.scrollToTop(),
91 | ),
92 | ),
93 | );
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/lib/page/view/category_grid.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:manhuagui_flutter/model/category.dart';
3 | import 'package:manhuagui_flutter/page/view/full_ripple.dart';
4 | import 'package:manhuagui_flutter/page/view/network_image.dart';
5 |
6 | enum CategoryGridViewStyle {
7 | threeColumns,
8 | fourColumns,
9 | }
10 |
11 | /// 漫画类别方格,在 [GenreSubPage] 使用
12 | class CategoryGridView extends StatelessWidget {
13 | const CategoryGridView({
14 | Key? key,
15 | required this.categories,
16 | required this.onSelected,
17 | this.style = CategoryGridViewStyle.threeColumns,
18 | }) : super(key: key);
19 |
20 | final List categories;
21 | final void Function(Category category) onSelected;
22 | final CategoryGridViewStyle style;
23 |
24 | Widget _buildItem({required BuildContext context, required Category category, required double width, required double imgHeight}) {
25 | return Card(
26 | margin: EdgeInsets.zero,
27 | elevation: 1.5,
28 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6.0)),
29 | child: FullRippleWidget(
30 | highlightColor: null,
31 | splashColor: null,
32 | radius: BorderRadius.circular(6.0),
33 | child: SizedBox(
34 | width: width,
35 | child: Column(
36 | children: [
37 | if (category.cover.isNotEmpty)
38 | NetworkImageView(
39 | url: category.cover,
40 | width: width,
41 | height: imgHeight,
42 | quality: FilterQuality.high,
43 | radius: BorderRadius.only(topLeft: Radius.circular(6.0), topRight: Radius.circular(6.0)),
44 | ),
45 | if (category.cover.isEmpty)
46 | Container(
47 | width: width,
48 | height: imgHeight,
49 | decoration: BoxDecoration(
50 | gradient: LinearGradient(
51 | begin: Alignment.topLeft,
52 | end: Alignment.bottomRight,
53 | stops: const [0, 0.5, 1],
54 | colors: [Colors.blue[100]!, Colors.orange[100]!, Colors.purple[100]!],
55 | ),
56 | borderRadius: BorderRadius.only(topLeft: Radius.circular(6.0), topRight: Radius.circular(6.0)),
57 | ),
58 | ),
59 | Padding(
60 | padding: EdgeInsets.only(top: 3, bottom: 4),
61 | child: Text(
62 | category.title == '全部' ? '全部漫画' : category.title,
63 | style: Theme.of(context).textTheme.bodyText1,
64 | maxLines: 1,
65 | overflow: TextOverflow.ellipsis,
66 | ),
67 | ),
68 | ],
69 | ),
70 | ),
71 | onTap: () => onSelected.call(category),
72 | ),
73 | );
74 | }
75 |
76 | @override
77 | Widget build(BuildContext context) {
78 | const hSpace = 15.0;
79 | const vSpace = 15.0;
80 | var width = style == CategoryGridViewStyle.threeColumns
81 | ? (MediaQuery.of(context).size.width - hSpace * 4) / 3 // | ▢ ▢ ▢ |
82 | : (MediaQuery.of(context).size.width - hSpace * 5) / 4; // | ▢ ▢ ▢ ▢ |
83 |
84 | return Padding(
85 | padding: EdgeInsets.symmetric(horizontal: hSpace),
86 | child: Wrap(
87 | spacing: hSpace,
88 | runSpacing: vSpace,
89 | children: [
90 | for (var category in categories)
91 | _buildItem(
92 | context: context,
93 | category: category,
94 | width: width,
95 | imgHeight: width,
96 | ),
97 | ],
98 | ),
99 | );
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/lib/service/dio/dio_manager.dart:
--------------------------------------------------------------------------------
1 | import 'package:dio/dio.dart';
2 | import 'package:manhuagui_flutter/app_setting.dart';
3 | import 'package:manhuagui_flutter/config.dart';
4 |
5 | class DioManager {
6 | DioManager._();
7 |
8 | static DioManager? _instance;
9 |
10 | static DioManager get instance {
11 | _instance ??= DioManager._();
12 | return _instance!;
13 | }
14 |
15 | // global Dio instances
16 | Dio? _dio;
17 | Dio? _longDio;
18 | Dio? _longLongDio;
19 | Dio? _noTimeoutDio;
20 |
21 | Dio get dio {
22 | if (_dio == null) {
23 | _dio = Dio();
24 | _dio!.options.connectTimeout = CONNECT_TIMEOUT;
25 | _dio!.options.sendTimeout = SEND_TIMEOUT;
26 | _dio!.options.receiveTimeout = RECEIVE_TIMEOUT;
27 | _dio!.interceptors.add(LogInterceptor());
28 | }
29 | if (_longDio == null) {
30 | _longDio = Dio();
31 | _longDio!.options.connectTimeout = CONNECT_LTIMEOUT;
32 | _longDio!.options.sendTimeout = SEND_LTIMEOUT;
33 | _longDio!.options.receiveTimeout = RECEIVE_LTIMEOUT;
34 | _longDio!.interceptors.add(LogInterceptor());
35 | }
36 | if (_longLongDio == null) {
37 | _longLongDio = Dio();
38 | _longLongDio!.options.connectTimeout = CONNECT_LLTIMEOUT;
39 | _longLongDio!.options.sendTimeout = SEND_LLTIMEOUT;
40 | _longLongDio!.options.receiveTimeout = RECEIVE_LLTIMEOUT;
41 | _longLongDio!.interceptors.add(LogInterceptor());
42 | }
43 | if (_noTimeoutDio == null) {
44 | _noTimeoutDio = Dio();
45 | _noTimeoutDio!.interceptors.add(LogInterceptor());
46 | }
47 | return AppSetting.instance.other.timeoutBehavior.determineValue(
48 | normal: _dio!,
49 | long: _longDio!,
50 | longLong: _longLongDio!,
51 | disable: _noTimeoutDio!,
52 | )!;
53 | }
54 | }
55 |
56 | class LogInterceptor extends Interceptor {
57 | @override
58 | Future onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
59 | print('┌─────────────────── Request ─────────────────────┐');
60 | print('date: ${DateTime.now().toIso8601String()}');
61 | print('uri: ${options.uri}');
62 | print('method: ${options.method}');
63 | if (options.extra.isNotEmpty) {
64 | print('extra: ${options.extra}');
65 | }
66 | print('headers:');
67 | options.headers.forEach((key, v) => print(' $key: $v'));
68 | print('└─────────────────── Request ─────────────────────┘');
69 | return super.onRequest(options, handler);
70 | }
71 |
72 | @override
73 | Future onError(DioError err, ErrorInterceptorHandler handler) async {
74 | print('┌─────────────────── DioError ────────────────────┐');
75 | print('date: ${DateTime.now().toIso8601String()}');
76 | print('uri: ${err.requestOptions.uri}');
77 | print('method: ${err.requestOptions.method}');
78 | print('error: $err');
79 | if (err.response != null) {
80 | _printResponse(err.response!);
81 | }
82 | print('└─────────────────── DioError ────────────────────┘');
83 | return super.onError(err, handler);
84 | }
85 |
86 | @override
87 | Future onResponse(Response response, ResponseInterceptorHandler handler) async {
88 | print('┌─────────────────── Response ────────────────────┐');
89 | print('date: ${DateTime.now().toIso8601String()}');
90 | _printResponse(response);
91 | print('└─────────────────── Response ────────────────────┘');
92 | return super.onResponse(response, handler);
93 | }
94 |
95 | void _printResponse(Response response) {
96 | print('uri: ${response.requestOptions.uri}');
97 | print('method: ${response.requestOptions.method}');
98 | print('statusCode: ${response.statusCode}');
99 | if (!response.headers.isEmpty) {
100 | print('headers:');
101 | response.headers.forEach((key, v) => print(' $key: ${v.join(',')}'));
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/lib/page/view/manga_ranking_line.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/manga.dart';
4 | import 'package:manhuagui_flutter/page/dlg/manga_dialog.dart';
5 | import 'package:manhuagui_flutter/page/manga.dart';
6 | import 'package:manhuagui_flutter/page/view/corner_icons.dart';
7 | import 'package:manhuagui_flutter/page/view/general_line.dart';
8 |
9 | /// 漫画排名行,在 [RankingSubPage] 使用
10 | class MangaRankingLineView extends StatelessWidget {
11 | const MangaRankingLineView({
12 | Key? key,
13 | required this.manga,
14 | this.flags,
15 | this.twoColumns = false,
16 | }) : super(key: key);
17 |
18 | final MangaRanking manga;
19 | final MangaCornerFlags? flags;
20 | final bool twoColumns;
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | return GeneralLineView(
25 | imageUrl: manga.cover,
26 | title: manga.title,
27 | icon1: Icons.person,
28 | text1: manga.authors.map((a) => a.name).join('/'),
29 | icon2: Icons.notes,
30 | text2: '最新章节 ${manga.newestChapter}',
31 | icon3: Icons.update,
32 | text3: '${manga.finished ? '已完结' : '连载中'}・${manga.formattedNewestDateWithDuration}',
33 | cornerIcons: flags?.buildIcons(),
34 | twoColumns: twoColumns,
35 | extraRightPaddingForTitle: 28 - 14 + 5 /* badge width - line horizontal padding + extra space */,
36 | extrasInStack: [
37 | Positioned(
38 | top: 0,
39 | bottom: 0,
40 | right: 25,
41 | child: Column(
42 | mainAxisAlignment: MainAxisAlignment.center,
43 | children: [
44 | manga.trend == 1
45 | ? Icon(Icons.arrow_drop_up, size: 26, color: Colors.red) // up
46 | : manga.trend == 2
47 | ? Icon(Icons.arrow_drop_down, size: 26, color: Colors.blue[400]) // down
48 | : Transform.scale(scaleX: 0.6, child: Icon(Icons.remove, size: 26, color: Colors.grey[600])) /* no change */,
49 | Text(
50 | manga.score.toString(),
51 | style: Theme.of(context).textTheme.bodyText1?.copyWith(
52 | color: manga.trend == 1 ? Colors.red : (manga.trend == 2 ? Colors.blue[400] : Colors.grey[600]),
53 | ),
54 | maxLines: 1,
55 | overflow: TextOverflow.ellipsis,
56 | ),
57 | ],
58 | ),
59 | ),
60 | Positioned(
61 | top: 0,
62 | right: 0,
63 | child: Container(
64 | width: 28,
65 | height: 28,
66 | decoration: BoxDecoration(
67 | color: manga.order == 1 ? Colors.red : (manga.order == 2 ? Colors.orange : (manga.order == 3 ? Colors.yellow[600] : Colors.grey[400])),
68 | borderRadius: BorderRadius.only(bottomLeft: Radius.circular(28)),
69 | ),
70 | alignment: Alignment.topRight,
71 | child: SizedBox(
72 | width: 28 * 0.8,
73 | height: 28 * 0.85,
74 | child: Center(
75 | child: Text(
76 | manga.order.toString(),
77 | style: Theme.of(context).textTheme.bodyText2?.copyWith(color: Colors.white),
78 | ),
79 | ),
80 | ),
81 | ),
82 | ),
83 | ],
84 | onPressed: () => Navigator.of(context).push(
85 | CustomPageRoute(
86 | context: context,
87 | builder: (c) => MangaPage(
88 | id: manga.mid,
89 | title: manga.title,
90 | url: manga.url,
91 | ),
92 | ),
93 | ),
94 | onLongPressed: () => showPopupMenuForMangaList(
95 | context: context,
96 | mangaId: manga.mid,
97 | mangaTitle: manga.title,
98 | mangaCover: manga.cover,
99 | mangaUrl: manga.url,
100 | ),
101 | );
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/lib/model/category.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 |
3 | import 'package:json_annotation/json_annotation.dart';
4 |
5 | part 'category.g.dart';
6 |
7 | @JsonSerializable(fieldRename: FieldRename.snake)
8 | class Category {
9 | final String name;
10 | final String title;
11 | final String url;
12 | final String cover;
13 |
14 | const Category({required this.name, required this.title, required this.url, required this.cover});
15 |
16 | factory Category.fromJson(Map json) => _$CategoryFromJson(json);
17 |
18 | Map toJson() => _$CategoryToJson(this);
19 |
20 | TinyCategory toTiny() {
21 | return TinyCategory(name: name, title: title);
22 | }
23 | }
24 |
25 | @JsonSerializable(fieldRename: FieldRename.snake)
26 | class CategoryList {
27 | final List genres;
28 | final List ages;
29 | final List zones;
30 |
31 | const CategoryList({required this.genres, required this.ages, required this.zones});
32 |
33 | factory CategoryList.fromJson(Map json) => _$CategoryListFromJson(json);
34 |
35 | Map toJson() => _$CategoryListToJson(this);
36 | }
37 |
38 | class TinyCategory {
39 | final String name;
40 | final String title;
41 |
42 | const TinyCategory({required this.name, required this.title});
43 |
44 | @override
45 | bool operator ==(Object other) {
46 | return other is TinyCategory && other.name == name;
47 | }
48 |
49 | @override
50 | int get hashCode => hashValues(name, title);
51 |
52 | Category toCategory({String? cover}) {
53 | return Category(
54 | name: name,
55 | title: title,
56 | url: 'https://www.manhuagui.com/list/$name/',
57 | cover: cover ?? '',
58 | );
59 | }
60 |
61 | bool isAll() {
62 | return name == 'all';
63 | }
64 | }
65 |
66 | /*
67 | genre: (all|...)
68 | age: (all|shaonv|shaonian|qingnian|ertong|tongyong)
69 | zone: (all|japan|hongkong|other|europe|china|korea)
70 | status: (all|lianzai|wanjie)
71 |
72 | ranking_durations: (day|week|month|total)
73 | ranking_type: (all|...zones|...ages|...genres)
74 | */
75 |
76 | // 全局的漫画类别,不包括 all
77 | CategoryList? globalCategoryList; // genres, ages, zones
78 |
79 | // 按剧情
80 | final allGenres = [
81 | const TinyCategory(title: '全部', name: 'all'),
82 | // ...
83 | ];
84 |
85 | // 按受众 (顺序已调整)
86 | final allAges = [
87 | const TinyCategory(title: '全部', name: 'all'),
88 | const TinyCategory(title: '少年', name: 'shaonian'),
89 | const TinyCategory(title: '少女', name: 'shaonv'),
90 | const TinyCategory(title: '青年', name: 'qingnian'),
91 | const TinyCategory(title: '儿童', name: 'ertong'),
92 | const TinyCategory(title: '通用', name: 'tongyong'),
93 | ];
94 |
95 | // 按地区 (顺序已调整)
96 | final allZones = [
97 | const TinyCategory(title: '全部', name: 'all'),
98 | const TinyCategory(title: '日本', name: 'japan'),
99 | const TinyCategory(title: '内地', name: 'china'),
100 | const TinyCategory(title: '港台', name: 'hongkong'),
101 | const TinyCategory(title: '欧美', name: 'europe'),
102 | const TinyCategory(title: '韩国', name: 'korea'),
103 | const TinyCategory(title: '其它', name: 'other'),
104 | ];
105 |
106 | // 按进度
107 | final allStatuses = [
108 | const TinyCategory(title: '全部', name: 'all'),
109 | const TinyCategory(title: '连载', name: 'lianzai'),
110 | const TinyCategory(title: '完结', name: 'wanjie'),
111 | ];
112 |
113 | // 排行榜周期
114 | final allRankingDurations = [
115 | const TinyCategory(title: '日排行', name: 'day'),
116 | const TinyCategory(title: '周排行', name: 'week'),
117 | const TinyCategory(title: '月排行', name: 'month'),
118 | const TinyCategory(title: '总排行', name: 'total'),
119 | ];
120 |
121 | // 排行榜类型
122 | final allRankingTypes = [
123 | const TinyCategory(title: '全部', name: 'all'),
124 | // 按地区
125 | for (var i = 1; i < allZones.length; i++) allZones[i],
126 | // 按受众
127 | for (var i = 1; i < allAges.length; i++) allAges[i],
128 | // 按剧情
129 | // ...
130 | ];
131 |
--------------------------------------------------------------------------------
/lib/page/view/manga_simple_toc.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:manhuagui_flutter/model/chapter.dart';
3 | import 'package:manhuagui_flutter/page/view/chapter_grid.dart';
4 | import 'package:manhuagui_flutter/page/view/manga_toc.dart';
5 |
6 | /// 漫画章节列表(给定章节分组列表,不包括正逆序等按钮),在 [DlFinishedSubPage] 使用
7 | class MangaSimpleTocView extends StatelessWidget {
8 | const MangaSimpleTocView({
9 | Key? key,
10 | required this.groups,
11 | this.columns = 4,
12 | this.gridPadding,
13 | this.invertOrder = true,
14 | this.highlightColor,
15 | this.highlightedChapters = const [],
16 | this.showNewBadge = true,
17 | this.customBadgeBuilder,
18 | this.itemBuilder,
19 | required this.onChapterPressed,
20 | this.onChapterLongPressed,
21 | }) : super(key: key);
22 |
23 | final List groups;
24 | final int columns;
25 | final EdgeInsets? gridPadding;
26 | final bool invertOrder;
27 | final Color? highlightColor;
28 | final List highlightedChapters;
29 | final bool showNewBadge;
30 | final Widget? Function(int cid)? customBadgeBuilder;
31 | final Widget Function(BuildContext context, int? cid, Widget itemWidget)? itemBuilder;
32 | final void Function(int cid) onChapterPressed;
33 | final void Function(int cid)? onChapterLongPressed;
34 |
35 | Widget _buildGrid({required int idx, required List chapters}) {
36 | return ChapterGridView(
37 | chapters: chapters,
38 | padding: gridPadding ?? EdgeInsets.symmetric(horizontal: 12),
39 | showPageCount: true,
40 | invertOrder: invertOrder /* true means desc */,
41 | maxLines: -1 /* show all chapters */,
42 | columns: columns,
43 | highlightColor: highlightColor,
44 | highlightedChapters: highlightedChapters,
45 | extrasInStack: (chapter) {
46 | if (chapter == null) {
47 | return [];
48 | }
49 | var newBadge = showNewBadge && chapter.isNew ? NewBadge() : null;
50 | var customBadge = customBadgeBuilder?.call(chapter.cid);
51 | return [
52 | if (newBadge != null) newBadge,
53 | if (customBadge != null) customBadge,
54 | ];
55 | },
56 | itemBuilder: itemBuilder == null
57 | ? null //
58 | : (ctx, chapter, itemWidget) => itemBuilder!.call(ctx, chapter?.cid, itemWidget),
59 | onChapterPressed: (chapter) {
60 | if (chapter != null) {
61 | onChapterPressed.call(chapter.cid);
62 | }
63 | },
64 | onChapterLongPressed: onChapterLongPressed == null
65 | ? null
66 | : (chapter) {
67 | if (chapter != null) {
68 | onChapterLongPressed!.call(chapter.cid);
69 | }
70 | },
71 | );
72 | }
73 |
74 | @override
75 | Widget build(BuildContext context) {
76 | var isEmpty = this.groups.isEmpty || this.groups.allChapters.isEmpty;
77 | if (isEmpty) {
78 | return Padding(
79 | padding: EdgeInsets.symmetric(vertical: 10),
80 | child: Center(
81 | child: Text('暂无章节', style: Theme.of(context).textTheme.subtitle1),
82 | ),
83 | );
84 | }
85 |
86 | var groups = this.groups.makeSureRegularGroupIsFirst(); // 保证【单话】为首个章节分组
87 | return Column(
88 | children: [
89 | SizedBox(height: 10),
90 | for (var i = 0; i < groups.length; i++) ...[
91 | Padding(
92 | padding: EdgeInsets.symmetric(horizontal: 12),
93 | child: Text(
94 | '・${groups[i].title}・',
95 | style: Theme.of(context).textTheme.subtitle1,
96 | ),
97 | ),
98 | SizedBox(height: 10),
99 | SizedBox(
100 | width: MediaQuery.of(context).size.width,
101 | child: _buildGrid(
102 | idx: i,
103 | chapters: groups[i].chapters,
104 | ),
105 | ),
106 | SizedBox(height: 10),
107 | ],
108 | ],
109 | );
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/lib/page/manga_detail.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/manga.dart';
4 | import 'package:manhuagui_flutter/page/view/detail_table.dart';
5 |
6 | /// 漫画详情页,展示所给 [Manga] 信息
7 | class MangaDetailPage extends StatefulWidget {
8 | const MangaDetailPage({
9 | Key? key,
10 | required this.data,
11 | }) : super(key: key);
12 |
13 | final Manga data;
14 |
15 | @override
16 | _MangaDetailPageState createState() => _MangaDetailPageState();
17 | }
18 |
19 | class _MangaDetailPageState extends State {
20 | final _controller = ScrollController();
21 |
22 | @override
23 | void dispose() {
24 | _controller.dispose();
25 | super.dispose();
26 | }
27 |
28 | @override
29 | Widget build(BuildContext context) {
30 | return Scaffold(
31 | appBar: AppBar(
32 | title: Text('漫画详情'),
33 | leading: AppBarActionButton.leading(context: context),
34 | ),
35 | body: ExtendedScrollbar(
36 | controller: _controller,
37 | interactive: true,
38 | mainAxisMargin: 2,
39 | crossAxisMargin: 2,
40 | child: ListView(
41 | controller: _controller,
42 | padding: EdgeInsets.symmetric(horizontal: 20, vertical: 15),
43 | physics: AlwaysScrollableScrollPhysics(),
44 | children: [
45 | DetailTableView(
46 | rows: [
47 | DetailRow('mid', widget.data.mid.toString()),
48 | DetailRow('标题', '《${widget.data.title}》', textForCopy: widget.data.title),
49 | DetailRow(
50 | '标题别名',
51 | widget.data.aliases.isEmpty ? '暂无' : widget.data.aliases.map((a) => '《$a》').join('\n'),
52 | textForCopy: widget.data.aliases.join('\n'),
53 | canCopy: widget.data.aliases.isNotEmpty,
54 | ),
55 | DetailRow('封面链接', widget.data.cover),
56 | DetailRow('网页链接', widget.data.url),
57 | DetailRow('状态', widget.data.finished ? '已完结' : '连载中'),
58 | DetailRow('出版年份', widget.data.publishYear),
59 | DetailRow('漫画地区', widget.data.mangaZone),
60 | DetailRow('漫画类别', widget.data.genres.map((g) => g.title).join(', ')),
61 | DetailRow('漫画作者', widget.data.authors.map((a) => a.name).join(', ')),
62 | DetailRow('最新章节', widget.data.newestChapter),
63 | DetailRow('更新时间', widget.data.formattedNewestDate),
64 | DetailRow('总章节数', widget.data.chapterGroups.expand((g) => g.chapters).length.toString()),
65 | DetailRow('章节分组数', widget.data.chapterGroups.length.toString()),
66 | for (var group in widget.data.chapterGroups) //
67 | DetailRow('【${group.title}】章节数', group.chapters.length.toString()),
68 | DetailRow('包含色情暴力', widget.data.banned ? '是' : '否', canCopy: false),
69 | DetailRow('拥有版权', widget.data.copyright ? '是' : '否', canCopy: false),
70 | DetailRow('漫画排名', widget.data.mangaRank),
71 | DetailRow('平均得分', widget.data.averageScore.toStringAsFixed(1)),
72 | DetailRow('评分人数', widget.data.scoreCount.toString()),
73 | for (var num in [5, 4, 3, 2, 1]) //
74 | DetailRow('评 $num 星比例', widget.data.perScores[num]),
75 | DetailRow('简要介绍', widget.data.briefIntroduction),
76 | DetailRow('详细介绍', widget.data.introduction),
77 | ],
78 | tableWidth: MediaQuery.of(context).size.width - MediaQuery.of(context).padding.horizontal - 40,
79 | ),
80 | ],
81 | ),
82 | ),
83 | floatingActionButton: ScrollAnimatedFab(
84 | scrollController: _controller,
85 | condition: ScrollAnimatedCondition.direction,
86 | fab: FloatingActionButton(
87 | child: Icon(Icons.vertical_align_top),
88 | heroTag: null,
89 | onPressed: () => _controller.scrollToTop(),
90 | ),
91 | ),
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/lib/page/manga_aud_ranking.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/model/common.dart';
4 | import 'package:manhuagui_flutter/model/manga.dart';
5 | import 'package:manhuagui_flutter/page/view/app_drawer.dart';
6 | import 'package:manhuagui_flutter/page/view/corner_icons.dart';
7 | import 'package:manhuagui_flutter/page/view/homepage_column.dart';
8 | import 'package:manhuagui_flutter/page/view/manga_aud_ranking.dart';
9 | import 'package:manhuagui_flutter/page/view/manga_ranking_line.dart';
10 |
11 | /// 漫画受众排行榜页,展示所给单个 [MangaRanking] 列表信息
12 | class MangaAudRankingPage extends StatefulWidget {
13 | const MangaAudRankingPage({
14 | Key? key,
15 | required this.type,
16 | required this.rankings,
17 | required this.rankingDatetime,
18 | }) : super(key: key);
19 |
20 | final MangaAudRankingType type;
21 | final List rankings;
22 | final DateTime? rankingDatetime;
23 |
24 | @override
25 | State createState() => _MangaAudRankingPageState();
26 | }
27 |
28 | class _MangaAudRankingPageState extends State {
29 | final _controller = ScrollController();
30 |
31 | @override
32 | void initState() {
33 | super.initState();
34 | WidgetsBinding.instance?.addPostFrameCallback((_) => _loadData());
35 | }
36 |
37 | @override
38 | void dispose() {
39 | _controller.dispose();
40 | _flagStorage.dispose();
41 | super.dispose();
42 | }
43 |
44 | late final _flagStorage = MangaCornerFlagStorage(stateSetter: () => mountedSetState(() {}));
45 |
46 | Future _loadData() async {
47 | _flagStorage.queryAndStoreFlags(mangaIds: widget.rankings.map((e) => e.mid)).then((_) => mountedSetState(() {}));
48 | }
49 |
50 | @override
51 | Widget build(BuildContext context) {
52 | return Scaffold(
53 | appBar: AppBar(
54 | title: Text('漫画受众排行榜'),
55 | leading: AppBarActionButton.leading(context: context, allowDrawerButton: false),
56 | ),
57 | drawer: AppDrawer(
58 | currentSelection: DrawerSelection.none,
59 | ),
60 | drawerEdgeDragWidth: MediaQuery.of(context).size.width,
61 | body: ExtendedScrollbar(
62 | controller: _controller,
63 | interactive: true,
64 | mainAxisMargin: 2,
65 | crossAxisMargin: 2,
66 | child: ListView(
67 | controller: _controller,
68 | padding: EdgeInsets.zero,
69 | physics: AlwaysScrollableScrollPhysics(),
70 | children: [
71 | HomepageColumnView(
72 | title: '日排行榜 - ' +
73 | (widget.type == MangaAudRankingType.all
74 | ? '全部漫画'
75 | : widget.type == MangaAudRankingType.qingnian
76 | ? '青年漫画'
77 | : '少女漫画'),
78 | icon: Icons.emoji_events,
79 | rightText: '更新于 ${formatDatetimeAndDuration(widget.rankingDatetime ?? DateTime.now(), FormatPattern.date)}',
80 | padding: EdgeInsets.zero,
81 | childColor: Theme.of(context).scaffoldBackgroundColor,
82 | child: Column(
83 | children: [
84 | for (var manga in widget.rankings) ...[
85 | Divider(height: 0, thickness: 1),
86 | MangaRankingLineView(
87 | manga: manga,
88 | flags: _flagStorage.getFlags(mangaId: manga.mid),
89 | ),
90 | ],
91 | ],
92 | ),
93 | ),
94 | ],
95 | ),
96 | ),
97 | floatingActionButton: ScrollAnimatedFab(
98 | scrollController: _controller,
99 | condition: ScrollAnimatedCondition.direction,
100 | fab: FloatingActionButton(
101 | child: Icon(Icons.vertical_align_top),
102 | heroTag: null,
103 | onPressed: () => _controller.scrollToTop(),
104 | ),
105 | ),
106 | );
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/lib/page/page/home.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/page/page/home_overall.dart';
4 | import 'package:manhuagui_flutter/page/page/home_ranking.dart';
5 | import 'package:manhuagui_flutter/page/page/home_recent.dart';
6 | import 'package:manhuagui_flutter/page/page/home_recommend.dart';
7 | import 'package:manhuagui_flutter/page/search.dart';
8 | import 'package:manhuagui_flutter/service/evb/evb_manager.dart';
9 | import 'package:manhuagui_flutter/service/evb/events.dart';
10 |
11 | /// 首页
12 | class HomeSubPage extends StatefulWidget {
13 | const HomeSubPage({
14 | Key? key,
15 | this.action,
16 | }) : super(key: key);
17 |
18 | final ActionController? action;
19 |
20 | @override
21 | _HomeSubPageState createState() => _HomeSubPageState();
22 | }
23 |
24 | class _HomeSubPageState extends State with SingleTickerProviderStateMixin {
25 | late final _controller = TabController(length: 4, vsync: this);
26 | late final _keys = List.generate(4, (_) => GlobalKey>());
27 | late final _actions = List.generate(4, (_) => ActionController());
28 | late final _tabs = [
29 | Tuple2('推荐', RecommendSubPage(key: _keys[0], action: _actions[0])),
30 | Tuple2('更新', RecentSubPage(key: _keys[1], action: _actions[1])),
31 | Tuple2('全部', OverallSubPage(key: _keys[2], action: _actions[2])),
32 | Tuple2('排行', RankingSubPage(key: _keys[3], action: _actions[3])),
33 | ];
34 | final _cancelHandlers = [];
35 |
36 | @override
37 | void initState() {
38 | super.initState();
39 | widget.action?.addAction(() => _actions[_controller.index].invoke());
40 | _cancelHandlers.add(EventBusManager.instance.listen((_) {
41 | _keys.where((k) => k.currentState?.mounted == true).forEach((k) => k.currentState?.setState(() {}));
42 | if (mounted) setState(() {});
43 | }));
44 | _cancelHandlers.add(EventBusManager.instance.listen((_) => _controller.animateTo(0)));
45 | _cancelHandlers.add(EventBusManager.instance.listen((_) => _controller.animateTo(1)));
46 | _cancelHandlers.add(EventBusManager.instance.listen((_) => _controller.animateTo(3)));
47 | }
48 |
49 | @override
50 | void dispose() {
51 | widget.action?.removeAction();
52 | _cancelHandlers.forEach((c) => c.call());
53 | _controller.dispose();
54 | _actions.forEach((a) => a.dispose());
55 | super.dispose();
56 | }
57 |
58 | @override
59 | Widget build(BuildContext context) {
60 | return Scaffold(
61 | appBar: AppBar(
62 | title: TabBar(
63 | controller: _controller,
64 | isScrollable: true,
65 | indicatorSize: TabBarIndicatorSize.label,
66 | tabs: [
67 | for (var t in _tabs)
68 | Padding(
69 | padding: EdgeInsets.symmetric(vertical: 5),
70 | child: Text(
71 | t.item1,
72 | style: Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.white, fontSize: 16),
73 | ),
74 | ),
75 | ],
76 | onTap: (idx) {
77 | if (!_controller.indexIsChanging) {
78 | _actions[idx].invoke();
79 | }
80 | },
81 | ),
82 | leading: AppBarActionButton.leading(context: context, allowDrawerButton: true),
83 | actions: [
84 | AppBarActionButton(
85 | icon: Icon(Icons.search),
86 | tooltip: '搜索漫画',
87 | onPressed: () => Navigator.of(context).push(
88 | CustomPageRoute(
89 | context: context,
90 | builder: (c) => SearchPage(),
91 | ),
92 | ),
93 | ),
94 | ],
95 | ),
96 | body: TabBarView(
97 | controller: _controller,
98 | physics: DefaultScrollPhysics.of(context),
99 | children: _tabs.map((t) => t.item2).toList(),
100 | ),
101 | );
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/lib/page/view/image_load.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/service/dio/wrap_error.dart';
4 |
5 | /// 图片加载和错误控件,在 [MangaGalleryView] / [ImageViewerPage] 使用
6 |
7 | class ImageLoadingView extends StatelessWidget {
8 | const ImageLoadingView({
9 | Key? key,
10 | required this.title,
11 | required this.event,
12 | }) : super(key: key);
13 |
14 | final String title;
15 | final ImageChunkEvent? event;
16 |
17 | @override
18 | Widget build(BuildContext context) {
19 | var event = this.event;
20 | return Container(
21 | color: Colors.black,
22 | padding: EdgeInsets.symmetric(vertical: 40),
23 | constraints: BoxConstraints(
24 | maxWidth: MediaQuery.of(context).size.width - MediaQuery.of(context).padding.horizontal,
25 | ),
26 | child: Column(
27 | mainAxisAlignment: MainAxisAlignment.center,
28 | children: [
29 | if (title.isNotEmpty)
30 | Text(
31 | title,
32 | textAlign: TextAlign.center,
33 | style: TextStyle(fontSize: 45, color: Colors.grey),
34 | ),
35 | Padding(
36 | padding: EdgeInsets.all(30),
37 | child: SizedBox(
38 | height: 50,
39 | width: 50,
40 | child: Padding(
41 | padding: EdgeInsets.all(4.5 / 2),
42 | child: CircularProgressIndicator(
43 | strokeWidth: 4.5,
44 | ),
45 | ),
46 | ),
47 | ),
48 | Padding(
49 | padding: EdgeInsets.symmetric(horizontal: 30),
50 | child: Text(
51 | event == null
52 | ? ' '
53 | : (event.expectedTotalBytes ?? 0) == 0
54 | ? ' ${filesize(event.cumulativeBytesLoaded)} '
55 | : ' ${filesize(event.cumulativeBytesLoaded)} / ${filesize(event.expectedTotalBytes!)} ',
56 | style: TextStyle(color: Colors.grey),
57 | textAlign: TextAlign.center,
58 | ),
59 | ),
60 | ],
61 | ),
62 | );
63 | }
64 | }
65 |
66 | class ImageLoadFailedView extends StatelessWidget {
67 | const ImageLoadFailedView({
68 | Key? key,
69 | required this.title,
70 | this.error,
71 | this.errorFormatter,
72 | }) : super(key: key);
73 |
74 | final String title;
75 | final dynamic error;
76 | final String? Function(dynamic error)? errorFormatter;
77 |
78 | @override
79 | Widget build(BuildContext context) {
80 | return Container(
81 | color: Colors.black,
82 | padding: EdgeInsets.symmetric(vertical: 40),
83 | constraints: BoxConstraints(
84 | maxWidth: MediaQuery.of(context).size.width - MediaQuery.of(context).padding.horizontal,
85 | ),
86 | child: Column(
87 | mainAxisAlignment: MainAxisAlignment.center,
88 | children: [
89 | if (title.isNotEmpty)
90 | Text(
91 | title,
92 | textAlign: TextAlign.center,
93 | style: TextStyle(fontSize: 45, color: Colors.grey),
94 | ),
95 | Padding(
96 | padding: EdgeInsets.all(30),
97 | child: Container(
98 | width: 50,
99 | height: 50,
100 | child: Icon(
101 | Icons.broken_image,
102 | color: Colors.grey,
103 | size: 50,
104 | ),
105 | ),
106 | ),
107 | Padding(
108 | padding: EdgeInsets.symmetric(horizontal: 30),
109 | child: Text(
110 | errorFormatter?.call(error) ??
111 | (error == null //
112 | ? '未知错误'
113 | : wrapError(error, StackTrace.empty).text),
114 | style: TextStyle(color: Colors.grey),
115 | textAlign: TextAlign.center,
116 | ),
117 | ),
118 | ],
119 | ),
120 | );
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/lib/model/author.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'author.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | Author _$AuthorFromJson(Map json) => Author(
10 | aid: json['aid'] as int,
11 | name: json['name'] as String,
12 | alias: json['alias'] as String,
13 | zone: json['zone'] as String,
14 | cover: json['cover'] as String,
15 | url: json['url'] as String,
16 | mangaCount: json['manga_count'] as int,
17 | newestMangaId: json['newest_manga_id'] as int,
18 | newestMangaTitle: json['newest_manga_title'] as String,
19 | newestMangaUrl: json['newest_manga_url'] as String,
20 | newestDate: json['newest_date'] as String,
21 | highestMangaId: json['highest_manga_id'] as int,
22 | highestMangaTitle: json['highest_manga_title'] as String,
23 | highestMangaUrl: json['highest_manga_url'] as String,
24 | highestScore: (json['highest_score'] as num).toDouble(),
25 | averageScore: (json['average_score'] as num).toDouble(),
26 | popularity: json['popularity'] as int,
27 | introduction: json['introduction'] as String,
28 | relatedAuthors: (json['related_authors'] as List)
29 | .map((e) => TinyZonedAuthor.fromJson(e as Map))
30 | .toList(),
31 | );
32 |
33 | Map _$AuthorToJson(Author instance) => {
34 | 'aid': instance.aid,
35 | 'name': instance.name,
36 | 'alias': instance.alias,
37 | 'zone': instance.zone,
38 | 'cover': instance.cover,
39 | 'url': instance.url,
40 | 'manga_count': instance.mangaCount,
41 | 'newest_manga_id': instance.newestMangaId,
42 | 'newest_manga_title': instance.newestMangaTitle,
43 | 'newest_manga_url': instance.newestMangaUrl,
44 | 'newest_date': instance.newestDate,
45 | 'highest_manga_id': instance.highestMangaId,
46 | 'highest_manga_title': instance.highestMangaTitle,
47 | 'highest_manga_url': instance.highestMangaUrl,
48 | 'highest_score': instance.highestScore,
49 | 'average_score': instance.averageScore,
50 | 'popularity': instance.popularity,
51 | 'introduction': instance.introduction,
52 | 'related_authors': instance.relatedAuthors,
53 | };
54 |
55 | SmallAuthor _$SmallAuthorFromJson(Map json) => SmallAuthor(
56 | aid: json['aid'] as int,
57 | name: json['name'] as String,
58 | zone: json['zone'] as String,
59 | cover: json['cover'] as String,
60 | url: json['url'] as String,
61 | mangaCount: json['manga_count'] as int,
62 | newestDate: json['newest_date'] as String,
63 | );
64 |
65 | Map _$SmallAuthorToJson(SmallAuthor instance) =>
66 | {
67 | 'aid': instance.aid,
68 | 'name': instance.name,
69 | 'zone': instance.zone,
70 | 'cover': instance.cover,
71 | 'url': instance.url,
72 | 'manga_count': instance.mangaCount,
73 | 'newest_date': instance.newestDate,
74 | };
75 |
76 | TinyAuthor _$TinyAuthorFromJson(Map json) => TinyAuthor(
77 | aid: json['aid'] as int,
78 | name: json['name'] as String,
79 | url: json['url'] as String,
80 | );
81 |
82 | Map _$TinyAuthorToJson(TinyAuthor instance) =>
83 | {
84 | 'aid': instance.aid,
85 | 'name': instance.name,
86 | 'url': instance.url,
87 | };
88 |
89 | TinyZonedAuthor _$TinyZonedAuthorFromJson(Map json) =>
90 | TinyZonedAuthor(
91 | aid: json['aid'] as int,
92 | name: json['name'] as String,
93 | url: json['url'] as String,
94 | zone: json['zone'] as String,
95 | );
96 |
97 | Map _$TinyZonedAuthorToJson(TinyZonedAuthor instance) =>
98 | {
99 | 'aid': instance.aid,
100 | 'name': instance.name,
101 | 'url': instance.url,
102 | 'zone': instance.zone,
103 | };
104 |
--------------------------------------------------------------------------------
/lib/service/storage/storage.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:io' show File, Directory;
3 |
4 | import 'package:external_path/external_path.dart';
5 | import 'package:flutter_cache_manager/flutter_cache_manager.dart';
6 | import 'package:intl/intl.dart';
7 | import 'package:manhuagui_flutter/config.dart';
8 | import 'package:path/path.dart' as path_;
9 | import 'package:path_provider/path_provider.dart';
10 |
11 | // ============
12 | // storage path
13 | // ============
14 |
15 | Future getPublicStorageDirectoryPath() async {
16 | final storageDirectories = await ExternalPath.getExternalStorageDirectories(); // external public
17 | if (storageDirectories.isEmpty) {
18 | throw Exception('Cannot get public storage directory.');
19 | }
20 | return await PathUtils.joinPathAndCheck(
21 | [storageDirectories.first, APP_NAME],
22 | isDirectoryPath: true,
23 | ); // /storage/emulated/0/Manhuagui
24 | }
25 |
26 | Future getPrivateStorageDirectoryPath() async {
27 | final storageDirectory = await getExternalStorageDirectory(); // external private (sandbox)
28 | if (storageDirectory == null) {
29 | throw Exception('Cannot get private storage directory.');
30 | }
31 | return await PathUtils.joinPathAndCheck(
32 | [storageDirectory.path],
33 | isDirectoryPath: true,
34 | ); // /storage/emulated/0/android/com.aoihosizora.manhuagui/files
35 | }
36 |
37 | Future getSharedPicturesDirectoryPath() async {
38 | final sharedDirectoryPath = await ExternalPath.getExternalStoragePublicDirectory(ExternalPath.DIRECTORY_PICTURES); // external shared
39 | if (sharedDirectoryPath.isEmpty) {
40 | throw Exception('Cannot get shared pictures directory.');
41 | }
42 | return await PathUtils.joinPathAndCheck(
43 | [sharedDirectoryPath],
44 | isDirectoryPath: true,
45 | ); // /storage/emulated/0/Pictures
46 | }
47 |
48 | String getTimestampTokenForFilename([DateTime? time, String? pattern]) {
49 | final df = DateFormat(pattern ?? 'yyyyMMdd_HHmmss_SSS');
50 | return df.format(time ?? DateTime.now());
51 | }
52 |
53 | // ==========
54 | // path utils
55 | // ==========
56 |
57 | class PathUtils {
58 | static String joinPath(List paths) {
59 | return path_.joinAll(paths);
60 | }
61 |
62 | static String getWithoutExtension(String path) {
63 | return path_.withoutExtension(path);
64 | }
65 |
66 | static String getExtension(String path) {
67 | return path_.extension(path);
68 | }
69 |
70 | static String getBasename(String path) {
71 | return path_.basename(path);
72 | }
73 |
74 | static String getDirname(String path) {
75 | return path_.dirname(path);
76 | }
77 |
78 | static Future joinPathAndCheck(List paths, {bool isDirectoryPath = false}) async {
79 | var newPath = path_.joinAll(paths);
80 | var directory = Directory(isDirectoryPath ? newPath : path_.dirname(newPath));
81 | if (!(await directory.exists())) {
82 | await directory.create(recursive: true);
83 | }
84 | return newPath;
85 | }
86 | }
87 |
88 | // =====
89 | // cache
90 | // =====
91 |
92 | Future getDefaultCacheManagerDirectoryPath() async {
93 | var baseDir = await getTemporaryDirectory();
94 | return PathUtils.joinPath([baseDir.path, DefaultCacheManager.key]);
95 | }
96 |
97 | Future getDefaultCacheManagerDirectoryBytes() async {
98 | var cachePath = await getDefaultCacheManagerDirectoryPath();
99 | var directory = Directory(cachePath);
100 | if (!(await directory.exists())) {
101 | return 0;
102 | }
103 |
104 | var totalBytes = 0;
105 | await for (var entity in directory.list(recursive: true, followLinks: false)) {
106 | if (entity is File) {
107 | totalBytes += await entity.length();
108 | }
109 | }
110 | return totalBytes;
111 | }
112 |
113 | Future getCachedOrDownloadedFilepath({String? url, File? file}) async {
114 | if (file == null || !(await file.exists())) {
115 | if (url != null && url.isNotEmpty) {
116 | var info = await DefaultCacheManager().getFileFromCache(url);
117 | file = info?.file;
118 | }
119 | }
120 | if (file == null || !(await file.exists())) {
121 | return null;
122 | }
123 | return file.path;
124 | }
125 |
--------------------------------------------------------------------------------
/lib/page/view/detail_table.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_ahlib/flutter_ahlib.dart';
3 | import 'package:manhuagui_flutter/service/native/clipboard.dart';
4 |
5 | class DetailRow {
6 | const DetailRow(
7 | this.key,
8 | this.value, {
9 | this.textForCopy,
10 | this.canCopy = true,
11 | });
12 |
13 | final String key;
14 | final String value;
15 | final String? textForCopy;
16 | final bool canCopy;
17 | }
18 |
19 | /// 详细信息表格,在 [MangaDetailPage] / [AuthorDetailPage] / [ChapterDetailsPage] 使用
20 | class DetailTableView extends StatefulWidget {
21 | const DetailTableView({
22 | Key? key,
23 | required this.rows,
24 | required this.tableWidth,
25 | this.padding = const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
26 | this.fractionColumnWidth = 0.3,
27 | }) : super(key: key);
28 |
29 | final List rows;
30 | final double tableWidth;
31 | final EdgeInsets padding;
32 | final double fractionColumnWidth;
33 |
34 | @override
35 | State createState() => _DetailTableViewState();
36 | }
37 |
38 | class _DetailTableViewState extends State {
39 | late var _helper = TableCellHelper(widget.rows.length, 2);
40 |
41 | @override
42 | void didUpdateWidget(covariant DetailTableView oldWidget) {
43 | super.didUpdateWidget(oldWidget);
44 | if (oldWidget.rows.length != widget.rows.length) {
45 | _helper = TableCellHelper(widget.rows.length, 2);
46 | }
47 | }
48 |
49 | @override
50 | Widget build(BuildContext context) {
51 | if (!_helper.hasSearched()) {
52 | WidgetsBinding.instance?.addPostFrameCallback((_) {
53 | if (_helper.searchForHighestCells()) {
54 | if (mounted) setState(() {});
55 | }
56 | });
57 | }
58 |
59 | final firstLineStyle = Theme.of(context).textTheme.bodyText2?.copyWith(color: Colors.grey);
60 | final textStyle = Theme.of(context).textTheme.bodyText2;
61 |
62 | return Table(
63 | columnWidths: {
64 | 0: FractionColumnWidth(widget.fractionColumnWidth),
65 | },
66 | border: TableBorder(
67 | horizontalInside: BorderSide(width: 1, color: Colors.grey),
68 | ),
69 | children: [
70 | TableRow(
71 | children: [
72 | Padding(padding: widget.padding, child: Text('键', style: firstLineStyle)),
73 | Padding(padding: widget.padding, child: Text('值', style: firstLineStyle)),
74 | ],
75 | ),
76 | for (var i = 0; i < widget.rows.length; i++)
77 | TableRow(
78 | children: [
79 | TableCell(
80 | key: _helper.getCellKey(i, 0),
81 | verticalAlignment: _helper.determineCellAlignment(i, 0, TableCellVerticalAlignment.top),
82 | child: TableWholeRowInkWell.preferred(
83 | child: Text('${widget.rows[i].key} ', style: textStyle),
84 | padding: widget.padding,
85 | onTap: () {
86 | if (widget.rows[i].canCopy) {
87 | copyText(widget.rows[i].textForCopy ?? widget.rows[i].value, showToast: true);
88 | }
89 | },
90 | tableWidth: widget.tableWidth,
91 | accumulativeWidthRatio: 0,
92 | ),
93 | ),
94 | TableCell(
95 | key: _helper.getCellKey(i, 1),
96 | verticalAlignment: _helper.determineCellAlignment(i, 1, TableCellVerticalAlignment.top),
97 | child: TableWholeRowInkWell.preferred(
98 | child: Text('${widget.rows[i].value} ', style: textStyle),
99 | padding: widget.padding,
100 | onTap: () {
101 | if (widget.rows[i].canCopy) {
102 | copyText(widget.rows[i].textForCopy ?? widget.rows[i].value, showToast: true);
103 | }
104 | },
105 | tableWidth: widget.tableWidth,
106 | accumulativeWidthRatio: widget.fractionColumnWidth,
107 | ),
108 | ),
109 | ],
110 | ),
111 | ],
112 | );
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/lib/service/native/android.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:device_info_plus/device_info_plus.dart';
4 | import 'package:flutter/services.dart';
5 | import 'package:manhuagui_flutter/app_setting.dart';
6 |
7 | // =======
8 | // version
9 | // =======
10 |
11 | int? _androidSDKVersion;
12 |
13 | Future