>(paths: Vec) -> String {
5 | match paths.len() {
6 | 0 => String::default(),
7 | _ => {
8 | let mut path: PathBuf = PathBuf::new();
9 | for x in paths {
10 | path = path.join(x);
11 | }
12 | return path.to_str().unwrap().to_string();
13 | }
14 | }
15 | }
16 |
17 | pub(crate) fn create_dir_if_not_exists(path: &String) {
18 | if !Path::new(path).exists() {
19 | std::fs::create_dir_all(path).unwrap();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/macos/Runner/Configs/Warnings.xcconfig:
--------------------------------------------------------------------------------
1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
2 | GCC_WARN_UNDECLARED_SELECTOR = YES
3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
6 | CLANG_WARN_PRAGMA_PACK = YES
7 | CLANG_WARN_STRICT_PROTOTYPES = YES
8 | CLANG_WARN_COMMA = YES
9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES
10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
12 | GCC_WARN_SHADOW = YES
13 | CLANG_WARN_UNREACHABLE_CODE = YES
14 |
--------------------------------------------------------------------------------
/windows/runner/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.14)
2 | project(runner LANGUAGES CXX)
3 |
4 | add_executable(${BINARY_NAME} WIN32
5 | "flutter_window.cpp"
6 | "main.cpp"
7 | "utils.cpp"
8 | "win32_window.cpp"
9 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
10 | "Runner.rc"
11 | "runner.exe.manifest"
12 | )
13 | apply_standard_settings(${BINARY_NAME})
14 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
15 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
16 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
17 | add_dependencies(${BINARY_NAME} flutter_assemble)
18 |
--------------------------------------------------------------------------------
/macos/Runner/Configs/AppInfo.xcconfig:
--------------------------------------------------------------------------------
1 | // Application-level settings for the Runner target.
2 | //
3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
4 | // future. If not, the values below would default to using the project name when this becomes a
5 | // 'flutter create' template.
6 |
7 | // The application's name. By default this is also the title of the Flutter window.
8 | PRODUCT_NAME = html
9 |
10 | // The application's bundle identifier
11 | PRODUCT_BUNDLE_IDENTIFIER = niuhuan.html
12 |
13 | // The copyright displayed in application information
14 | PRODUCT_COPYRIGHT = Copyright © 2022 niuhuan. All rights reserved.
15 |
--------------------------------------------------------------------------------
/native/src/utils.rs:
--------------------------------------------------------------------------------
1 | use std::collections::hash_map::DefaultHasher;
2 | use std::hash::Hasher;
3 |
4 | use lazy_static::lazy_static;
5 | use tokio::sync::{Mutex, MutexGuard};
6 |
7 | lazy_static! {
8 | static ref HASH_LOCK: Vec> = {
9 | let mut mutex_vec: Vec> = vec![];
10 | for _ in 0..64 {
11 | mutex_vec.push(Mutex::<()>::new(()));
12 | }
13 | mutex_vec
14 | };
15 | }
16 |
17 | pub(crate) async fn hash_lock(url: &String) -> MutexGuard<'static, ()> {
18 | let mut s = DefaultHasher::new();
19 | s.write(url.as_bytes());
20 | HASH_LOCK[s.finish() as usize % HASH_LOCK.len()]
21 | .lock()
22 | .await
23 | }
24 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | **/dgph
2 | *.mode1v3
3 | *.mode2v3
4 | *.moved-aside
5 | *.pbxuser
6 | *.perspectivev3
7 | **/*sync/
8 | .sconsign.dblite
9 | .tags*
10 | **/.vagrant/
11 | **/DerivedData/
12 | Icon?
13 | **/Pods/
14 | **/.symlinks/
15 | profile
16 | xcuserdata
17 | **/.generated/
18 | Flutter/App.framework
19 | Flutter/Flutter.framework
20 | Flutter/Flutter.podspec
21 | Flutter/Generated.xcconfig
22 | Flutter/ephemeral/
23 | Flutter/app.flx
24 | Flutter/app.zip
25 | Flutter/flutter_assets/
26 | Flutter/flutter_export_environment.sh
27 | ServiceDefinitions.json
28 | Runner/GeneratedPluginRegistrant.*
29 |
30 | # Exceptions to above rules.
31 | !default.mode1v3
32 | !default.mode2v3
33 | !default.pbxuser
34 | !default.perspectivev3
35 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/scripts/thin-payload.sh:
--------------------------------------------------------------------------------
1 | # 精简Payload文件夹 (上传到AppStore会自动区分平台, 此代码仅用于构建非签名ipa)
2 |
3 | foreachThin(){
4 | for file in $1/*
5 | do
6 | if test -f $file
7 | then
8 | mime=$(file --mime-type -b $file)
9 | if [ "$mime" == 'application/x-mach-binary' ] || [ "${file##*.}"x = "dylib"x ]
10 | then
11 | echo thin $file
12 | xcrun -sdk iphoneos lipo "$file" -thin arm64 -output "$file"
13 | xcrun -sdk iphoneos bitcode_strip "$file" -r -o "$file"
14 | strip -S -x "$file" -o "$file"
15 | fi
16 | fi
17 | if test -d $file
18 | then
19 | foreachThin $file
20 | fi
21 | done
22 | }
23 |
24 | foreachThin ./Payload
25 |
--------------------------------------------------------------------------------
/windows/runner/utils.h:
--------------------------------------------------------------------------------
1 | #ifndef RUNNER_UTILS_H_
2 | #define RUNNER_UTILS_H_
3 |
4 | #include
5 | #include
6 |
7 | // Creates a console for the process, and redirects stdout and stderr to
8 | // it for both the runner and the Flutter library.
9 | void CreateAndAttachConsole();
10 |
11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
12 | // encoded in UTF-8. Returns an empty std::string on failure.
13 | std::string Utf8FromUtf16(const wchar_t* utf16_string);
14 |
15 | // Gets the command line arguments passed in as a std::vector,
16 | // encoded in UTF-8. Returns an empty std::vector on failure.
17 | std::vector GetCommandLineArguments();
18 |
19 | #endif // RUNNER_UTILS_H_
20 |
--------------------------------------------------------------------------------
/lib/assets/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/screens/components/error_types.dart:
--------------------------------------------------------------------------------
1 | const errorTypeNetwork = "NETWORK_ERROR";
2 | const errorTypePermission = "PERMISSION_ERROR";
3 | const errorTypeTime = "TIME_ERROR";
4 |
5 | // 错误的类型, 方便照展示和谐的提示
6 | String errorType(String error) {
7 | // EXCEPTION
8 | if (error.contains("timeout") ||
9 | error.contains("tcp connect") ||
10 | error.contains("connection refused") ||
11 | error.contains("deadline") ||
12 | error.contains("connection abort") ||
13 | error.contains("certificate") ||
14 | error.contains("x509") ||
15 | error.contains("ssl")) {
16 | return errorTypeNetwork;
17 | }
18 | if (error.contains("permission denied")) {
19 | return errorTypePermission;
20 | }
21 | if (error.contains("time is not synchronize")) {
22 | return errorTypeTime;
23 | }
24 | return "";
25 | }
26 |
--------------------------------------------------------------------------------
/justfile:
--------------------------------------------------------------------------------
1 | # Homebrew installs LLVM in a place that is not visible to ffigen.
2 | # This explicitly specifies the place where the LLVM dylibs are kept.
3 | llvm_path := if os() == "macos" {
4 | "--llvm-path /opt/homebrew/opt/llvm"
5 | } else {
6 | ""
7 | }
8 |
9 | default: gen lint
10 |
11 | gen:
12 | flutter_rust_bridge_codegen {{llvm_path}} \
13 | --rust-input native/src/api.rs \
14 | --dart-output lib/bridge_generated.dart \
15 | --c-output ios/Runner/bridge_generated.h
16 | cp ios/Runner/bridge_generated.h macos/Runner/bridge_generated.h
17 | # Uncomment this line to invoke build_runner as well
18 | # flutter pub run build_runner build
19 |
20 | lint:
21 | cd native && cargo fmt
22 | dart format .
23 |
24 | clean:
25 | flutter clean
26 | cd native && cargo clean
27 |
28 | # vim:expandtab:sw=4:ts=4
29 |
--------------------------------------------------------------------------------
/linux/rust.cmake:
--------------------------------------------------------------------------------
1 | # We include Corrosion inline here, but ideally in a project with
2 | # many dependencies we would need to install Corrosion on the system.
3 | # See instructions on https://github.com/AndrewGaspar/corrosion#cmake-install
4 | # Once done, uncomment this line:
5 | # find_package(Corrosion REQUIRED)
6 |
7 | include(FetchContent)
8 |
9 | FetchContent_Declare(
10 | Corrosion
11 | GIT_REPOSITORY https://github.com/AndrewGaspar/corrosion.git
12 | GIT_TAG origin/master # Optionally specify a version tag or branch here
13 | )
14 |
15 | FetchContent_MakeAvailable(Corrosion)
16 |
17 | corrosion_import_crate(MANIFEST_PATH ../native/Cargo.toml)
18 |
19 | # Flutter-specific
20 |
21 | set(CRATE_NAME "native")
22 |
23 | target_link_libraries(${BINARY_NAME} PRIVATE ${CRATE_NAME})
24 |
25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
26 |
--------------------------------------------------------------------------------
/windows/rust.cmake:
--------------------------------------------------------------------------------
1 | # We include Corrosion inline here, but ideally in a project with
2 | # many dependencies we would need to install Corrosion on the system.
3 | # See instructions on https://github.com/AndrewGaspar/corrosion#cmake-install
4 | # Once done, uncomment this line:
5 | # find_package(Corrosion REQUIRED)
6 |
7 | include(FetchContent)
8 |
9 | FetchContent_Declare(
10 | Corrosion
11 | GIT_REPOSITORY https://github.com/AndrewGaspar/corrosion.git
12 | GIT_TAG origin/master # Optionally specify a version tag or branch here
13 | )
14 |
15 | FetchContent_MakeAvailable(Corrosion)
16 |
17 | corrosion_import_crate(MANIFEST_PATH ../native/Cargo.toml)
18 |
19 | # Flutter-specific
20 |
21 | set(CRATE_NAME "native")
22 |
23 | target_link_libraries(${BINARY_NAME} PRIVATE ${CRATE_NAME})
24 |
25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
26 |
--------------------------------------------------------------------------------
/linux/flutter/generated_plugins.cmake:
--------------------------------------------------------------------------------
1 | #
2 | # Generated file, do not edit.
3 | #
4 |
5 | list(APPEND FLUTTER_PLUGIN_LIST
6 | url_launcher_linux
7 | )
8 |
9 | list(APPEND FLUTTER_FFI_PLUGIN_LIST
10 | )
11 |
12 | set(PLUGIN_BUNDLED_LIBRARIES)
13 |
14 | foreach(plugin ${FLUTTER_PLUGIN_LIST})
15 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
16 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
19 | endforeach(plugin)
20 |
21 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
24 | endforeach(ffi_plugin)
25 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 9.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/lib/ffi.dart:
--------------------------------------------------------------------------------
1 | // This file initializes the dynamic library and connects it with the stub
2 | // generated by flutter_rust_bridge_codegen.
3 |
4 | import 'dart:ffi';
5 |
6 | import 'bridge_generated.dart';
7 |
8 | // Re-export the bridge so it is only necessary to import this file.
9 | export 'bridge_generated.dart';
10 | import 'dart:io' as io;
11 |
12 | const _base = 'native';
13 |
14 | // On MacOS, the dynamic library is not bundled with the binary,
15 | // but rather directly **linked** against the binary.
16 | final _dylib = io.Platform.isWindows ? '$_base.dll' : 'lib$_base.so';
17 |
18 | // The late modifier delays initializing the value until it is actually needed,
19 | // leaving precious little time for the program to quickly start up.
20 | late final Native native = NativeImpl(io.Platform.isIOS || io.Platform.isMacOS
21 | ? DynamicLibrary.executable()
22 | : DynamicLibrary.open(_dylib));
23 |
--------------------------------------------------------------------------------
/native/src/hitomi_client/gg.rs:
--------------------------------------------------------------------------------
1 | use serde_derive::{Deserialize, Serialize};
2 |
3 | use crate::Result;
4 |
5 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
6 | pub struct GG {
7 | pub b: String,
8 | pub m_list: Vec,
9 | pub m_result: i64,
10 | }
11 |
12 | impl GG {
13 | pub(crate) fn s(&self, hash: &str) -> Result {
14 | let len = hash.len();
15 | let s = format!(
16 | "{}{}{}",
17 | &hash[len - 1..len],
18 | &hash[len - 3..len - 2],
19 | &hash[len - 2..len - 1]
20 | );
21 | Ok(i64::from_str_radix(&s, 16)?)
22 | }
23 |
24 | pub(crate) fn m(&self, s: i64) -> i64 {
25 | if self.m_list.contains(&s) {
26 | self.m_result
27 | } else if self.m_result == 0 {
28 | 1
29 | } else {
30 | 0
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/windows/flutter/generated_plugins.cmake:
--------------------------------------------------------------------------------
1 | #
2 | # Generated file, do not edit.
3 | #
4 |
5 | list(APPEND FLUTTER_PLUGIN_LIST
6 | permission_handler_windows
7 | url_launcher_windows
8 | )
9 |
10 | list(APPEND FLUTTER_FFI_PLUGIN_LIST
11 | )
12 |
13 | set(PLUGIN_BUNDLED_LIBRARIES)
14 |
15 | foreach(plugin ${FLUTTER_PLUGIN_LIST})
16 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
17 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
20 | endforeach(plugin)
21 |
22 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
23 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
25 | endforeach(ffi_plugin)
26 |
--------------------------------------------------------------------------------
/lib/screens/components/fit_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class FitButton extends StatelessWidget {
4 | final void Function() onPressed;
5 | final String text;
6 |
7 | const FitButton({Key? key, required this.onPressed, required this.text})
8 | : super(key: key);
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | return LayoutBuilder(
13 | builder: (BuildContext context, BoxConstraints constraints) {
14 | return SizedBox(
15 | width: constraints.maxWidth,
16 | height: constraints.maxHeight,
17 | child: Container(
18 | padding: const EdgeInsets.all(10),
19 | child: MaterialButton(
20 | onPressed: onPressed,
21 | child: Center(
22 | child: Text(text),
23 | ),
24 | ),
25 | ),
26 | );
27 | },
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/native/src/database/cache/mod.rs:
--------------------------------------------------------------------------------
1 | use once_cell::sync::OnceCell;
2 | use sea_orm::{ConnectionTrait, DatabaseConnection, ExecResult, Statement};
3 | use tokio::sync::Mutex;
4 |
5 | use crate::database::connect_db;
6 |
7 | pub(crate) mod image_cache;
8 | pub(crate) mod web_cache;
9 |
10 | pub(crate) static CACHE_DATABASE: OnceCell> = OnceCell::new();
11 |
12 | pub(crate) async fn init() {
13 | let db = connect_db("cache.db").await;
14 | CACHE_DATABASE.set(Mutex::new(db)).unwrap();
15 | // init tables
16 | image_cache::init().await;
17 | web_cache::init().await;
18 | }
19 |
20 | pub(crate) async fn vacuum() -> anyhow::Result<()> {
21 | let db = CACHE_DATABASE.get().unwrap().lock().await;
22 | let backend = db.get_database_backend();
23 | let _: ExecResult = db
24 | .execute(Statement::from_string(backend, "VACUUM".to_owned()))
25 | .await?;
26 | Ok(())
27 | }
28 |
--------------------------------------------------------------------------------
/windows/runner/runner.exe.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PerMonitorV2
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: html
2 | description: A image gallery client.
3 | version: 1.0.0+1
4 |
5 | environment:
6 | sdk: ">=2.16.0 <3.0.0"
7 |
8 | dependencies:
9 | flutter:
10 | sdk: flutter
11 | flutter_localizations:
12 | sdk: flutter
13 | intl: ^0.17.0
14 |
15 | cupertino_icons: ^1.0.2
16 | flutter_rust_bridge:
17 | path: ../flutter_rust_bridge/frb_dart
18 | url_launcher: ^6.0.20
19 | flutter_styled_toast: ^2.0.0
20 | waterfall_flow: ^3.0.2
21 | permission_handler: ^9.2.0
22 | photo_view: ^0.13.0
23 | decorated_icon: ^1.2.1
24 | modal_bottom_sheet: ^2.0.1
25 | event: ^2.1.2
26 | flutter_svg: ^1.0.3
27 | another_xlider: ^1.0.1+2
28 | file_picker: ^4.5.1
29 | clipboard: ^0.1.3
30 | scrollable_positioned_list: ^0.2.3
31 |
32 | dev_dependencies:
33 | flutter_test:
34 | sdk: flutter
35 | flutter_lints: ^1.0.0
36 |
37 | flutter:
38 | generate: true
39 | uses-material-design: true
40 | assets:
41 | - lib/assets/
42 |
--------------------------------------------------------------------------------
/lib/l10n/app_zh.arb:
--------------------------------------------------------------------------------
1 | {
2 | "unsupportedPlatform":"不支持的平台",
3 | "ok":"确定",
4 | "cancel":"确定",
5 | "success":"成功",
6 | "failed":"失败",
7 | "copy":"复制",
8 | "copied":"已复制",
9 | "choose":"请选择",
10 | "previewImage":"预览图片",
11 | "saveImage": "保存图片",
12 | "tags":"标签",
13 | "nextPage": "下一页",
14 | "prePage": "上一页",
15 | "inputPageNumber": "请输入页数",
16 | "pages": "分页",
17 | "loading": "加载中",
18 | "errorTapToRefresh": "出错啦, 点击刷新",
19 | "refresh": "刷新",
20 | "connectExceptionCheckNetwork": "连接不上啦, 请检查网络",
21 | "illegalPermissions": "没有权限或路径不可用",
22 | "checkDeviceTime": "请检查设备时间",
23 | "hasBroken": "啊哦, 被玩坏了",
24 | "tapToRefresh": "点击刷新",
25 | "pullDownToRefresh": "下拉刷新",
26 | "settings": "设置",
27 | "theme": "主题",
28 | "themeDark": "主题(深色)",
29 | "enableDarkMode": "深色模式下使用不同的主题",
30 | "clearCache": "清除缓存",
31 | "about": "关于",
32 | "proxy": "代理",
33 | "inputProxy": "请输入代理",
34 | "proxyExample": " ( 例如 socks5://127.0.0.1:1080/ ) "
35 | }
36 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/windows/runner/flutter_window.h:
--------------------------------------------------------------------------------
1 | #ifndef RUNNER_FLUTTER_WINDOW_H_
2 | #define RUNNER_FLUTTER_WINDOW_H_
3 |
4 | #include
5 | #include
6 |
7 | #include
8 |
9 | #include "win32_window.h"
10 |
11 | // A window that does nothing but host a Flutter view.
12 | class FlutterWindow : public Win32Window {
13 | public:
14 | // Creates a new FlutterWindow hosting a Flutter view running |project|.
15 | explicit FlutterWindow(const flutter::DartProject& project);
16 | virtual ~FlutterWindow();
17 |
18 | protected:
19 | // Win32Window:
20 | bool OnCreate() override;
21 | void OnDestroy() override;
22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
23 | LPARAM const lparam) noexcept override;
24 |
25 | private:
26 | // The project to run.
27 | flutter::DartProject project_;
28 |
29 | // The Flutter instance hosted by this window.
30 | std::unique_ptr flutter_controller_;
31 | };
32 |
33 | #endif // RUNNER_FLUTTER_WINDOW_H_
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 |
18 | # The .vscode folder contains launch configuration and tasks you configure in
19 | # VS Code which you may wish to be included in version control, so this line
20 | # is commented out by default.
21 | #.vscode/
22 |
23 | # Flutter/Dart/Pub related
24 | **/doc/api/
25 | **/ios/Flutter/.last_build_id
26 | .dart_tool/
27 | .flutter-plugins
28 | .flutter-plugins-dependencies
29 | .packages
30 | .pub-cache/
31 | .pub/
32 | /build/
33 |
34 | # Web related
35 | lib/generated_plugin_registrant.dart
36 |
37 | # Symbolication related
38 | app.*.symbols
39 |
40 | # Obfuscation related
41 | app.*.map.json
42 |
43 | # Android Studio will place build artifacts here
44 | /android/app/debug
45 | /android/app/profile
46 | /android/app/release
47 | native/target
48 | jniLibs
49 |
50 | pubspec.lock
51 | Cargo.lock
52 | bridge_generated.*
53 | version.txt
54 | libnative.a
55 | Podfile.lock
56 | build/
57 |
--------------------------------------------------------------------------------
/lib/l10n/app_en.arb:
--------------------------------------------------------------------------------
1 | {
2 | "unsupportedPlatform":"Unsupported platform",
3 | "ok":"Ok",
4 | "cancel":"Cancel",
5 | "success":"Success",
6 | "failed":"Failed",
7 | "copy":"Copy",
8 | "copied":"Copied",
9 | "choose":"Choose",
10 | "previewImage":"Preview image",
11 | "saveImage": "Save image",
12 | "tags":"Tags",
13 | "nextPage": "Next",
14 | "prePage": "PRE",
15 | "inputPageNumber": "InputPageNumber",
16 | "pages": "Pages",
17 | "loading": "Loading",
18 | "errorTapToRefresh": "Error, tap to refresh",
19 | "refresh": "Refresh",
20 | "connectExceptionCheckNetwork": "Connect exception, check network",
21 | "illegalPermissions": "Illegal permissions",
22 | "checkDeviceTime": "Check device time",
23 | "hasBroken": "Oh, has broken",
24 | "tapToRefresh": "Tap to refresh",
25 | "pullDownToRefresh": "Pull down to refresh",
26 | "settings": "Settings",
27 | "theme": "Theme",
28 | "themeDark": "Theme (dark)",
29 | "enableDarkMode": "Another theme in dark mode",
30 | "clearCache": "Clear cache",
31 | "about": "About",
32 | "proxy": "Proxy",
33 | "inputProxy": "Input proxy",
34 | "proxyExample": " ( Like socks5://127.0.0.1:1080/ ) "
35 | }
36 |
--------------------------------------------------------------------------------
/macos/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleVersion
22 | $(FLUTTER_BUILD_NUMBER)
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | $(PRODUCT_COPYRIGHT)
27 | NSMainNibFile
28 | MainMenu
29 | NSPrincipalClass
30 | NSApplication
31 |
32 |
33 |
--------------------------------------------------------------------------------
/lib/configs/no_animation.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:html/ffi.dart';
3 |
4 | import '../commons.dart';
5 |
6 | const _propertyName = "noAnimation";
7 |
8 | late bool _noAnimation;
9 |
10 | Future initNoAnimation() async {
11 | _noAnimation = (await native.loadProperty(k: _propertyName)) == "true";
12 | }
13 |
14 | bool noAnimation() {
15 | return _noAnimation;
16 | }
17 |
18 | Future _chooseNoAnimation(BuildContext context) async {
19 | String? result = await chooseListDialog(
20 | context,
21 | title: "取消键盘或音量翻页动画",
22 | values: ["是", "否"],
23 | );
24 | if (result != null) {
25 | var target = result == "是";
26 | await native.saveProperty(k: _propertyName, v: "$target");
27 | _noAnimation = target;
28 | }
29 | }
30 |
31 | Widget noAnimationSetting() {
32 | return StatefulBuilder(
33 | builder: (BuildContext context, void Function(void Function()) setState) {
34 | return ListTile(
35 | title: const Text("取消键盘或音量翻页动画"),
36 | subtitle: Text(_noAnimation ? "是" : "否"),
37 | onTap: () async {
38 | await _chooseNoAnimation(context);
39 | setState(() {});
40 | },
41 | );
42 | },
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/lib/configs/export_rename.dart:
--------------------------------------------------------------------------------
1 | /// 自动全屏
2 | import 'package:flutter/material.dart';
3 | import 'package:html/ffi.dart';
4 |
5 | import '../commons.dart';
6 |
7 | const _propertyName = "exportRename";
8 | late bool _exportRename;
9 |
10 | Future initExportRename() async {
11 | _exportRename = (await native.loadProperty(k: _propertyName)) == "true";
12 | }
13 |
14 | bool currentExportRename() {
15 | return _exportRename;
16 | }
17 |
18 | Future _chooseExportRename(BuildContext context) async {
19 | String? result = await chooseListDialog(
20 | context,
21 | title: "导出时进行重命名",
22 | values: ["是", "否"],
23 | );
24 | if (result != null) {
25 | var target = result == "是";
26 | await native.saveProperty(k: _propertyName, v: "$target");
27 | _exportRename = target;
28 | }
29 | }
30 |
31 | Widget exportRenameSetting() {
32 | return StatefulBuilder(
33 | builder: (BuildContext context, void Function(void Function()) setState) {
34 | return ListTile(
35 | title: const Text("导出时进行重命名"),
36 | subtitle: Text(_exportRename ? "是" : "否"),
37 | onTap: () async {
38 | await _chooseExportRename(context);
39 | setState(() {});
40 | },
41 | );
42 | },
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/lib/configs/auto_full_screen.dart:
--------------------------------------------------------------------------------
1 | /// 自动全屏
2 | import 'package:flutter/material.dart';
3 | import 'package:html/ffi.dart';
4 |
5 | import '../commons.dart';
6 |
7 | const _propertyName = "autoFullScreen";
8 | late bool _autoFullScreen;
9 |
10 | Future initAutoFullScreen() async {
11 | _autoFullScreen = (await native.loadProperty(k: _propertyName)) == "true";
12 | }
13 |
14 | bool currentAutoFullScreen() {
15 | return _autoFullScreen;
16 | }
17 |
18 | Future _chooseAutoFullScreen(BuildContext context) async {
19 | String? result = await chooseListDialog(context,
20 | title: "进入阅读器自动全屏", values: ["是", "否"]);
21 | if (result != null) {
22 | var target = result == "是";
23 | await native.saveProperty(k: _propertyName, v: "$target");
24 | _autoFullScreen = target;
25 | }
26 | }
27 |
28 | Widget autoFullScreenSetting() {
29 | return StatefulBuilder(
30 | builder: (BuildContext context, void Function(void Function()) setState) {
31 | return ListTile(
32 | title: const Text("进入阅读器自动全屏"),
33 | subtitle: Text(_autoFullScreen ? "是" : "否"),
34 | onTap: () async {
35 | await _chooseAutoFullScreen(context);
36 | setState(() {});
37 | },
38 | );
39 | },
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/lib/screens/components/content_builder.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3 | import 'content_error.dart';
4 | import 'content_loading.dart';
5 |
6 | class ContentBuilder extends StatelessWidget {
7 | final Future future;
8 | final Future Function() onRefresh;
9 | final AsyncWidgetBuilder successBuilder;
10 |
11 | const ContentBuilder(
12 | {Key? key,
13 | required this.future,
14 | required this.onRefresh,
15 | required this.successBuilder})
16 | : super(key: key);
17 |
18 | @override
19 | Widget build(BuildContext context) {
20 | return FutureBuilder(
21 | future: future,
22 | builder: (BuildContext context, AsyncSnapshot snapshot) {
23 | if (snapshot.hasError) {
24 | return ContentError(
25 | error: snapshot.error,
26 | stackTrace: snapshot.stackTrace,
27 | onRefresh: onRefresh,
28 | );
29 | }
30 | if (snapshot.connectionState != ConnectionState.done) {
31 | return ContentLoading(label: AppLocalizations.of(context)!.loading);
32 | }
33 | return successBuilder(context, snapshot);
34 | },
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/screens/components/navigator.dart:
--------------------------------------------------------------------------------
1 | /// 导航相关
2 |
3 | import 'dart:async';
4 | import 'package:flutter/material.dart';
5 |
6 | // 用于监听返回到当前页面的事件
7 | // (await Navigator.push 会在子页面pushReplacement时结束阻塞)
8 | final RouteObserver> routeObserver =
9 | RouteObserver>();
10 |
11 | // 路径深度计数
12 |
13 | const _depthMax = 15;
14 | var _depth = 0;
15 |
16 | var navigatorObserver = _NavigatorObserver();
17 |
18 | class _NavigatorObserver extends NavigatorObserver {
19 | @override
20 | void didPop(Route route, Route? previousRoute) {
21 | _depth--;
22 | print("DEPTH : $_depth");
23 | super.didPop(route, previousRoute);
24 | }
25 |
26 | @override
27 | void didPush(Route route, Route? previousRoute) {
28 | _depth++;
29 | print("DEPTH : $_depth");
30 | super.didPush(route, previousRoute);
31 | }
32 | }
33 |
34 | // 路径达到一定深度的时候使用 pushReplacement
35 | Future navPushOrReplace(
36 | BuildContext context, WidgetBuilder builder) async {
37 | if (_depth < _depthMax) {
38 | return Navigator.push(
39 | context,
40 | MaterialPageRoute(builder: builder),
41 | );
42 | } else {
43 | return Navigator.pushReplacement(
44 | context,
45 | MaterialPageRoute(builder: builder),
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/screens/components/content_loading.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class ContentLoading extends StatelessWidget {
4 | final String label;
5 |
6 | const ContentLoading({Key? key, required this.label}) : super(key: key);
7 |
8 | @override
9 | Widget build(BuildContext context) {
10 | return LayoutBuilder(
11 | builder: (BuildContext context, BoxConstraints constraints) {
12 | var width = constraints.maxWidth;
13 | var height = constraints.maxHeight;
14 | var min = width < height ? width : height;
15 | var theme = Theme.of(context);
16 | return Center(
17 | child: Column(
18 | children: [
19 | Expanded(child: Container()),
20 | SizedBox(
21 | width: min / 2,
22 | height: min / 2,
23 | child: CircularProgressIndicator(
24 | color: theme.colorScheme.secondary,
25 | backgroundColor: Colors.grey[100],
26 | ),
27 | ),
28 | Container(height: min / 10),
29 | Text(label, style: TextStyle(fontSize: min / 15)),
30 | Expanded(child: Container()),
31 | ],
32 | ),
33 | );
34 | },
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/native/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "native"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [lib]
9 | crate-type = ["cdylib", "staticlib"]
10 |
11 | [dependencies]
12 | anyhow = "1.0"
13 | async_once = "0.2"
14 | base64 = "0.13"
15 | bytes = "1.1"
16 | chrono = "0.4"
17 | clipboard = "0.5.0"
18 | flutter_rust_bridge = { path = "../../flutter_rust_bridge/frb_rust" }
19 | hex = "0.4"
20 | image = { version = "0", features = ["jpeg", "gif", "webp", "bmp", "png", "jpeg_rayon"] }
21 | itertools = "0.10.3"
22 | lazy_static = "1"
23 | libc = "0.2"
24 | md5 = "0.7"
25 | once_cell = "1"
26 | prost = "0.9"
27 | prost-types = "0.9"
28 | regex = "1.5.5"
29 | reqwest = { version = "0.11", features = ["socks"] }
30 | rsa = "0.5"
31 | rust-crypto = "0"
32 | scraper = "0.13.0"
33 | sea-orm = { version = "0.6", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"], default-features = false }
34 | serde = "1.0"
35 | serde_derive = "1.0"
36 | serde_json = "1.0"
37 | serde_path_to_error = "0.1.7"
38 | tokio = { version = "1", features = ["full"] }
39 |
40 | [target.'cfg(any(target_os = "ios", target_os = "android", target_os = "macos"))'.dependencies]
41 | openssl = { version = "0.10", features = ["vendored"] }
42 |
--------------------------------------------------------------------------------
/lib/configs/volume_controller.dart:
--------------------------------------------------------------------------------
1 | /// 音量键翻页
2 | import 'dart:io';
3 |
4 | import 'package:flutter/material.dart';
5 | import 'package:html/ffi.dart';
6 |
7 | import '../commons.dart';
8 |
9 | const _propertyName = "volumeController";
10 | late bool volumeController;
11 |
12 | Future initVolumeController() async {
13 | volumeController = (await native.loadProperty(k: _propertyName)) == "true";
14 | }
15 |
16 | Future _chooseVolumeController(BuildContext context) async {
17 | String? result = await chooseListDialog(
18 | context,
19 | title: "音量键控制翻页",
20 | values: ["是", "否"],
21 | );
22 | if (result != null) {
23 | var target = result == "是";
24 | await native.saveProperty(k: _propertyName, v: "$target");
25 | volumeController = target;
26 | }
27 | }
28 |
29 | Widget volumeControllerSetting() {
30 | if (Platform.isAndroid) {
31 | return StatefulBuilder(builder:
32 | (BuildContext context, void Function(void Function()) setState) {
33 | return ListTile(
34 | title: const Text("阅读器音量键翻页"),
35 | subtitle: Text(volumeController ? "是" : "否"),
36 | onTap: () async {
37 | await _chooseVolumeController(context);
38 | setState(() {});
39 | });
40 | });
41 | }
42 | return Container();
43 | }
44 |
--------------------------------------------------------------------------------
/lib/screens/components/comic_list_builder.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:html/screens/components/comic_info_card.dart';
3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
4 | import 'comic_list.dart';
5 | import 'content_builder.dart';
6 | import 'fit_button.dart';
7 |
8 | class ComicListBuilder extends StatefulWidget {
9 | final Future> future;
10 | final Future Function() reload;
11 |
12 | const ComicListBuilder(this.future, this.reload, {Key? key})
13 | : super(key: key);
14 |
15 | @override
16 | State createState() => _ComicListBuilderState();
17 | }
18 |
19 | class _ComicListBuilderState extends State {
20 | @override
21 | Widget build(BuildContext context) {
22 | return ContentBuilder(
23 | future: widget.future,
24 | onRefresh: widget.reload,
25 | successBuilder:
26 | (BuildContext context, AsyncSnapshot> snapshot) {
27 | return RefreshIndicator(
28 | onRefresh: widget.reload,
29 | child: ComicList(
30 | snapshot.data!,
31 | appendWidget: FitButton(
32 | onPressed: widget.reload,
33 | text: AppLocalizations.of(context)!.refresh,
34 | ),
35 | ),
36 | );
37 | },
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3 | import 'package:flutter_localizations/flutter_localizations.dart';
4 | import 'package:html/configs/themes.dart';
5 | import 'package:html/screens/components/mouse_and_touch_scroll_behavior.dart';
6 | import 'package:html/screens/components/navigator.dart';
7 | import 'package:html/screens/init_screen.dart';
8 |
9 | void main() {
10 | runApp(const MyApp());
11 | }
12 |
13 | class MyApp extends StatelessWidget {
14 | const MyApp({Key? key}) : super(key: key);
15 |
16 | // This widget is the root of your application.
17 | @override
18 | Widget build(BuildContext context) {
19 | return MaterialApp(
20 | theme: currentLightThemeData(),
21 | darkTheme: currentDarkThemeData(),
22 | navigatorObservers: [
23 | routeObserver,
24 | navigatorObserver,
25 | ],
26 | debugShowCheckedModeBanner: false,
27 | scrollBehavior: mouseAndTouchScrollBehavior,
28 | localizationsDelegates: const [
29 | AppLocalizations.delegate,
30 | GlobalMaterialLocalizations.delegate,
31 | GlobalWidgetsLocalizations.delegate,
32 | GlobalCupertinoLocalizations.delegate,
33 | ],
34 | supportedLocales: AppLocalizations.supportedLocales,
35 | home: const InitScreen(),
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/configs/keyboard_controller.dart:
--------------------------------------------------------------------------------
1 | /// 上下键翻页
2 | import 'dart:io';
3 |
4 | import 'package:flutter/material.dart';
5 | import 'package:html/ffi.dart';
6 |
7 | import '../commons.dart';
8 |
9 | const _propertyName = "keyboardController";
10 |
11 | late bool keyboardController;
12 |
13 | Future initKeyboardController() async {
14 | keyboardController = (await native.loadProperty(k: _propertyName)) == "true";
15 | }
16 |
17 | Future _chooseKeyboardController(BuildContext context) async {
18 | String? result = await chooseListDialog(
19 | context,
20 | title: "键盘控制翻页",
21 | values: ["是", "否"],
22 | );
23 | if (result != null) {
24 | var target = result == "是";
25 | await native.saveProperty(k: _propertyName, v: "$target");
26 | keyboardController = target;
27 | }
28 | }
29 |
30 | Widget keyboardControllerSetting() {
31 | if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
32 | return StatefulBuilder(
33 | builder: (BuildContext context, void Function(void Function()) setState) {
34 | return ListTile(
35 | title: const Text("阅读器键盘翻页(仅PC)"),
36 | subtitle: Text(keyboardController ? "是" : "否"),
37 | onTap: () async {
38 | await _chooseKeyboardController(context);
39 | setState(() {});
40 | },
41 | );
42 | },
43 | );
44 | }
45 | return Container();
46 | }
47 |
--------------------------------------------------------------------------------
/lib/screens/comics_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:html/ffi.dart';
3 | import 'package:html/screens/components/badge.dart';
4 | import 'package:html/screens/components/comic_pager.dart';
5 | import 'package:html/screens/settings_screen.dart';
6 |
7 | class ComicsScreen extends StatefulWidget {
8 | const ComicsScreen({Key? key}) : super(key: key);
9 |
10 | @override
11 | State createState() => _ComicsScreenState();
12 | }
13 |
14 | class _ComicsScreenState extends State {
15 | Future _fetchPage(int offset, int limit) async {
16 | return native.comics(
17 | sortType: 'index',
18 | lang: 'all',
19 | offset: offset,
20 | limit: limit,
21 | );
22 | }
23 |
24 | @override
25 | Widget build(BuildContext context) {
26 | return Scaffold(
27 | appBar: AppBar(
28 | actions: [
29 | IconButton(
30 | onPressed: () {
31 | Navigator.push(
32 | context,
33 | MaterialPageRoute(builder: (BuildContext context) {
34 | return const SettingsScreen();
35 | }),
36 | );
37 | },
38 | icon: const VersionBadged(
39 | child: Icon(Icons.settings),
40 | ),
41 | ),
42 | ],
43 | ),
44 | body: ComicPager(fetchPage: _fetchPage),
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/windows/runner/main.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | #include "flutter_window.h"
6 | #include "utils.h"
7 |
8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
9 | _In_ wchar_t *command_line, _In_ int show_command) {
10 | // Attach to console when present (e.g., 'flutter run') or create a
11 | // new console when running with a debugger.
12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
13 | CreateAndAttachConsole();
14 | }
15 |
16 | // Initialize COM, so that it is available for use in the library and/or
17 | // plugins.
18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
19 |
20 | flutter::DartProject project(L"data");
21 |
22 | std::vector command_line_arguments =
23 | GetCommandLineArguments();
24 |
25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
26 |
27 | FlutterWindow window(project);
28 | Win32Window::Point origin(10, 10);
29 | Win32Window::Size size(1280, 720);
30 | if (!window.CreateAndShow(L"html", origin, size)) {
31 | return EXIT_FAILURE;
32 | }
33 | window.SetQuitOnClose(true);
34 |
35 | ::MSG msg;
36 | while (::GetMessage(&msg, nullptr, 0, 0)) {
37 | ::TranslateMessage(&msg);
38 | ::DispatchMessage(&msg);
39 | }
40 |
41 | ::CoUninitialize();
42 | return EXIT_SUCCESS;
43 | }
44 |
--------------------------------------------------------------------------------
/lib/configs/proxy.dart:
--------------------------------------------------------------------------------
1 | /// 代理设置
2 | import 'package:flutter/material.dart';
3 | import 'package:html/ffi.dart';
4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
5 | import '../commons.dart';
6 |
7 | late String _currentProxy;
8 | const String _k = "proxy";
9 |
10 | Future initProxy() async {
11 | _currentProxy = await native.loadProperty(k: _k);
12 | await native.setProxy(url: _currentProxy);
13 | }
14 |
15 | String currentProxyName() {
16 | return _currentProxy == "" ? "未设置" : _currentProxy;
17 | }
18 |
19 | Future inputProxy(BuildContext context) async {
20 | String? input = await displayTextInputDialog(
21 | context,
22 | src: _currentProxy,
23 | title: AppLocalizations.of(context)!.proxy,
24 | hint: AppLocalizations.of(context)!.inputProxy,
25 | desc: AppLocalizations.of(context)!.proxyExample,
26 | );
27 | if (input != null) {
28 | await native.setProxy(url: input);
29 | await native.saveProperty(k: _k, v: input);
30 | _currentProxy = input;
31 | }
32 | }
33 |
34 | Widget proxySetting() {
35 | return StatefulBuilder(
36 | builder: (BuildContext context, void Function(void Function()) setState) {
37 | return ListTile(
38 | title: Text(AppLocalizations.of(context)!.proxy),
39 | subtitle: Text(currentProxyName()),
40 | onTap: () async {
41 | await inputProxy(context);
42 | setState(() {});
43 | },
44 | );
45 | },
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/macos/Podfile:
--------------------------------------------------------------------------------
1 | platform :osx, '10.11'
2 |
3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
5 |
6 | project 'Runner', {
7 | 'Debug' => :debug,
8 | 'Profile' => :release,
9 | 'Release' => :release,
10 | }
11 |
12 | def flutter_root
13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
14 | unless File.exist?(generated_xcode_build_settings_path)
15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
16 | end
17 |
18 | File.foreach(generated_xcode_build_settings_path) do |line|
19 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
20 | return matches[1].strip if matches
21 | end
22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
23 | end
24 |
25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
26 |
27 | flutter_macos_podfile_setup
28 |
29 | target 'Runner' do
30 | use_frameworks!
31 | use_modular_headers!
32 |
33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
34 | end
35 |
36 | post_install do |installer|
37 | installer.pods_project.targets.each do |target|
38 | flutter_additional_macos_build_settings(target)
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def flutter_root
14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15 | unless File.exist?(generated_xcode_build_settings_path)
16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17 | end
18 |
19 | File.foreach(generated_xcode_build_settings_path) do |line|
20 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
21 | return matches[1].strip if matches
22 | end
23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24 | end
25 |
26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27 |
28 | flutter_ios_podfile_setup
29 |
30 | target 'Runner' do
31 | use_frameworks!
32 | use_modular_headers!
33 |
34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
35 | end
36 |
37 | post_install do |installer|
38 | installer.pods_project.targets.each do |target|
39 | flutter_additional_ios_build_settings(target)
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at
17 | # https://dart-lang.github.io/linter/lints/index.html.
18 | #
19 | # Instead of disabling a lint rule for the entire project in the
20 | # section below, it can also be suppressed for a single line of code
21 | # or a specific dart file by using the `// ignore: name_of_lint` and
22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
23 | # producing the lint.
24 | rules:
25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
27 |
28 | # Additional information about this file can be found at
29 | # https://dart.dev/guides/language/analysis-options
30 |
--------------------------------------------------------------------------------
/lib/configs/android_secure_flag.dart:
--------------------------------------------------------------------------------
1 | /// 音量键翻页
2 | import 'dart:io';
3 |
4 | import 'package:flutter/material.dart';
5 | import 'package:html/cross.dart';
6 | import 'package:html/ffi.dart';
7 |
8 | import '../commons.dart';
9 |
10 | const _propertyName = "androidSecureFlag";
11 |
12 | late bool _androidSecureFlag;
13 |
14 | Future initAndroidSecureFlag() async {
15 | if (Platform.isAndroid) {
16 | _androidSecureFlag =
17 | (await native.loadProperty(k: _propertyName)) == "true";
18 | if (_androidSecureFlag) {
19 | await cross.androidSecureFlag(true);
20 | }
21 | }
22 | }
23 |
24 | Future _chooseAndroidSecureFlag(BuildContext context) async {
25 | String? result = await chooseListDialog(
26 | context,
27 | title: "禁止截图/禁止显示在任务视图",
28 | values: ["是", "否"],
29 | );
30 | if (result != null) {
31 | var target = result == "是";
32 | await native.saveProperty(k: _propertyName, v: "$target");
33 | _androidSecureFlag = target;
34 | await cross.androidSecureFlag(_androidSecureFlag);
35 | }
36 | }
37 |
38 | Widget androidSecureFlagSetting() {
39 | if (Platform.isAndroid) {
40 | return StatefulBuilder(builder:
41 | (BuildContext context, void Function(void Function()) setState) {
42 | return ListTile(
43 | title: const Text("禁止截图/禁止显示在任务视图"),
44 | subtitle: Text(_androidSecureFlag ? "是" : "否"),
45 | onTap: () async {
46 | await _chooseAndroidSecureFlag(context);
47 | setState(() {});
48 | });
49 | });
50 | }
51 | return Container();
52 | }
53 |
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "app_icon_16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "app_icon_32.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "app_icon_32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "app_icon_64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "app_icon_128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "app_icon_256.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "app_icon_256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "app_icon_512.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "app_icon_512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "app_icon_1024.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/lib/configs/list_layout.dart:
--------------------------------------------------------------------------------
1 | /// 列表页的布局
2 | import 'package:event/event.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:html/ffi.dart';
5 |
6 | import '../commons.dart';
7 |
8 | enum ListLayout {
9 | infoCard,
10 | onlyImage,
11 | coverAndTitle,
12 | }
13 |
14 | const Map _listLayoutMap = {
15 | '详情': ListLayout.infoCard,
16 | '封面': ListLayout.onlyImage,
17 | '封面+标题': ListLayout.coverAndTitle,
18 | };
19 |
20 | const _propertyName = "listLayout";
21 | late ListLayout currentLayout;
22 |
23 | var listLayoutEvent = Event();
24 |
25 | Future initListLayout() async {
26 | var value = await native.loadProperty(
27 | k: _propertyName,
28 | );
29 | if (value == "") value = ListLayout.infoCard.toString();
30 | currentLayout = _listLayoutFromString(value);
31 | }
32 |
33 | ListLayout _listLayoutFromString(String layoutString) {
34 | for (var value in ListLayout.values) {
35 | if (layoutString == value.toString()) {
36 | return value;
37 | }
38 | }
39 | return ListLayout.infoCard;
40 | }
41 |
42 | void _chooseListLayout(BuildContext context) async {
43 | ListLayout? layout = await chooseMapDialog(
44 | context,
45 | values: _listLayoutMap,
46 | title: '请选择布局',
47 | );
48 | if (layout != null) {
49 | await native.saveProperty(k: _propertyName, v: layout.toString());
50 | currentLayout = layout;
51 | listLayoutEvent.broadcast();
52 | }
53 | }
54 |
55 | IconButton chooseLayoutActionButton(BuildContext context) => IconButton(
56 | onPressed: () {
57 | _chooseListLayout(context);
58 | },
59 | icon: const Icon(Icons.view_quilt),
60 | );
61 |
--------------------------------------------------------------------------------
/lib/configs/time_offset_hour.dart:
--------------------------------------------------------------------------------
1 | /// 时区设置
2 | import 'package:flutter/material.dart';
3 | import 'package:html/ffi.dart';
4 |
5 | import '../commons.dart';
6 |
7 | const _propertyName = "timeOffsetHour";
8 | int _timeOffsetHour = 8;
9 |
10 | Future initTimeZone() async {
11 | var value = await native.loadProperty(k: _propertyName);
12 | if (value == "") value = "8";
13 | _timeOffsetHour = int.parse(value);
14 | }
15 |
16 | int currentTimeOffsetHour() {
17 | return _timeOffsetHour;
18 | }
19 |
20 | Future _chooseTimeZone(BuildContext context) async {
21 | List timeZones = [];
22 | for (var i = -12; i <= 12; i++) {
23 | var str = i.toString();
24 | if (!str.startsWith("-")) {
25 | str = "+" + str;
26 | }
27 | timeZones.add(str);
28 | }
29 | String? result = await chooseListDialog(
30 | context,
31 | title: "时区选择",
32 | values: timeZones,
33 | );
34 | if (result != null) {
35 | if (result.startsWith("+")) {
36 | result = result.substring(1);
37 | }
38 | _timeOffsetHour = int.parse(result);
39 | await native.saveProperty(k: _propertyName, v: result);
40 | }
41 | }
42 |
43 | Widget timeZoneSetting() {
44 | return StatefulBuilder(
45 | builder: (BuildContext context, void Function(void Function()) setState) {
46 | var c = "$_timeOffsetHour";
47 | if (!c.startsWith("-")) {
48 | c = "+" + c;
49 | }
50 | return ListTile(
51 | title: const Text("时区"),
52 | subtitle: Text(c),
53 | onTap: () async {
54 | await _chooseTimeZone(context);
55 | setState(() {});
56 | },
57 | );
58 | },
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/lib/configs/android_display_mode.dart:
--------------------------------------------------------------------------------
1 | /// 显示模式, 仅安卓有效
2 | import 'dart:io';
3 |
4 | import 'package:flutter/material.dart';
5 | import 'package:html/cross.dart';
6 | import 'package:html/ffi.dart';
7 |
8 | import '../commons.dart';
9 |
10 | const _propertyName = "androidDisplayMode";
11 | List _modes = [];
12 | String _androidDisplayMode = "";
13 |
14 | Future initAndroidDisplayMode() async {
15 | if (Platform.isAndroid) {
16 | _androidDisplayMode = await native.loadProperty(k: _propertyName);
17 | _modes = await cross.loadAndroidModes();
18 | await _changeMode();
19 | }
20 | }
21 |
22 | Future _changeMode() async {
23 | await cross.setAndroidMode(_androidDisplayMode);
24 | }
25 |
26 | Future _chooseAndroidDisplayMode(BuildContext context) async {
27 | if (Platform.isAndroid) {
28 | List list = [""];
29 | list.addAll(_modes);
30 | String? result = await chooseListDialog(
31 | context,
32 | title: "安卓屏幕刷新率",
33 | values: list,
34 | );
35 | if (result != null) {
36 | await native.saveProperty(k: _propertyName, v: result);
37 | _androidDisplayMode = result;
38 | await _changeMode();
39 | }
40 | }
41 | }
42 |
43 | Widget androidDisplayModeSetting() {
44 | if (Platform.isAndroid) {
45 | return StatefulBuilder(
46 | builder: (BuildContext context, void Function(void Function()) setState) {
47 | return ListTile(
48 | title: const Text("屏幕刷新率(安卓)"),
49 | subtitle: Text(_androidDisplayMode),
50 | onTap: () async {
51 | await _chooseAndroidDisplayMode(context);
52 | setState(() {});
53 | },
54 | );
55 | },
56 | );
57 | }
58 | return Container();
59 | }
60 |
--------------------------------------------------------------------------------
/native/src/database/properties/property.rs:
--------------------------------------------------------------------------------
1 | use std::ops::Deref;
2 |
3 | use sea_orm::entity::prelude::*;
4 | use sea_orm::IntoActiveModel;
5 | use sea_orm::{EntityTrait, Set};
6 |
7 | use crate::database::properties::PROPERTIES_DATABASE;
8 | use crate::database::{create_index, create_table_if_not_exists, index_exists};
9 |
10 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
11 | #[sea_orm(table_name = "property")]
12 | pub struct Model {
13 | #[sea_orm(primary_key, auto_increment = false)]
14 | pub k: String,
15 | pub v: String,
16 | }
17 |
18 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
19 | pub enum Relation {}
20 |
21 | impl ActiveModelBehavior for ActiveModel {}
22 |
23 | pub(crate) async fn init() {
24 | let db = PROPERTIES_DATABASE.get().unwrap().lock().await;
25 | create_table_if_not_exists(db.deref(), Entity).await;
26 | if !index_exists(db.deref(), "property", "property_idx_k").await {
27 | create_index(db.deref(), "property", vec!["k"], "property_idx_k").await;
28 | }
29 | }
30 |
31 | pub async fn save_property(k: String, v: String) -> anyhow::Result<()> {
32 | let db = PROPERTIES_DATABASE.get().unwrap().lock().await;
33 | if let Some(in_db) = Entity::find_by_id(k.clone()).one(db.deref()).await? {
34 | let mut in_db = in_db.into_active_model();
35 | in_db.v = Set(v);
36 | in_db.update(db.deref()).await?;
37 | } else {
38 | Model { k, v }
39 | .into_active_model()
40 | .insert(db.deref())
41 | .await?;
42 | }
43 | Ok(())
44 | }
45 |
46 | pub async fn load_property(k: String) -> anyhow::Result {
47 | let in_db = Entity::find_by_id(k)
48 | .one(PROPERTIES_DATABASE.get().unwrap().lock().await.deref())
49 | .await?;
50 | Ok(if let Some(in_db) = in_db {
51 | in_db.v
52 | } else {
53 | "".to_owned()
54 | })
55 | }
56 |
--------------------------------------------------------------------------------
/windows/runner/utils.cpp:
--------------------------------------------------------------------------------
1 | #include "utils.h"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | #include
9 |
10 | void CreateAndAttachConsole() {
11 | if (::AllocConsole()) {
12 | FILE *unused;
13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
14 | _dup2(_fileno(stdout), 1);
15 | }
16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
17 | _dup2(_fileno(stdout), 2);
18 | }
19 | std::ios::sync_with_stdio();
20 | FlutterDesktopResyncOutputStreams();
21 | }
22 | }
23 |
24 | std::vector GetCommandLineArguments() {
25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
26 | int argc;
27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
28 | if (argv == nullptr) {
29 | return std::vector();
30 | }
31 |
32 | std::vector command_line_arguments;
33 |
34 | // Skip the first argument as it's the binary name.
35 | for (int i = 1; i < argc; i++) {
36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
37 | }
38 |
39 | ::LocalFree(argv);
40 |
41 | return command_line_arguments;
42 | }
43 |
44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) {
45 | if (utf16_string == nullptr) {
46 | return std::string();
47 | }
48 | int target_length = ::WideCharToMultiByte(
49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
50 | -1, nullptr, 0, nullptr, nullptr);
51 | if (target_length == 0) {
52 | return std::string();
53 | }
54 | std::string utf8_string;
55 | utf8_string.resize(target_length);
56 | int converted_length = ::WideCharToMultiByte(
57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
58 | -1, utf8_string.data(),
59 | target_length, nullptr, nullptr);
60 | if (converted_length == 0) {
61 | return std::string();
62 | }
63 | return utf8_string;
64 | }
65 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPhotoLibraryUsageDescription
6 | Usage images
7 | NSPhotoLibraryAddUsageDescription
8 | Save images
9 | CFBundleDevelopmentRegion
10 | $(DEVELOPMENT_LANGUAGE)
11 | CFBundleDisplayName
12 | Flutter Rust Bridge Template
13 | CFBundleExecutable
14 | $(EXECUTABLE_NAME)
15 | CFBundleIdentifier
16 | $(PRODUCT_BUNDLE_IDENTIFIER)
17 | CFBundleInfoDictionaryVersion
18 | 6.0
19 | CFBundleName
20 | html
21 | CFBundlePackageType
22 | APPL
23 | CFBundleShortVersionString
24 | $(FLUTTER_BUILD_NAME)
25 | CFBundleSignature
26 | ????
27 | CFBundleVersion
28 | $(FLUTTER_BUILD_NUMBER)
29 | LSRequiresIPhoneOS
30 |
31 | UILaunchStoryboardName
32 | LaunchScreen
33 | UIMainStoryboardFile
34 | Main
35 | UISupportedInterfaceOrientations
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationLandscapeLeft
39 | UIInterfaceOrientationLandscapeRight
40 |
41 | UISupportedInterfaceOrientations~ipad
42 |
43 | UIInterfaceOrientationPortrait
44 | UIInterfaceOrientationPortraitUpsideDown
45 | UIInterfaceOrientationLandscapeLeft
46 | UIInterfaceOrientationLandscapeRight
47 |
48 | UIViewControllerBasedStatusBarAppearance
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/lib/configs/pager_action.dart:
--------------------------------------------------------------------------------
1 | /// 列表页下一页的行为
2 | import 'package:flutter/material.dart';
3 |
4 | import '../commons.dart';
5 | import '../ffi.dart';
6 |
7 | enum PagerAction {
8 | controller,
9 | stream,
10 | }
11 |
12 | Map _pagerActionMap = {
13 | "使用按钮": PagerAction.controller,
14 | "瀑布流": PagerAction.stream,
15 | };
16 |
17 | const _propertyName = "pagerAction";
18 | late PagerAction _pagerAction;
19 |
20 | Future initPagerAction() async {
21 | var value = await native.loadProperty(k: _propertyName);
22 | if (value == "") value = PagerAction.controller.toString();
23 | _pagerAction = _pagerActionFromString(value);
24 | }
25 |
26 | PagerAction currentPagerAction() {
27 | return _pagerAction;
28 | }
29 |
30 | PagerAction _pagerActionFromString(String string) {
31 | for (var value in PagerAction.values) {
32 | if (string == value.toString()) {
33 | return value;
34 | }
35 | }
36 | return PagerAction.controller;
37 | }
38 |
39 | String _currentPagerActionName() {
40 | for (var e in _pagerActionMap.entries) {
41 | if (e.value == _pagerAction) {
42 | return e.key;
43 | }
44 | }
45 | return '';
46 | }
47 |
48 | Future _choosePagerAction(BuildContext context) async {
49 | PagerAction? result = await chooseMapDialog(
50 | context,
51 | values: _pagerActionMap,
52 | title: "选择列表页加载方式",
53 | );
54 | if (result != null) {
55 | await native.saveProperty(k: _propertyName, v: result.toString());
56 | _pagerAction = result;
57 | }
58 | }
59 |
60 | Widget pagerActionSetting() {
61 | return StatefulBuilder(
62 | builder: (BuildContext context, void Function(void Function()) setState) {
63 | return ListTile(
64 | title: const Text("列表页加载方式"),
65 | subtitle: Text(_currentPagerActionName()),
66 | onTap: () async {
67 | await _choosePagerAction(context);
68 | setState(() {});
69 | },
70 | );
71 | },
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/windows/runner/flutter_window.cpp:
--------------------------------------------------------------------------------
1 | #include "flutter_window.h"
2 |
3 | #include
4 |
5 | #include "flutter/generated_plugin_registrant.h"
6 |
7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project)
8 | : project_(project) {}
9 |
10 | FlutterWindow::~FlutterWindow() {}
11 |
12 | bool FlutterWindow::OnCreate() {
13 | if (!Win32Window::OnCreate()) {
14 | return false;
15 | }
16 |
17 | RECT frame = GetClientArea();
18 |
19 | // The size here must match the window dimensions to avoid unnecessary surface
20 | // creation / destruction in the startup path.
21 | flutter_controller_ = std::make_unique(
22 | frame.right - frame.left, frame.bottom - frame.top, project_);
23 | // Ensure that basic setup of the controller was successful.
24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) {
25 | return false;
26 | }
27 | RegisterPlugins(flutter_controller_->engine());
28 | SetChildContent(flutter_controller_->view()->GetNativeWindow());
29 | return true;
30 | }
31 |
32 | void FlutterWindow::OnDestroy() {
33 | if (flutter_controller_) {
34 | flutter_controller_ = nullptr;
35 | }
36 |
37 | Win32Window::OnDestroy();
38 | }
39 |
40 | LRESULT
41 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
42 | WPARAM const wparam,
43 | LPARAM const lparam) noexcept {
44 | // Give Flutter, including plugins, an opportunity to handle window messages.
45 | if (flutter_controller_) {
46 | std::optional result =
47 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
48 | lparam);
49 | if (result) {
50 | return *result;
51 | }
52 | }
53 |
54 | switch (message) {
55 | case WM_FONTCHANGE:
56 | flutter_controller_->engine()->ReloadSystemFonts();
57 | break;
58 | }
59 |
60 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
61 | }
62 |
--------------------------------------------------------------------------------
/lib/configs/auto_clean.dart:
--------------------------------------------------------------------------------
1 | /// 自动清理
2 | import 'package:flutter/material.dart';
3 | import 'package:html/ffi.dart';
4 |
5 | const _autoCleanMap = {
6 | "一个月前": "${1000 * 3600 * 24 * 30}",
7 | "一周前": "${1000 * 3600 * 24 * 7}",
8 | "一天前": "${1000 * 3600 * 24 * 1}",
9 | "不自动清理": "${0}",
10 | };
11 | late String _autoCleanSec;
12 |
13 | Future initAutoClean() async {
14 | _autoCleanSec = await native.loadProperty(k: "autoCleanSec");
15 | if (_autoCleanSec == "") {
16 | _autoCleanSec = "${3600 * 24 * 30}";
17 | }
18 | if ("0" != _autoCleanSec) {
19 | await native.autoClean(time: int.parse(_autoCleanSec));
20 | }
21 | }
22 |
23 | String _currentAutoCleanSec() {
24 | for (var value in _autoCleanMap.entries) {
25 | if (value.value == _autoCleanSec) {
26 | return value.key;
27 | }
28 | }
29 | return "";
30 | }
31 |
32 | Future _chooseAutoCleanSec(BuildContext context) async {
33 | String? choose = await showDialog(
34 | context: context,
35 | builder: (BuildContext context) {
36 | return SimpleDialog(
37 | title: const Text('选择自动清理周期'),
38 | children: [
39 | ..._autoCleanMap.entries.map(
40 | (e) => SimpleDialogOption(
41 | child: Text(e.key),
42 | onPressed: () {
43 | Navigator.of(context).pop(e.value);
44 | },
45 | ),
46 | ),
47 | ],
48 | );
49 | },
50 | );
51 | if (choose != null) {
52 | await native.saveProperty(k: "autoCleanSec", v: choose);
53 | _autoCleanSec = choose;
54 | }
55 | }
56 |
57 | Widget autoCleanSecSetting() {
58 | return StatefulBuilder(
59 | builder: (BuildContext context, void Function(void Function()) setState) {
60 | return ListTile(
61 | title: const Text("自动清理缓存"),
62 | subtitle: Text(_currentAutoCleanSec()),
63 | onTap: () async {
64 | await _chooseAutoCleanSec(context);
65 | setState(() {});
66 | },
67 | );
68 | },
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/lib/configs/reader_slider_position.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:html/ffi.dart';
3 |
4 | import '../commons.dart';
5 |
6 | enum ReaderSliderPosition { bottom, right, left }
7 |
8 | const _positionNames = {
9 | ReaderSliderPosition.bottom: '下方',
10 | ReaderSliderPosition.right: '右侧',
11 | ReaderSliderPosition.left: '左侧',
12 | };
13 |
14 | const _propertyName = "readerSliderPosition";
15 | late ReaderSliderPosition _readerSliderPosition;
16 |
17 | Future initReaderSliderPosition() async {
18 | _readerSliderPosition = _readerSliderPositionFromString(
19 | await native.loadProperty(k: _propertyName),
20 | );
21 | }
22 |
23 | ReaderSliderPosition _readerSliderPositionFromString(String str) {
24 | for (var value in ReaderSliderPosition.values) {
25 | if (str == value.toString()) return value;
26 | }
27 | return ReaderSliderPosition.bottom;
28 | }
29 |
30 | ReaderSliderPosition get currentReaderSliderPosition => _readerSliderPosition;
31 |
32 | String currentReaderSliderPositionName() =>
33 | _positionNames[_readerSliderPosition] ?? "";
34 |
35 | Future chooseReaderSliderPosition(BuildContext context) async {
36 | Map map = {};
37 | _positionNames.forEach((key, value) {
38 | map[value] = key;
39 | });
40 | ReaderSliderPosition? result = await chooseMapDialog(
41 | context,
42 | values: map,
43 | title: "选择滑动条位置");
44 | if (result != null) {
45 | await native.saveProperty(k: _propertyName, v: result.toString());
46 | _readerSliderPosition = result;
47 | }
48 | }
49 |
50 | Widget readerSliderPositionSetting() {
51 | return StatefulBuilder(
52 | builder: (BuildContext context, void Function(void Function()) setState) {
53 | return ListTile(
54 | title: const Text("滚动条的位置"),
55 | subtitle: Text(currentReaderSliderPositionName()),
56 | onTap: () async {
57 | await chooseReaderSliderPosition(context);
58 | setState(() {});
59 | },
60 | );
61 | },
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/lib/screens/theme_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import '../configs/themes.dart';
3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
4 |
5 | class ThemeScreen extends StatefulWidget {
6 | const ThemeScreen({Key? key}) : super(key: key);
7 |
8 | @override
9 | State createState() => _ThemeScreenState();
10 | }
11 |
12 | class _ThemeScreenState extends State {
13 | @override
14 | Widget build(BuildContext context) {
15 | return Scaffold(
16 | appBar: AppBar(title: Text(AppLocalizations.of(context)!.theme)),
17 | body: ListView(
18 | children: [
19 | const Divider(),
20 | ListTile(
21 | onTap: () async {
22 | await chooseLightTheme(context);
23 | setState(() {});
24 | },
25 | title: Text(AppLocalizations.of(context)!.theme),
26 | subtitle: Text(currentLightThemeName()),
27 | ),
28 | const Divider(),
29 | ...androidNightModeDisplay
30 | ? [
31 | SwitchListTile(
32 | title: Text(AppLocalizations.of(context)!.enableDarkMode),
33 | value: androidNightMode,
34 | onChanged: (value) async {
35 | await setAndroidNightMode(value);
36 | setState(() {});
37 | }),
38 | ]
39 | : [],
40 | const Divider(),
41 | ...androidNightModeDisplay && androidNightMode
42 | ? [
43 | ListTile(
44 | onTap: () async {
45 | await chooseDarkTheme(context);
46 | setState(() {});
47 | },
48 | title: Text(AppLocalizations.of(context)!.themeDark),
49 | subtitle: Text(currentDarkThemeName()),
50 | ),
51 | ]
52 | : [],
53 | const Divider(),
54 | ],
55 | ),
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/lib/screens/file_photo_view_screen.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:html/cross.dart';
5 | import 'package:photo_view/photo_view.dart';
6 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
7 | import '../commons.dart';
8 |
9 | // 预览图片
10 | class FilePhotoViewScreen extends StatelessWidget {
11 | final String filePath;
12 |
13 | const FilePhotoViewScreen(this.filePath, {Key? key}) : super(key: key);
14 |
15 | @override
16 | Widget build(BuildContext context) => Scaffold(
17 | body: Stack(
18 | children: [
19 | GestureDetector(
20 | onLongPress: () async {
21 | int? choose = await chooseMapDialog(
22 | context,
23 | title: AppLocalizations.of(context)!.choose,
24 | values: {
25 | AppLocalizations.of(context)!.saveImage: 1
26 | },
27 | );
28 | switch (choose) {
29 | case 1:
30 | cross.saveImageFileToGallery(filePath, context);
31 | break;
32 | }
33 | },
34 | child: PhotoView(
35 | imageProvider: FileImage(File(filePath)),
36 | ),
37 | ),
38 | InkWell(
39 | onTap: () => Navigator.of(context).pop(),
40 | child: Container(
41 | margin: const EdgeInsets.only(top: 30),
42 | padding: const EdgeInsets.only(left: 4, right: 4),
43 | decoration: BoxDecoration(
44 | color: Colors.black.withOpacity(.75),
45 | borderRadius: const BorderRadius.only(
46 | topRight: Radius.circular(8),
47 | bottomRight: Radius.circular(8),
48 | ),
49 | ),
50 | child:
51 | const Icon(Icons.keyboard_backspace, color: Colors.white),
52 | ),
53 | ),
54 | ],
55 | ),
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/lib/screens/components/comic_tags_card.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import 'navigator.dart';
4 |
5 | // 漫画tag
6 | class ComicTagsCard extends StatelessWidget {
7 | final List tags;
8 |
9 | const ComicTagsCard(this.tags, {Key? key}) : super(key: key);
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | var theme = Theme.of(context);
14 | return Container(
15 | padding: const EdgeInsets.only(top: 5, bottom: 5),
16 | decoration: BoxDecoration(
17 | border: Border(
18 | bottom: BorderSide(
19 | color: theme.dividerColor,
20 | ),
21 | ),
22 | ),
23 | child: Wrap(
24 | children: tags.map((e) {
25 | return InkWell(
26 | onTap: () {
27 | // todo
28 | // navPushOrReplace(context, (context) => ComicsScreen(tag: e));
29 | },
30 | child: Container(
31 | padding: const EdgeInsets.only(
32 | left: 10,
33 | right: 10,
34 | top: 3,
35 | bottom: 3,
36 | ),
37 | margin: const EdgeInsets.only(
38 | left: 5,
39 | right: 5,
40 | top: 3,
41 | bottom: 3,
42 | ),
43 | decoration: BoxDecoration(
44 | color: Colors.pink.shade100,
45 | border: Border.all(
46 | style: BorderStyle.solid,
47 | color: Colors.pink.shade400,
48 | ),
49 | borderRadius: const BorderRadius.all(Radius.circular(30)),
50 | ),
51 | child: Text(
52 | e,
53 | style: TextStyle(
54 | color: Colors.pink.shade500,
55 | height: 1.4,
56 | ),
57 | strutStyle: const StrutStyle(
58 | height: 1.4,
59 | ),
60 | ),
61 | ),
62 | );
63 | }).toList(),
64 | ),
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/lib/configs/version.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async' show Future;
2 | import 'dart:convert';
3 |
4 | import 'package:event/event.dart';
5 | import 'package:flutter/material.dart';
6 | import 'package:flutter/services.dart' show rootBundle;
7 | import 'package:html/ffi.dart';
8 |
9 | import '../commons.dart';
10 |
11 | const _versionUrl =
12 | "https://api.github.com/repos/niuhuan/html-comic/releases/latest";
13 | const _versionAssets = 'lib/assets/version.txt';
14 |
15 | late String _version;
16 | String? _latestVersion;
17 | String? _latestVersionInfo;
18 |
19 | Future initVersion() async {
20 | // 当前版本
21 | try {
22 | _version = (await rootBundle.loadString(_versionAssets)).trim();
23 | } catch (e) {
24 | _version = "dirty";
25 | }
26 | }
27 |
28 | var versionEvent = Event();
29 |
30 | String currentVersion() {
31 | return _version;
32 | }
33 |
34 | String? latestVersion() {
35 | if (_latestVersion == _version) {
36 | return null;
37 | }
38 | return _latestVersion;
39 | }
40 |
41 | String? latestVersionInfo() {
42 | if (_latestVersion == _version) {
43 | return null;
44 | }
45 | return _latestVersionInfo;
46 | }
47 |
48 | Future autoCheckNewVersion() {
49 | return _versionCheck();
50 | }
51 |
52 | Future manualCheckNewVersion(BuildContext context) async {
53 | try {
54 | defaultToast(context, "检查更新中");
55 | await _versionCheck();
56 | defaultToast(context, "检查更新成功");
57 | } catch (e) {
58 | defaultToast(context, "检查更新失败 : $e");
59 | }
60 | }
61 |
62 | bool dirtyVersion() {
63 | return "dirty" == _version;
64 | }
65 |
66 | // maybe exception
67 | Future _versionCheck() async {
68 | if (!dirtyVersion()) {
69 | // 检查更新只能使用defaultHttpClient, 而不能使用pika的client, 否则会 "tls handshake failure"
70 | var json = jsonDecode(await native.httpGet(url: _versionUrl));
71 | if (json["name"] != null) {
72 | String latestVersion = (json["name"]);
73 | if (latestVersion != _version) {
74 | _latestVersion = latestVersion;
75 | _latestVersionInfo = json["body"] ?? "";
76 | }
77 | }
78 | } // else dirtyVersion
79 | versionEvent.broadcast();
80 | }
81 |
--------------------------------------------------------------------------------
/lib/screens/components/badge.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../../configs/version.dart';
4 |
5 | // 提示信息, 组件右上角的小红点
6 | class Badged extends StatelessWidget {
7 | final String? badge;
8 | final Widget child;
9 |
10 | const Badged({Key? key, required this.child, this.badge}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | if (badge == null) {
15 | return child;
16 | }
17 | return Stack(
18 | children: [
19 | child,
20 | Positioned(
21 | right: 0,
22 | child: Container(
23 | padding: const EdgeInsets.all(1),
24 | decoration: BoxDecoration(
25 | color: Colors.red,
26 | borderRadius: BorderRadius.circular(6),
27 | ),
28 | constraints: const BoxConstraints(
29 | minWidth: 12,
30 | minHeight: 12,
31 | ),
32 | child: Text(
33 | badge!,
34 | style: const TextStyle(
35 | color: Colors.white,
36 | fontSize: 8,
37 | ),
38 | textAlign: TextAlign.center,
39 | ),
40 | ),
41 | ),
42 | ],
43 | );
44 | }
45 | }
46 |
47 | class VersionBadged extends StatefulWidget {
48 | final Widget child;
49 |
50 | const VersionBadged({required this.child, Key? key}) : super(key: key);
51 |
52 | @override
53 | State createState() => _VersionBadgedState();
54 | }
55 |
56 | class _VersionBadgedState extends State {
57 | @override
58 | void initState() {
59 | versionEvent.subscribe(_onVersion);
60 | super.initState();
61 | }
62 |
63 | @override
64 | void dispose() {
65 | versionEvent.unsubscribe(_onVersion);
66 | super.dispose();
67 | }
68 |
69 | void _onVersion(dynamic a) {
70 | setState(() {});
71 | }
72 |
73 | @override
74 | Widget build(BuildContext context) {
75 | return Badged(
76 | child: widget.child,
77 | badge:
78 | currentVersion() == 'dirty' || latestVersion() != null ? "1" : null,
79 | );
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/native/src/lib.rs:
--------------------------------------------------------------------------------
1 | use std::sync::{Arc, Mutex};
2 |
3 | use anyhow::Result;
4 | use lazy_static::lazy_static;
5 | use once_cell::sync::OnceCell;
6 | use tokio::runtime;
7 | use tokio::sync::RwLock;
8 |
9 | use hitomi_client::Client;
10 | use local::join_paths;
11 |
12 | use crate::database::init_database;
13 | use crate::local::create_dir_if_not_exists;
14 |
15 | mod api;
16 | mod bridge_generated;
17 | mod database;
18 | pub mod hitomi_client;
19 |
20 | mod local;
21 | mod utils;
22 |
23 | #[cfg(test)]
24 | mod tests;
25 |
26 | lazy_static! {
27 | pub(crate) static ref RUNTIME: runtime::Runtime = runtime::Builder::new_multi_thread()
28 | .enable_all()
29 | .thread_keep_alive(tokio::time::Duration::new(60, 0))
30 | .worker_threads(30)
31 | .max_blocking_threads(30)
32 | .build()
33 | .unwrap();
34 | pub(crate) static ref CLIENT: Arc> = Arc::new(RwLock::new(Client::new()));
35 | static ref INIT_ED: Mutex = Mutex::new(false);
36 | }
37 |
38 | static ROOT: OnceCell = OnceCell::new();
39 | static IMAGE_CACHE_DIR: OnceCell = OnceCell::new();
40 | static DATABASE_DIR: OnceCell = OnceCell::new();
41 |
42 | pub fn init_root(path: &str) {
43 | let mut lock = INIT_ED.lock().unwrap();
44 | if *lock {
45 | return;
46 | }
47 | *lock = true;
48 | println!("Init application with root : {}", path);
49 | ROOT.set(path.to_owned()).unwrap();
50 | IMAGE_CACHE_DIR
51 | .set(join_paths(vec![path, "image_cache"]))
52 | .unwrap();
53 | DATABASE_DIR
54 | .set(join_paths(vec![path, "database"]))
55 | .unwrap();
56 | create_dir_if_not_exists(ROOT.get().unwrap());
57 | create_dir_if_not_exists(IMAGE_CACHE_DIR.get().unwrap());
58 | create_dir_if_not_exists(DATABASE_DIR.get().unwrap());
59 | RUNTIME.block_on(init_database());
60 | }
61 |
62 | #[allow(dead_code)]
63 | pub(crate) fn get_root() -> &'static String {
64 | ROOT.get().unwrap()
65 | }
66 |
67 | pub(crate) fn get_image_cache_dir() -> &'static String {
68 | IMAGE_CACHE_DIR.get().unwrap()
69 | }
70 |
71 | pub(crate) fn get_database_dir() -> &'static String {
72 | DATABASE_DIR.get().unwrap()
73 | }
74 |
--------------------------------------------------------------------------------
/lib/screens/components/item_builder.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | // 非全屏FutureBuilder封装
4 | class ItemBuilder extends StatelessWidget {
5 | final Future future;
6 | final AsyncWidgetBuilder successBuilder;
7 | final Future Function() onRefresh;
8 | final double? loadingHeight;
9 | final double? height;
10 |
11 | const ItemBuilder({
12 | Key? key,
13 | required this.future,
14 | required this.successBuilder,
15 | required this.onRefresh,
16 | this.height,
17 | this.loadingHeight,
18 | }) : super(key: key);
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | return LayoutBuilder(
23 | builder: (BuildContext context, BoxConstraints constraints) {
24 | var _maxWidth = constraints.maxWidth;
25 | var _loadingHeight = height ?? loadingHeight ?? _maxWidth / 2;
26 | return FutureBuilder(
27 | future: future,
28 | builder: (BuildContext context, AsyncSnapshot snapshot) {
29 | if (snapshot.hasError) {
30 | print("${snapshot.error}");
31 | print("${snapshot.stackTrace}");
32 | return InkWell(
33 | onTap: onRefresh,
34 | child: SizedBox(
35 | width: _maxWidth,
36 | height: _loadingHeight,
37 | child: Center(
38 | child:
39 | Icon(Icons.sync_problem, size: _loadingHeight / 1.5),
40 | ),
41 | ),
42 | );
43 | }
44 | if (snapshot.connectionState != ConnectionState.done) {
45 | return SizedBox(
46 | width: _maxWidth,
47 | height: _loadingHeight,
48 | child: Center(
49 | child: Icon(Icons.sync, size: _loadingHeight / 1.5),
50 | ),
51 | );
52 | }
53 | return SizedBox(
54 | width: _maxWidth,
55 | height: height,
56 | child: successBuilder(context, snapshot),
57 | );
58 | });
59 | },
60 | );
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/lib/configs/content_failed_reload_action.dart:
--------------------------------------------------------------------------------
1 | /// 全屏操作
2 | import 'package:flutter/material.dart';
3 | import 'package:html/ffi.dart';
4 |
5 | import '../commons.dart';
6 |
7 | enum ContentFailedReloadAction {
8 | pullDown,
9 | touchLoader,
10 | }
11 |
12 | const _propertyName = "contentFailedReloadAction";
13 | late ContentFailedReloadAction contentFailedReloadAction;
14 |
15 | Future initContentFailedReloadAction() async {
16 | var value = await native.loadProperty(k: _propertyName);
17 | if (value == "") {
18 | value = ContentFailedReloadAction.pullDown.toString();
19 | }
20 | contentFailedReloadAction = _contentFailedReloadActionFromString(value);
21 | }
22 |
23 | ContentFailedReloadAction _contentFailedReloadActionFromString(String string) {
24 | for (var value in ContentFailedReloadAction.values) {
25 | if (string == value.toString()) {
26 | return value;
27 | }
28 | }
29 | return ContentFailedReloadAction.pullDown;
30 | }
31 |
32 | Map _contentFailedReloadActionMap = {
33 | "下拉刷新": ContentFailedReloadAction.pullDown,
34 | "点击屏幕刷新": ContentFailedReloadAction.touchLoader,
35 | };
36 |
37 | String _currentContentFailedReloadActionName() {
38 | for (var e in _contentFailedReloadActionMap.entries) {
39 | if (e.value == contentFailedReloadAction) {
40 | return e.key;
41 | }
42 | }
43 | return '';
44 | }
45 |
46 | Future _chooseContentFailedReloadAction(BuildContext context) async {
47 | ContentFailedReloadAction? result =
48 | await chooseMapDialog(
49 | context,
50 | values: _contentFailedReloadActionMap,
51 | title: "选择页面加载失败刷新的方式",
52 | );
53 | if (result != null) {
54 | await native.saveProperty(k: _propertyName, v: result.toString());
55 | contentFailedReloadAction = result;
56 | }
57 | }
58 |
59 | Widget contentFailedReloadActionSetting() {
60 | return StatefulBuilder(
61 | builder: (BuildContext context, void Function(void Function()) setState) {
62 | return ListTile(
63 | title: const Text("加载失败时"),
64 | subtitle: Text(_currentContentFailedReloadActionName()),
65 | onTap: () async {
66 | await _chooseContentFailedReloadAction(context);
67 | setState(() {});
68 | },
69 | );
70 | },
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/lib/configs/full_screen_action.dart:
--------------------------------------------------------------------------------
1 | /// 全屏操作
2 | import 'package:flutter/material.dart';
3 | import 'package:html/ffi.dart';
4 | import '../commons.dart';
5 |
6 | enum FullScreenAction {
7 | touchOnce,
8 | controller,
9 | touchDouble,
10 | touchDoubleOnceNext,
11 | threeArea,
12 | }
13 |
14 | Map _fullScreenActionMap = {
15 | "点击屏幕一次全屏": FullScreenAction.touchOnce,
16 | "使用控制器全屏": FullScreenAction.controller,
17 | "双击屏幕全屏": FullScreenAction.touchDouble,
18 | "双击屏幕全屏 + 单击屏幕下一页": FullScreenAction.touchDoubleOnceNext,
19 | "将屏幕划分成三个区域 (上一页, 下一页, 全屏)": FullScreenAction.threeArea,
20 | };
21 |
22 | const _defaultController = FullScreenAction.touchOnce;
23 | const _propertyName = "fullScreenAction";
24 | late FullScreenAction _fullScreenAction;
25 |
26 | Future initFullScreenAction() async {
27 | var value = await native.loadProperty(k: _propertyName);
28 | if (value == "") value = FullScreenAction.touchOnce.toString();
29 | _fullScreenAction = _fullScreenActionFromString(value);
30 | }
31 |
32 | FullScreenAction get currentFullScreenAction => _fullScreenAction;
33 |
34 | FullScreenAction _fullScreenActionFromString(String string) {
35 | for (var value in FullScreenAction.values) {
36 | if (string == value.toString()) {
37 | return value;
38 | }
39 | }
40 | return _defaultController;
41 | }
42 |
43 | String currentFullScreenActionName() {
44 | for (var e in _fullScreenActionMap.entries) {
45 | if (e.value == _fullScreenAction) {
46 | return e.key;
47 | }
48 | }
49 | return '';
50 | }
51 |
52 | Future chooseFullScreenAction(BuildContext context) async {
53 | FullScreenAction? result = await chooseMapDialog(context,
54 | values: _fullScreenActionMap, title: "选择操控方式");
55 | if (result != null) {
56 | await native.saveProperty(k: _propertyName, v: result.toString());
57 | _fullScreenAction = result;
58 | }
59 | }
60 |
61 | Widget fullScreenActionSetting() {
62 | return StatefulBuilder(
63 | builder: (BuildContext context, void Function(void Function()) setState) {
64 | return ListTile(
65 | title: const Text("操控方式"),
66 | subtitle: Text(currentFullScreenActionName()),
67 | onTap: () async {
68 | await chooseFullScreenAction(context);
69 | setState(() {});
70 | },
71 | );
72 | },
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/lib/configs/reader_type.dart:
--------------------------------------------------------------------------------
1 | /// 阅读器的类型
2 | import 'package:flutter/material.dart';
3 | import 'package:html/ffi.dart';
4 |
5 | enum ReaderType {
6 | webToon,
7 | webToonZoom,
8 | gallery,
9 | webToonFreeZoom,
10 | }
11 |
12 | const _types = {
13 | 'WebToon (默认)': ReaderType.webToon,
14 | 'WebToon (双击放大)': ReaderType.webToonZoom,
15 | '相册': ReaderType.gallery,
16 | 'WebToon (ListView双击放大)\n(此模式进度条无效)': ReaderType.webToonFreeZoom
17 | };
18 |
19 | const _propertyName = "readerType";
20 | late ReaderType _readerType;
21 |
22 | Future initReaderType() async {
23 | var value = await native.loadProperty(k: _propertyName);
24 | if (value == "") value = ReaderType.webToon.toString();
25 | _readerType = _readerTypeFromString(value);
26 | }
27 |
28 | ReaderType get currentReaderType => _readerType;
29 |
30 | ReaderType _readerTypeFromString(String pagerTypeString) {
31 | for (var value in ReaderType.values) {
32 | if (pagerTypeString == value.toString()) {
33 | return value;
34 | }
35 | }
36 | return ReaderType.webToon;
37 | }
38 |
39 | String currentReaderTypeName() {
40 | for (var e in _types.entries) {
41 | if (e.value == _readerType) {
42 | return e.key;
43 | }
44 | }
45 | return '';
46 | }
47 |
48 | Future choosePagerType(BuildContext buildContext) async {
49 | ReaderType? t = await showDialog(
50 | context: buildContext,
51 | builder: (BuildContext context) {
52 | return SimpleDialog(
53 | title: const Text("选择阅读模式"),
54 | children: _types.entries
55 | .map((e) => SimpleDialogOption(
56 | child: Text(e.key),
57 | onPressed: () {
58 | Navigator.of(context).pop(e.value);
59 | },
60 | ))
61 | .toList(),
62 | );
63 | },
64 | );
65 | if (t != null) {
66 | await native.saveProperty(k: _propertyName, v: t.toString());
67 | _readerType = t;
68 | }
69 | }
70 |
71 | Widget readerTypeSetting() {
72 | return StatefulBuilder(
73 | builder: (BuildContext context, void Function(void Function()) setState) {
74 | return ListTile(
75 | title: const Text("阅读器模式"),
76 | subtitle: Text(currentReaderTypeName()),
77 | onTap: () async {
78 | await choosePagerType(context);
79 | setState(() {});
80 | },
81 | );
82 | },
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/ci/src/check_release/main.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use std::collections::HashMap;
3 | use std::process::exit;
4 |
5 | const OWNER: &str = "niuhuan";
6 | const REPO: &str = "html-comic";
7 | const UA: &str = "niuhuan html-comic ci";
8 | const MAIN_BRANCH: &str = "master";
9 |
10 | #[tokio::main]
11 | async fn main() -> Result<()> {
12 | // get ghToken
13 | let gh_token = std::env::var("GH_TOKEN")?;
14 | if gh_token.is_empty() {
15 | panic!("Please set GH_TOKEN");
16 | }
17 |
18 | let vs_code_txt = tokio::fs::read_to_string("version.code.txt").await?;
19 | let vs_info_txt = tokio::fs::read_to_string("version.info.txt").await?;
20 |
21 | let code = vs_code_txt.trim();
22 | let info = vs_info_txt.trim();
23 |
24 | let client = reqwest::ClientBuilder::new().user_agent(UA).build()?;
25 |
26 | let check_response = client
27 | .get(format!(
28 | "https://api.github.com/repos/{}/{}/releases/tags/{}",
29 | OWNER, REPO, code
30 | ))
31 | .send()
32 | .await?;
33 |
34 | match check_response.status().as_u16() {
35 | 200 => {
36 | println!("release exists");
37 | exit(0);
38 | }
39 | 404 => (),
40 | code => {
41 | let text = check_response.text().await?;
42 | panic!("error for check release : {} : {}", code, text);
43 | }
44 | }
45 | drop(check_response);
46 |
47 | // 404
48 |
49 | let check_response = client
50 | .post(format!(
51 | "https://api.github.com/repos/{}/{}/releases",
52 | OWNER, REPO
53 | ))
54 | .header("Authorization", format!("token {}", gh_token))
55 | .json(&{
56 | let mut params = HashMap::::new();
57 | params.insert("tag_name".to_string(), code.to_string());
58 | params.insert("target_commitish".to_string(), MAIN_BRANCH.to_string());
59 | params.insert("name".to_string(), code.to_string());
60 | params.insert("body".to_string(), info.to_string());
61 | params
62 | })
63 | .send()
64 | .await?;
65 |
66 | match check_response.status().as_u16() {
67 | 201 => (),
68 | code => {
69 | let text = check_response.text().await?;
70 | panic!("error for create release : {} : {}", code, text);
71 | }
72 | }
73 | Ok(())
74 | }
75 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | @UIApplicationMain
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 |
11 | let controller = self.window.rootViewController as! FlutterViewController
12 | let channel = FlutterMethodChannel.init(name: "cross", binaryMessenger: controller as! FlutterBinaryMessenger)
13 |
14 | channel.setMethodCallHandler { (call, result) in
15 | Thread {
16 | if call.method == "root" {
17 |
18 | let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
19 |
20 | result(documentsPath)
21 |
22 | }
23 | else if call.method == "saveImageToGallery"{
24 | if let args = call.arguments as? String{
25 |
26 | do {
27 | let fileURL: URL = URL(fileURLWithPath: args)
28 | let imageData = try Data(contentsOf: fileURL)
29 |
30 | if let uiImage = UIImage(data: imageData) {
31 | UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil)
32 | result("OK")
33 | }else{
34 | result(FlutterError(code: "", message: "Error loading image ", details: ""))
35 | }
36 |
37 | } catch {
38 | result(FlutterError(code: "", message: "Error loading image : \(error)", details: ""))
39 | }
40 |
41 | }else{
42 | result(FlutterError(code: "", message: "params error", details: ""))
43 | }
44 | }
45 | else{
46 | result(FlutterMethodNotImplemented)
47 | }
48 | }.start()
49 | }
50 |
51 | print("dummy_value=\(dummy_method_to_enforce_bundling())");
52 | GeneratedPluginRegistrant.register(with: self)
53 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
22 |
30 |
34 |
38 |
39 |
40 |
41 |
42 |
43 |
45 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/lib/configs/reader_direction.dart:
--------------------------------------------------------------------------------
1 | /// 阅读器的方向
2 | import 'package:flutter/material.dart';
3 | import 'package:html/ffi.dart';
4 |
5 | enum ReaderDirection {
6 | topToBottom,
7 | leftToRight,
8 | rightToLeft,
9 | }
10 |
11 | const _types = {
12 | '从上到下': ReaderDirection.topToBottom,
13 | '从左到右': ReaderDirection.leftToRight,
14 | '从右到左': ReaderDirection.rightToLeft,
15 | };
16 |
17 | const _propertyName = "readerDirection";
18 | late ReaderDirection _readerDirection;
19 |
20 | ReaderDirection get gReaderDirection => _readerDirection;
21 |
22 | Future initReaderDirection() async {
23 | var value = await native.loadProperty(k: _propertyName);
24 | if (value == "") {
25 | value = ReaderDirection.topToBottom.toString();
26 | }
27 | _readerDirection = _pagerDirectionFromString(value);
28 | }
29 |
30 | ReaderDirection _pagerDirectionFromString(String pagerDirectionString) {
31 | for (var value in ReaderDirection.values) {
32 | if (pagerDirectionString == value.toString()) {
33 | return value;
34 | }
35 | }
36 | return ReaderDirection.topToBottom;
37 | }
38 |
39 | String currentReaderDirectionName() {
40 | for (var e in _types.entries) {
41 | if (e.value == _readerDirection) {
42 | return e.key;
43 | }
44 | }
45 | return '';
46 | }
47 |
48 | /// ?? to ActionButton And Event ??
49 | Future choosePagerDirection(BuildContext buildContext) async {
50 | ReaderDirection? choose = await showDialog(
51 | context: buildContext,
52 | builder: (BuildContext context) {
53 | return SimpleDialog(
54 | title: const Text("选择翻页方向"),
55 | children: _types.entries
56 | .map((e) => SimpleDialogOption(
57 | child: Text(e.key),
58 | onPressed: () {
59 | Navigator.of(context).pop(e.value);
60 | },
61 | ))
62 | .toList(),
63 | );
64 | },
65 | );
66 | if (choose != null) {
67 | await native.saveProperty(k: _propertyName, v: choose.toString());
68 | _readerDirection = choose;
69 | }
70 | }
71 |
72 | Widget readerDirectionSetting() {
73 | return StatefulBuilder(
74 | builder: (BuildContext context, void Function(void Function()) setState) {
75 | return ListTile(
76 | title: const Text("阅读器方向"),
77 | subtitle: Text(currentReaderDirectionName()),
78 | onTap: () async {
79 | await choosePagerDirection(context);
80 | setState(() {});
81 | },
82 | );
83 | },
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/native/src/hitomi_client/tests.rs:
--------------------------------------------------------------------------------
1 | use crate::Result;
2 |
3 | use super::{Client, ComicFile, ComicFilter, ComicFilterType, Lang, SortType};
4 |
5 | fn print(result: Result)
6 | where
7 | T: serde::Serialize + Send + Sync,
8 | {
9 | match result {
10 | Ok(t) => match serde_json::to_string(&t) {
11 | Ok(text) => println!("{}", text),
12 | Err(err) => panic!("{}", err),
13 | },
14 | Err(err) => panic!("{}", err),
15 | }
16 | }
17 |
18 | #[cfg(target_os = "windows")]
19 | fn client() -> Client {
20 | crate::Client::new_with_agent(
21 | reqwest::ClientBuilder::new()
22 | .proxy(reqwest::Proxy::all("socks5://127.0.0.1:10808/").unwrap())
23 | .build()
24 | .unwrap(),
25 | )
26 | }
27 |
28 | #[tokio::test]
29 | async fn test_comics() {
30 | print(
31 | client()
32 | .comics(
33 | ComicFilter {
34 | filter_type: ComicFilterType::Tag,
35 | filter_value: "full color".to_string(),
36 | },
37 | SortType::PopularWeek,
38 | Lang::Ja,
39 | 0,
40 | 10,
41 | )
42 | .await,
43 | );
44 | // {"records":[2202264,2202105,2202259,2202253,2202254,2202252,2202250,2202251,2202248,2202247],"min_index":0,"max_index":9,"limit":10,"offset":0,"total":705295}
45 | }
46 |
47 | #[tokio::test]
48 | async fn test_comic_introduction() {
49 | print(client().comic_introduction(2202248).await);
50 | }
51 |
52 | #[tokio::test]
53 | async fn test_comic_reader_info() {
54 | print(client().comic_reader_info(2202248).await);
55 | }
56 |
57 | const TEST_FILE: &str = r#"{"name":"4.jpg","width":1204,"hasavif":1,"hash":"b77ef8cdf4461a43f3a58acaffa95abd7aae805ce6a1b1d52479aeb14ed80d93","haswebp":1,"height":1700}"#;
58 |
59 | #[tokio::test]
60 | async fn test_file_url() {
61 | let client = client();
62 | let gg = client.download_gg().await.unwrap();
63 | let file: ComicFile = serde_json::from_str(TEST_FILE).unwrap();
64 | println!("{}", client.file_url(&gg, &file).unwrap());
65 | // web https://aa.hitomi.la/avif/1650877201/648/e474b251eda29035d7423b527b0e507034d7613b3dd3e7fb910c0f600f144882.avif
66 | // https://ba.hitomi.la/avif/1650873602/985/b77ef8cdf4461a43f3a58acaffa95abd7aae805ce6a1b1d52479aeb14ed80d93.avif
67 | // 2202248
68 | // .header("Referer", "$BASE_URL/reader/$hlId.html")
69 | // curl -x socks5://localhost:10808/ -H "Referer: https://hitomi.la/reader/2202248.html" https://ba.hitomi.la/webp/1650873602/648/e474b251eda29035d7423b527b0e507034d7613b3dd3e7fb910c0f600f144882.webp
70 | }
71 |
--------------------------------------------------------------------------------
/lib/screens/init_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../configs/export_rename.dart';
4 | import '../configs/platform.dart';
5 | import '../configs/proxy.dart';
6 | import '../configs/android_display_mode.dart';
7 | import '../configs/android_secure_flag.dart';
8 | import '../configs/auto_clean.dart';
9 | import '../configs/auto_full_screen.dart';
10 | import '../configs/content_failed_reload_action.dart';
11 | import '../configs/full_screen_action.dart';
12 | import '../configs/keyboard_controller.dart';
13 | import '../configs/list_layout.dart';
14 | import '../configs/no_animation.dart';
15 | import '../configs/pager_action.dart';
16 | import '../configs/reader_direction.dart';
17 | import '../configs/reader_slider_position.dart';
18 | import '../configs/reader_type.dart';
19 | import '../configs/themes.dart';
20 | import '../configs/time_offset_hour.dart';
21 | import '../configs/version.dart';
22 | import '../configs/volume_controller.dart';
23 | import '../cross.dart';
24 | import '../ffi.dart';
25 | import 'comics_screen.dart';
26 |
27 | // 初始化界面
28 | class InitScreen extends StatefulWidget {
29 | const InitScreen({Key? key}) : super(key: key);
30 |
31 | @override
32 | State createState() => _InitScreenState();
33 | }
34 |
35 | class _InitScreenState extends State {
36 | @override
37 | initState() {
38 | _init();
39 | super.initState();
40 | }
41 |
42 | Future _init() async {
43 | // 初始化配置文件
44 | await native.init(root: await cross.root(context));
45 | await initPlatform(); // 必须第一个初始化, 加载设备信息
46 | await initAutoClean();
47 | await initProxy();
48 | await initFont();
49 | await initTheme();
50 | await initListLayout();
51 | await initReaderType();
52 | await initReaderDirection();
53 | await initReaderSliderPosition();
54 | await initAutoFullScreen();
55 | await initFullScreenAction();
56 | await initPagerAction();
57 | await initContentFailedReloadAction();
58 | await initVolumeController();
59 | await initKeyboardController();
60 | await initAndroidDisplayMode();
61 | await initTimeZone();
62 | await initAndroidSecureFlag();
63 | await initNoAnimation();
64 | await initExportRename();
65 | await initVersion();
66 | autoCheckNewVersion();
67 | Navigator.pushReplacement(
68 | context,
69 | MaterialPageRoute(builder: (context) => const ComicsScreen()),
70 | );
71 | }
72 |
73 | @override
74 | Widget build(BuildContext context) {
75 | return Scaffold(
76 | backgroundColor: const Color(0xff8abedc),
77 | body: ConstrainedBox(
78 | constraints: const BoxConstraints.expand(),
79 | child: Image.asset(
80 | "lib/assets/init.png",
81 | fit: BoxFit.contain,
82 | ),
83 | ),
84 | );
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/lib/cross.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:file_picker/file_picker.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter/services.dart';
6 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
7 | import 'package:permission_handler/permission_handler.dart';
8 | import 'package:url_launcher/url_launcher.dart';
9 |
10 | // todo save origin image in android // like with filesystem_picker
11 |
12 | import 'commons.dart';
13 | import 'ffi.dart';
14 |
15 | const cross = Cross._();
16 |
17 | class Cross {
18 | const Cross._();
19 |
20 | static const _channel = MethodChannel("cross");
21 |
22 | Future root(BuildContext context) async {
23 | if (Platform.isAndroid || Platform.isIOS) {
24 | return await _channel.invokeMethod("root");
25 | }
26 | if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
27 | return await native.desktopRoot();
28 | }
29 | throw AppLocalizations.of(context)!.unsupportedPlatform;
30 | }
31 |
32 | Future saveImageFileToGallery(String path, BuildContext context) async {
33 | if (Platform.isIOS || Platform.isAndroid) {
34 | if (Platform.isAndroid) {
35 | if (!(await Permission.storage.request()).isGranted) {
36 | return;
37 | }
38 | }
39 | try {
40 | await _channel.invokeMethod("saveImageToGallery", path);
41 | defaultToast(context, AppLocalizations.of(context)!.success);
42 | } catch (e) {
43 | errorToast(context, AppLocalizations.of(context)!.failed + " : $e");
44 | }
45 | } else if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
46 | String? selectedDirectory = await FilePicker.platform.getDirectoryPath();
47 | if (selectedDirectory != null) {
48 | try {
49 | await native.copyImageTo(srcPath: path, toDir: selectedDirectory);
50 | defaultToast(context, AppLocalizations.of(context)!.success);
51 | } catch (e) {
52 | errorToast(context, AppLocalizations.of(context)!.failed + " : $e");
53 | }
54 | }
55 | }
56 | }
57 |
58 | Future> loadAndroidModes() async {
59 | return List.of(await _channel.invokeMethod("androidGetModes"))
60 | .map((e) => "$e")
61 | .toList();
62 | }
63 |
64 | Future setAndroidMode(String androidDisplayMode) {
65 | return _channel
66 | .invokeMethod("androidSetMode", {"mode": androidDisplayMode});
67 | }
68 |
69 | Future androidSecureFlag(bool flag) {
70 | return _channel.invokeMethod("androidSecureFlag", {
71 | "flag": flag,
72 | });
73 | }
74 |
75 | Future androidGetVersion() async {
76 | return await _channel.invokeMethod("androidGetVersion", {});
77 | }
78 | }
79 |
80 | /// 打开web页面
81 | Future openUrl(String url) async {
82 | if (await canLaunch(url)) {
83 | await launch(
84 | url,
85 | forceSafariVC: false,
86 | );
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/native/src/database/cache/web_cache.rs:
--------------------------------------------------------------------------------
1 | use std::future::Future;
2 | use std::ops::Deref;
3 | use std::pin::Pin;
4 | use std::time::Duration;
5 |
6 | use sea_orm::entity::prelude::*;
7 | use sea_orm::sea_query::Expr;
8 | use sea_orm::EntityTrait;
9 | use sea_orm::IntoActiveModel;
10 |
11 | use crate::database::cache::CACHE_DATABASE;
12 | use crate::database::{create_index, create_table_if_not_exists, index_exists};
13 |
14 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
15 | #[sea_orm(table_name = "web_cache")]
16 | pub struct Model {
17 | #[sea_orm(primary_key, auto_increment = false)]
18 | pub cache_key: String,
19 | pub cache_content: String,
20 | pub cache_time: i64,
21 | }
22 |
23 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
24 | pub enum Relation {}
25 |
26 | impl ActiveModelBehavior for ActiveModel {}
27 |
28 | pub(crate) async fn init() {
29 | let gdb = CACHE_DATABASE.get().unwrap().lock().await;
30 | let db = gdb.deref();
31 | create_table_if_not_exists(db, Entity).await;
32 | if !index_exists(db, "web_cache", "web_cache_idx_cache_time").await {
33 | create_index(
34 | db,
35 | "web_cache",
36 | vec!["cache_time"],
37 | "web_cache_idx_cache_time",
38 | )
39 | .await;
40 | }
41 | }
42 |
43 | pub(crate) async fn cache_first serde::Deserialize<'de> + serde::Serialize>(
44 | key: String,
45 | expire: Duration,
46 | pin: Pin>>>,
47 | ) -> anyhow::Result {
48 | let time = chrono::Local::now().timestamp_millis();
49 | let db = CACHE_DATABASE.get().unwrap().lock().await;
50 | let in_db = Entity::find_by_id(key.clone()).one(db.deref()).await?;
51 | if let Some(ref model) = in_db {
52 | if time < (model.cache_time + expire.as_millis() as i64) {
53 | return Ok(serde_json::from_str(&model.cache_content)?);
54 | }
55 | };
56 | drop(db);
57 | let t = pin.await?;
58 | let content = serde_json::to_string(&t)?;
59 | let db = CACHE_DATABASE.get().unwrap().lock().await;
60 | let in_db = Entity::find_by_id(key.clone()).one(db.deref()).await?;
61 | if let Some(_) = in_db {
62 | Entity::update_many()
63 | .filter(Column::CacheKey.eq(key.clone()))
64 | .col_expr(Column::CacheTime, Expr::value(time.clone()))
65 | .col_expr(Column::CacheContent, Expr::value(content.clone()))
66 | .exec(db.deref())
67 | .await?;
68 | } else {
69 | Model {
70 | cache_key: key,
71 | cache_content: content,
72 | cache_time: time,
73 | }
74 | .into_active_model()
75 | .insert(db.deref())
76 | .await?;
77 | }
78 | Ok(t)
79 | }
80 |
81 | pub(crate) async fn clean_web_cache_by_time(time: i64) -> anyhow::Result<()> {
82 | Entity::delete_many()
83 | .filter(Column::CacheTime.lt(time))
84 | .exec(CACHE_DATABASE.get().unwrap().lock().await.deref())
85 | .await?;
86 | Ok(())
87 | }
88 |
--------------------------------------------------------------------------------
/linux/flutter/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.10)
2 |
3 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
4 |
5 | # Configuration provided via flutter tool.
6 | include(${EPHEMERAL_DIR}/generated_config.cmake)
7 |
8 | # TODO: Move the rest of this into files in ephemeral. See
9 | # https://github.com/flutter/flutter/issues/57146.
10 |
11 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...),
12 | # which isn't available in 3.10.
13 | function(list_prepend LIST_NAME PREFIX)
14 | set(NEW_LIST "")
15 | foreach(element ${${LIST_NAME}})
16 | list(APPEND NEW_LIST "${PREFIX}${element}")
17 | endforeach(element)
18 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
19 | endfunction()
20 |
21 | # === Flutter Library ===
22 | # System-level dependencies.
23 | find_package(PkgConfig REQUIRED)
24 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
25 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
26 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
27 |
28 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
29 |
30 | # Published to parent scope for install step.
31 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
32 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
33 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
34 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
35 |
36 | list(APPEND FLUTTER_LIBRARY_HEADERS
37 | "fl_basic_message_channel.h"
38 | "fl_binary_codec.h"
39 | "fl_binary_messenger.h"
40 | "fl_dart_project.h"
41 | "fl_engine.h"
42 | "fl_json_message_codec.h"
43 | "fl_json_method_codec.h"
44 | "fl_message_codec.h"
45 | "fl_method_call.h"
46 | "fl_method_channel.h"
47 | "fl_method_codec.h"
48 | "fl_method_response.h"
49 | "fl_plugin_registrar.h"
50 | "fl_plugin_registry.h"
51 | "fl_standard_message_codec.h"
52 | "fl_standard_method_codec.h"
53 | "fl_string_codec.h"
54 | "fl_value.h"
55 | "fl_view.h"
56 | "flutter_linux.h"
57 | )
58 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
59 | add_library(flutter INTERFACE)
60 | target_include_directories(flutter INTERFACE
61 | "${EPHEMERAL_DIR}"
62 | )
63 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
64 | target_link_libraries(flutter INTERFACE
65 | PkgConfig::GTK
66 | PkgConfig::GLIB
67 | PkgConfig::GIO
68 | )
69 | add_dependencies(flutter flutter_assemble)
70 |
71 | # === Flutter tool backend ===
72 | # _phony_ is a non-existent file to force this command to run every time,
73 | # since currently there's no way to get a full input/output list from the
74 | # flutter tool.
75 | add_custom_command(
76 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
77 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_
78 | COMMAND ${CMAKE_COMMAND} -E env
79 | ${FLUTTER_TOOL_ENVIRONMENT}
80 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
81 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
82 | VERBATIM
83 | )
84 | add_custom_target(flutter_assemble DEPENDS
85 | "${FLUTTER_LIBRARY}"
86 | ${FLUTTER_LIBRARY_HEADERS}
87 | )
88 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15 | if (flutterVersionCode == null) {
16 | flutterVersionCode = '1'
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | flutterVersionName = '1.0'
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply plugin: 'kotlin-android'
26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
27 |
28 | android {
29 | compileSdkVersion flutter.compileSdkVersion
30 |
31 | compileOptions {
32 | sourceCompatibility JavaVersion.VERSION_1_8
33 | targetCompatibility JavaVersion.VERSION_1_8
34 | }
35 |
36 | kotlinOptions {
37 | jvmTarget = '1.8'
38 | }
39 |
40 | sourceSets {
41 | main.java.srcDirs += 'src/main/kotlin'
42 | }
43 |
44 | defaultConfig {
45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
46 | applicationId "niuhuan.html"
47 | minSdkVersion flutter.minSdkVersion
48 | targetSdkVersion flutter.targetSdkVersion
49 | versionCode flutterVersionCode.toInteger()
50 | versionName flutterVersionName
51 | }
52 |
53 | buildTypes {
54 | release {
55 | // TODO: Add your own signing config for the release build.
56 | // Signing with the debug keys for now, so `flutter run --release` works.
57 | signingConfig signingConfigs.debug
58 | }
59 | }
60 | }
61 |
62 | flutter {
63 | source '../..'
64 | }
65 |
66 | dependencies {
67 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
68 | }
69 | //
70 | //[
71 | // new Tuple2('Debug', ''),
72 | // new Tuple2('Profile', '--release'),
73 | // new Tuple2('Release', '--release')
74 | //].each {
75 | // def taskPostfix = it.first
76 | // def profileMode = it.second
77 | // tasks.whenTaskAdded { task ->
78 | // if (task.name == "javaPreCompile$taskPostfix") {
79 | // task.dependsOn "cargoBuild$taskPostfix"
80 | // }
81 | // }
82 | // tasks.register("cargoBuild$taskPostfix", Exec) {
83 | // // Until https://github.com/bbqsrc/cargo-ndk/pull/13 is merged,
84 | // // this workaround is necessary.
85 | // commandLine 'sh', '-c', """cd ../../native && \
86 | // ANDROID_NDK_HOME="$ANDROID_NDK" cargo ndk \
87 | // -t armeabi-v7a -t arm64-v8a \
88 | // -o ../android/app/src/main/jniLibs build $profileMode"""
89 | // }
90 | //}
91 |
92 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/lib/assets/android.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
65 |
--------------------------------------------------------------------------------
/lib/screens/settings_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:html/configs/proxy.dart';
3 | import 'package:html/screens/about_screen.dart';
4 | import 'package:html/screens/components/badge.dart';
5 | import 'package:html/screens/theme_screen.dart';
6 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
7 | import '../configs/themes.dart';
8 | import '../configs/android_secure_flag.dart';
9 | import '../configs/auto_clean.dart';
10 | import '../configs/auto_full_screen.dart';
11 | import '../configs/content_failed_reload_action.dart';
12 | import '../configs/full_screen_action.dart';
13 | import '../configs/keyboard_controller.dart';
14 | import '../configs/no_animation.dart';
15 | import '../configs/pager_action.dart';
16 | import '../configs/reader_direction.dart';
17 | import '../configs/reader_slider_position.dart';
18 | import '../configs/reader_type.dart';
19 | import '../configs/time_offset_hour.dart';
20 | import '../configs/volume_controller.dart';
21 | import '../configs/android_display_mode.dart';
22 |
23 | class SettingsScreen extends StatelessWidget {
24 | const SettingsScreen({Key? key}) : super(key: key);
25 |
26 | @override
27 | Widget build(BuildContext context) {
28 | return Scaffold(
29 | appBar: AppBar(
30 | title: Text(AppLocalizations.of(context)!.settings),
31 | ),
32 | body: ListView(children: [
33 | const Divider(),
34 | ListTile(
35 | onTap: () async {
36 | Navigator.push(
37 | context,
38 | MaterialPageRoute(builder: (context) => const AboutScreen()),
39 | );
40 | },
41 | title: VersionBadged(
42 | child: Text(AppLocalizations.of(context)!.about),
43 | ),
44 | ),
45 | const Divider(),
46 | ListTile(
47 | onTap: () async {
48 | if (androidNightModeDisplay) {
49 | Navigator.push(
50 | context,
51 | MaterialPageRoute(builder: (context) => const ThemeScreen()),
52 | );
53 | } else {
54 | chooseLightTheme(context);
55 | }
56 | },
57 | title: Text(AppLocalizations.of(context)!.theme),
58 | ),
59 | const Divider(),
60 | proxySetting(),
61 | const Divider(),
62 | const Divider(),
63 | pagerActionSetting(),
64 | contentFailedReloadActionSetting(),
65 | timeZoneSetting(),
66 | const Divider(),
67 | readerTypeSetting(),
68 | readerDirectionSetting(),
69 | readerSliderPositionSetting(),
70 | autoFullScreenSetting(),
71 | fullScreenActionSetting(),
72 | volumeControllerSetting(),
73 | keyboardControllerSetting(),
74 | noAnimationSetting(),
75 | const Divider(),
76 | const Divider(),
77 | autoCleanSecSetting(),
78 | ListTile(
79 | onTap: () {
80 | // todo
81 | },
82 | title: Text(AppLocalizations.of(context)!.clearCache),
83 | ),
84 | const Divider(),
85 | const Divider(),
86 | androidDisplayModeSetting(),
87 | androidSecureFlagSetting(),
88 | const Divider(),
89 | fontSetting(),
90 | const Divider(),
91 | ]),
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/windows/runner/Runner.rc:
--------------------------------------------------------------------------------
1 | // Microsoft Visual C++ generated resource script.
2 | //
3 | #pragma code_page(65001)
4 | #include "resource.h"
5 |
6 | #define APSTUDIO_READONLY_SYMBOLS
7 | /////////////////////////////////////////////////////////////////////////////
8 | //
9 | // Generated from the TEXTINCLUDE 2 resource.
10 | //
11 | #include "winres.h"
12 |
13 | /////////////////////////////////////////////////////////////////////////////
14 | #undef APSTUDIO_READONLY_SYMBOLS
15 |
16 | /////////////////////////////////////////////////////////////////////////////
17 | // English (United States) resources
18 |
19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
21 |
22 | #ifdef APSTUDIO_INVOKED
23 | /////////////////////////////////////////////////////////////////////////////
24 | //
25 | // TEXTINCLUDE
26 | //
27 |
28 | 1 TEXTINCLUDE
29 | BEGIN
30 | "resource.h\0"
31 | END
32 |
33 | 2 TEXTINCLUDE
34 | BEGIN
35 | "#include ""winres.h""\r\n"
36 | "\0"
37 | END
38 |
39 | 3 TEXTINCLUDE
40 | BEGIN
41 | "\r\n"
42 | "\0"
43 | END
44 |
45 | #endif // APSTUDIO_INVOKED
46 |
47 |
48 | /////////////////////////////////////////////////////////////////////////////
49 | //
50 | // Icon
51 | //
52 |
53 | // Icon with lowest ID value placed first to ensure application icon
54 | // remains consistent on all systems.
55 | IDI_APP_ICON ICON "resources\\app_icon.ico"
56 |
57 |
58 | /////////////////////////////////////////////////////////////////////////////
59 | //
60 | // Version
61 | //
62 |
63 | #ifdef FLUTTER_BUILD_NUMBER
64 | #define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
65 | #else
66 | #define VERSION_AS_NUMBER 1,0,0
67 | #endif
68 |
69 | #ifdef FLUTTER_BUILD_NAME
70 | #define VERSION_AS_STRING #FLUTTER_BUILD_NAME
71 | #else
72 | #define VERSION_AS_STRING "1.0.0"
73 | #endif
74 |
75 | VS_VERSION_INFO VERSIONINFO
76 | FILEVERSION VERSION_AS_NUMBER
77 | PRODUCTVERSION VERSION_AS_NUMBER
78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
79 | #ifdef _DEBUG
80 | FILEFLAGS VS_FF_DEBUG
81 | #else
82 | FILEFLAGS 0x0L
83 | #endif
84 | FILEOS VOS__WINDOWS32
85 | FILETYPE VFT_APP
86 | FILESUBTYPE 0x0L
87 | BEGIN
88 | BLOCK "StringFileInfo"
89 | BEGIN
90 | BLOCK "040904e4"
91 | BEGIN
92 | VALUE "CompanyName", "niuhuan" "\0"
93 | VALUE "FileDescription", "html" "\0"
94 | VALUE "FileVersion", VERSION_AS_STRING "\0"
95 | VALUE "InternalName", "html" "\0"
96 | VALUE "LegalCopyright", "Copyright (C) 2022 niuhuan. All rights reserved." "\0"
97 | VALUE "OriginalFilename", "html.exe" "\0"
98 | VALUE "ProductName", "html" "\0"
99 | VALUE "ProductVersion", VERSION_AS_STRING "\0"
100 | END
101 | END
102 | BLOCK "VarFileInfo"
103 | BEGIN
104 | VALUE "Translation", 0x409, 1252
105 | END
106 | END
107 |
108 | #endif // English (United States) resources
109 | /////////////////////////////////////////////////////////////////////////////
110 |
111 |
112 |
113 | #ifndef APSTUDIO_INVOKED
114 | /////////////////////////////////////////////////////////////////////////////
115 | //
116 | // Generated from the TEXTINCLUDE 3 resource.
117 | //
118 |
119 |
120 | /////////////////////////////////////////////////////////////////////////////
121 | #endif // not APSTUDIO_INVOKED
122 |
--------------------------------------------------------------------------------
/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
54 |
60 |
61 |
62 |
63 |
69 |
71 |
77 |
78 |
79 |
80 |
82 |
83 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
54 |
60 |
61 |
62 |
63 |
69 |
71 |
77 |
78 |
79 |
80 |
82 |
83 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/native/src/database/mod.rs:
--------------------------------------------------------------------------------
1 | use std::time::Duration;
2 |
3 | use sea_orm::prelude::DatabaseConnection;
4 | use sea_orm::{ConnectionTrait, EntityTrait, Schema, Statement};
5 |
6 | use crate::{get_database_dir, join_paths};
7 |
8 | pub(crate) mod active;
9 | pub(crate) mod cache;
10 | pub(crate) mod properties;
11 |
12 | pub(crate) async fn init_database() {
13 | cache::init().await;
14 | properties::init().await;
15 | active::init().await;
16 | }
17 |
18 | pub(crate) async fn connect_db(path: &str) -> DatabaseConnection {
19 | println!("CONNECT TO DB : {}", path);
20 | let path = join_paths(vec![get_database_dir().as_str(), path]);
21 | println!("DB PATH : {}", path);
22 | let url = format!("sqlite:{}?mode=rwc", path);
23 | let mut opt = sea_orm::ConnectOptions::new(url);
24 | opt.max_connections(20)
25 | .min_connections(5)
26 | .connect_timeout(Duration::from_secs(8))
27 | .idle_timeout(Duration::from_secs(8))
28 | .sqlx_logging(true);
29 | sea_orm::Database::connect(opt).await.unwrap()
30 | }
31 |
32 | pub(crate) async fn create_table_if_not_exists(db: &DatabaseConnection, entity: E)
33 | where
34 | E: EntityTrait,
35 | {
36 | if !has_table(db, entity.table_name()).await {
37 | create_table(db, entity).await;
38 | };
39 | }
40 |
41 | pub(crate) async fn has_table(db: &DatabaseConnection, table_name: &str) -> bool {
42 | let stmt = Statement::from_string(
43 | db.get_database_backend(),
44 | format!(
45 | "SELECT COUNT(*) AS c FROM sqlite_master WHERE type='table' AND name='{}';",
46 | table_name,
47 | ),
48 | );
49 | let rsp = db.query_one(stmt).await.unwrap().unwrap();
50 | let count: i32 = rsp.try_get("", "c").unwrap();
51 | count > 0
52 | }
53 |
54 | pub(crate) async fn create_table(db: &DatabaseConnection, entity: E)
55 | where
56 | E: EntityTrait,
57 | {
58 | let builder = db.get_database_backend();
59 | let schema = Schema::new(builder);
60 | let stmt = &schema.create_table_from_entity(entity);
61 | let stmt = builder.build(stmt);
62 | db.execute(stmt).await.unwrap();
63 | }
64 |
65 | pub(crate) async fn index_exists(
66 | db: &DatabaseConnection,
67 | table_name: &str,
68 | index_name: &str,
69 | ) -> bool {
70 | let stmt = Statement::from_string(
71 | db.get_database_backend(),
72 | format!(
73 | "select COUNT(*) AS c from sqlite_master where type='index' AND tbl_name='{}' AND name='{}';",
74 | table_name, index_name,
75 | ),
76 | );
77 | db.query_one(stmt)
78 | .await
79 | .unwrap()
80 | .unwrap()
81 | .try_get::("", "c")
82 | .unwrap()
83 | > 0
84 | }
85 |
86 | pub(crate) async fn create_index_a(
87 | db: &DatabaseConnection,
88 | table_name: &str,
89 | columns: Vec<&str>,
90 | index_name: &str,
91 | uk: bool,
92 | ) {
93 | let stmt = Statement::from_string(
94 | db.get_database_backend(),
95 | format!(
96 | "CREATE {} INDEX {} ON {}({});",
97 | if uk { "UNIQUE" } else { "" },
98 | index_name,
99 | table_name,
100 | columns.join(","),
101 | ),
102 | );
103 | db.execute(stmt).await.unwrap();
104 | }
105 |
106 | #[allow(dead_code)]
107 | pub(crate) async fn create_index(
108 | db: &DatabaseConnection,
109 | table_name: &str,
110 | columns: Vec<&str>,
111 | index_name: &str,
112 | ) {
113 | create_index_a(db, table_name, columns, index_name, false).await
114 | }
115 |
--------------------------------------------------------------------------------
/native/src/database/active/comic_view_log.rs:
--------------------------------------------------------------------------------
1 | use std::ops::Deref;
2 |
3 | use sea_orm::entity::prelude::*;
4 | use sea_orm::QueryOrder;
5 | use sea_orm::QuerySelect;
6 | use sea_orm::{EntityTrait, IntoActiveModel, Set};
7 |
8 | use crate::database::active::ACTIVE_DATABASE;
9 | use crate::database::{create_index, create_table_if_not_exists, index_exists};
10 |
11 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
12 | #[sea_orm(table_name = "comic_view_log")]
13 | pub struct Model {
14 | #[sea_orm(primary_key, auto_increment = false)]
15 | pub comic_id: i32,
16 | pub comic_title: String,
17 | pub comic_artists: String,
18 | pub comic_series: String,
19 | pub comic_tags: String,
20 | pub comic_type: String,
21 | pub comic_img1: String,
22 | pub comic_img2: String,
23 | pub add_timestamp_utc: i64,
24 | pub page_rank: i32,
25 | pub view_time: i64,
26 | }
27 |
28 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
29 | pub enum Relation {}
30 |
31 | impl ActiveModelBehavior for ActiveModel {}
32 |
33 | pub(crate) async fn init() {
34 | let db = ACTIVE_DATABASE.get().unwrap().lock().await;
35 | create_table_if_not_exists(db.deref(), Entity).await;
36 | if !index_exists(db.deref(), "comic_view_log", "comic_view_log_idx_view_time").await {
37 | create_index(
38 | db.deref(),
39 | "comic_view_log",
40 | vec!["view_time"],
41 | "comic_view_log_idx_view_time",
42 | )
43 | .await;
44 | }
45 | }
46 |
47 | pub(crate) async fn view_info(mut model: Model) -> anyhow::Result<()> {
48 | let db = ACTIVE_DATABASE.get().unwrap().lock().await;
49 | if let Some(in_db) = Entity::find_by_id(model.comic_id.clone())
50 | .one(db.deref())
51 | .await?
52 | {
53 | let mut in_db = in_db.into_active_model();
54 | in_db.comic_id = Set(model.comic_id);
55 | in_db.comic_title = Set(model.comic_title);
56 | in_db.comic_artists = Set(model.comic_artists);
57 | in_db.comic_series = Set(model.comic_series);
58 | in_db.comic_tags = Set(model.comic_tags);
59 | in_db.comic_type = Set(model.comic_type);
60 | in_db.comic_img1 = Set(model.comic_img1);
61 | in_db.comic_img2 = Set(model.comic_img2);
62 | in_db.add_timestamp_utc = Set(model.add_timestamp_utc);
63 | in_db.view_time = Set(chrono::Local::now().timestamp_millis());
64 | in_db.update(db.deref()).await?;
65 | } else {
66 | model.view_time = chrono::Local::now().timestamp_millis();
67 | model.into_active_model().insert(db.deref()).await?;
68 | }
69 | Ok(())
70 | }
71 |
72 | pub(crate) async fn view_page(comic_id: i32, page_rank: i32) -> anyhow::Result<()> {
73 | let db = ACTIVE_DATABASE.get().unwrap().lock().await;
74 | if let Some(in_db) = Entity::find_by_id(comic_id).one(db.deref()).await? {
75 | let mut in_db = in_db.into_active_model();
76 | in_db.page_rank = Set(page_rank);
77 | in_db.view_time = Set(chrono::Local::now().timestamp_millis());
78 | in_db.update(db.deref()).await?;
79 | }
80 | Ok(())
81 | }
82 |
83 | pub(crate) async fn load_view_logs(page: i64) -> anyhow::Result> {
84 | let db = ACTIVE_DATABASE.get().unwrap().lock().await;
85 | Ok(Entity::find()
86 | .order_by_desc(Column::ViewTime)
87 | .offset(page as u64 * 20)
88 | .limit(20)
89 | .all(db.deref())
90 | .await?)
91 | }
92 |
93 | pub(crate) async fn view_log_by_comic_id(comic_id: i32) -> anyhow::Result