From 67d3227fde12f1f2cae025aadfa11958829676c6 Mon Sep 17 00:00:00 2001 From: Jing <42014615+jing332@users.noreply.github.com> Date: Sun, 14 Jan 2024 17:53:58 +0800 Subject: [PATCH] init --- .gitignore | 48 +++ .metadata | 30 ++ README.md | 16 + analysis_options.yaml | 28 ++ android/.gitignore | 26 ++ android/alist-lib/scripts/clear.sh | 10 + android/alist-lib/scripts/init_alist_core.sh | 13 + android/alist-lib/scripts/init_alist_web.sh | 5 + android/alist-lib/scripts/install_alist.sh | 37 +++ android/app/build.gradle | 111 +++++++ android/app/src/debug/AndroidManifest.xml | 7 + android/app/src/main/AndroidManifest.xml | 42 +++ .../jing332/alistflutter/AListService.kt | 223 +++++++++++++ .../com/github/jing332/alistflutter/App.kt | 21 ++ .../jing332/alistflutter/BridgeUtils.kt | 69 ++++ .../jing332/alistflutter/MainActivity.kt | 106 ++++++ .../alistflutter/SwitchServerActivity.kt | 24 ++ .../jing332/alistflutter/config/AppConfig.kt | 18 ++ .../jing332/alistflutter/constant/AppConst.kt | 23 ++ .../jing332/alistflutter/model/ShortCuts.kt | 40 +++ .../alistflutter/model/UpdateResult.kt | 11 + .../jing332/alistflutter/model/alist/AList.kt | 148 +++++++++ .../alistflutter/model/alist/AListConfig.kt | 93 ++++++ .../model/alist/AListConfigManager.kt | 74 +++++ .../alistflutter/utils/AndroidUtils.kt | 22 ++ .../alistflutter/utils/ClipBoardUtils.kt | 97 ++++++ .../jing332/alistflutter/utils/FileUtils.kt | 42 +++ .../jing332/alistflutter/utils/StringUtils.kt | 40 +++ .../jing332/alistflutter/utils/ToastUtils.kt | 56 ++++ .../res/drawable-v21/launch_background.xml | 12 + .../app/src/main/res/drawable/alist_logo.xml | 6 + .../src/main/res/drawable/alist_switch.xml | 10 + .../app/src/main/res/drawable/ic_female.xml | 12 + .../res/drawable/ic_launcher_foreground.xml | 17 + .../main/res/drawable/launch_background.xml | 12 + android/app/src/main/res/drawable/server.xml | 5 + android/app/src/main/res/drawable/server2.xml | 5 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1278 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2636 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 936 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1708 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1776 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3684 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2588 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5956 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3540 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 8482 bytes .../app/src/main/res/values-night/styles.xml | 18 ++ .../res/values/ic_launcher_background.xml | 4 + android/app/src/main/res/values/strings.xml | 74 +++++ android/app/src/main/res/values/styles.xml | 18 ++ android/app/src/main/res/values/themes.xml | 6 + android/app/src/main/res/xml/backup_rules.xml | 2 + .../main/res/xml/data_extraction_rules.xml | 4 + .../app/src/main/res/xml/file_path_data.xml | 4 + android/app/src/main/res/xml/file_paths.xml | 8 + .../main/res/xml/network_security_config.xml | 5 + android/app/src/profile/AndroidManifest.xml | 7 + android/build.gradle | 47 +++ android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 6 + android/settings.gradle | 29 ++ .../java/com/github/jing332/pigeon/Api.java | 42 +++ android/utils/.gitignore | 2 + android/utils/build.gradle.kts | 54 ++++ android/utils/consumer-rules.pro | 0 android/utils/proguard-rules.pro | 21 ++ .../jing332/utils/ExampleInstrumentedTest.kt | 24 ++ android/utils/src/main/AndroidManifest.xml | 4 + android/utils/src/main/cpp/CMakeLists.txt | 37 +++ android/utils/src/main/cpp/utils.cpp | 65 ++++ .../com/github/jing332/utils/NativeLib.kt | 10 + .../github/jing332/utils/ExampleUnitTest.kt | 17 + lib/bridges/app_config.dart | 29 ++ lib/bridges/bridge.dart | 13 + lib/bridges/event.dart | 8 + lib/generated_api.dart | 9 + lib/main.dart | 126 ++++++++ lib/pages/alist.dart | 24 ++ lib/pages/settings.dart | 13 + lib/pages/web.dart | 12 + lib/router.dart | 27 ++ .../switch_floating_action_button.dart | 64 ++++ pigeons/pigeon.dart | 0 pigeons/run.cmd | 2 + pubspec.lock | 301 ++++++++++++++++++ pubspec.yaml | 94 ++++++ test/widget_test.dart | 30 ++ 90 files changed, 2832 insertions(+) create mode 100644 .gitignore create mode 100644 .metadata create mode 100644 README.md create mode 100644 analysis_options.yaml create mode 100644 android/.gitignore create mode 100644 android/alist-lib/scripts/clear.sh create mode 100644 android/alist-lib/scripts/init_alist_core.sh create mode 100644 android/alist-lib/scripts/init_alist_web.sh create mode 100644 android/alist-lib/scripts/install_alist.sh create mode 100644 android/app/build.gradle create mode 100644 android/app/src/debug/AndroidManifest.xml create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/AListService.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/App.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/BridgeUtils.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/MainActivity.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/SwitchServerActivity.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/config/AppConfig.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/constant/AppConst.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/model/ShortCuts.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/model/UpdateResult.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AList.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AListConfig.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AListConfigManager.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/AndroidUtils.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/ClipBoardUtils.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/FileUtils.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/StringUtils.kt create mode 100644 android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/ToastUtils.kt create mode 100644 android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 android/app/src/main/res/drawable/alist_logo.xml create mode 100644 android/app/src/main/res/drawable/alist_switch.xml create mode 100644 android/app/src/main/res/drawable/ic_female.xml create mode 100644 android/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 android/app/src/main/res/drawable/launch_background.xml create mode 100644 android/app/src/main/res/drawable/server.xml create mode 100644 android/app/src/main/res/drawable/server2.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 android/app/src/main/res/values-night/styles.xml create mode 100644 android/app/src/main/res/values/ic_launcher_background.xml create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/app/src/main/res/values/themes.xml create mode 100644 android/app/src/main/res/xml/backup_rules.xml create mode 100644 android/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 android/app/src/main/res/xml/file_path_data.xml create mode 100644 android/app/src/main/res/xml/file_paths.xml create mode 100644 android/app/src/main/res/xml/network_security_config.xml create mode 100644 android/app/src/profile/AndroidManifest.xml create mode 100644 android/build.gradle create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/settings.gradle create mode 100644 android/src/main/java/com/github/jing332/pigeon/Api.java create mode 100644 android/utils/.gitignore create mode 100644 android/utils/build.gradle.kts create mode 100644 android/utils/consumer-rules.pro create mode 100644 android/utils/proguard-rules.pro create mode 100644 android/utils/src/androidTest/java/com/github/jing332/utils/ExampleInstrumentedTest.kt create mode 100644 android/utils/src/main/AndroidManifest.xml create mode 100644 android/utils/src/main/cpp/CMakeLists.txt create mode 100644 android/utils/src/main/cpp/utils.cpp create mode 100644 android/utils/src/main/java/com/github/jing332/utils/NativeLib.kt create mode 100644 android/utils/src/test/java/com/github/jing332/utils/ExampleUnitTest.kt create mode 100644 lib/bridges/app_config.dart create mode 100644 lib/bridges/bridge.dart create mode 100644 lib/bridges/event.dart create mode 100644 lib/generated_api.dart create mode 100644 lib/main.dart create mode 100644 lib/pages/alist.dart create mode 100644 lib/pages/settings.dart create mode 100644 lib/pages/web.dart create mode 100644 lib/router.dart create mode 100644 lib/widgets/switch_floating_action_button.dart create mode 100644 pigeons/pigeon.dart create mode 100644 pigeons/run.cmd create mode 100644 pubspec.lock create mode 100644 pubspec.yaml create mode 100644 test/widget_test.dart diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..916dcfa --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + + + +local.properties + diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..52b125f --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ef1af02aead6fe2414f3aafa5a61087b610e1332" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 + base_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 + - platform: android + create_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 + base_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..bac1367 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# alist_flutter + +AList for Android + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..2f81e04 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,26 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks + +/alist-lib/* +alist-main +!alist-lib/alistlib +!alist-lib/scripts + +*.aar +*.exe +*.tgz +*.jar +*.zip +*.so + diff --git a/android/alist-lib/scripts/clear.sh b/android/alist-lib/scripts/clear.sh new file mode 100644 index 0000000..dc7d0d3 --- /dev/null +++ b/android/alist-lib/scripts/clear.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +mv androidlib /tmp +mv scripts /tmp + +rm -rf * + +mv /tmp/androidlib ./ +mv /tmp/scripts ./ + diff --git a/android/alist-lib/scripts/init_alist_core.sh b/android/alist-lib/scripts/init_alist_core.sh new file mode 100644 index 0000000..71e1a04 --- /dev/null +++ b/android/alist-lib/scripts/init_alist_core.sh @@ -0,0 +1,13 @@ +TAG_NAME=$(curl -s https://api.github.com/repos/alist-org/alist/releases/latest | grep -o '"tag_name": ".*"' | cut -d'"' -f4) + +URL="https://github.com/alist-org/alist/archive/refs/tags/${TAG_NAME}.tar.gz" +echo "Downloading alist ${TAG_NAME} from ${URL}" + +curl -L -o "alist.tgz" $URL +tar xf "alist.tgz" --strip-components 1 -C ../ + +echo "Write version to local.properties" +cd ../../ +touch local.properties +sed -i '/ALIST_VERSION/d' local.properties +echo "ALIST_VERSION=${TAG_NAME}" >> local.properties diff --git a/android/alist-lib/scripts/init_alist_web.sh b/android/alist-lib/scripts/init_alist_web.sh new file mode 100644 index 0000000..cbb4cac --- /dev/null +++ b/android/alist-lib/scripts/init_alist_web.sh @@ -0,0 +1,5 @@ +curl -L https://github.com/alist-org/alist-web/releases/latest/download/dist.tar.gz -o dist.tar.gz +tar -zxvf dist.tar.gz +rm -rf ../public/dist +mv -f dist ../public +rm -rf dist.tar.gz \ No newline at end of file diff --git a/android/alist-lib/scripts/install_alist.sh b/android/alist-lib/scripts/install_alist.sh new file mode 100644 index 0000000..a76af06 --- /dev/null +++ b/android/alist-lib/scripts/install_alist.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +export dir=$PWD +function build() { + echo "Building $1 $2 ${PWD}" + + export CGO_ENABLED=1 + export GOOS=android + export GOARCH="$1" + + FN="libalist.so" + rm -f ${FN} + + go build -ldflags "-s -w" -o ${FN} + + mkdir -p ${dir}/../app/libs/$2 + cp -f ${FN} ${dir}/../app/libs/$2 +} + +#cp -f ./frp-*/conf/* ../app/src/main/assets/defaultData + +build $1 $2 + +# function build_all() { +# rm -f $1 +# build $1 "arm" "armeabi-v7a" +# build $1 "arm64" "arm64-v8a" +# build $1 "386" "x86" +# build $1 "amd64" "x86_64" +# } + +# cd frp-*/cmd +# cd ./frpc +# build_all frpc + +# cd ./frps +# build_all frps diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..b9b95ff --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,111 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "kotlin-parcelize" + id "kotlinx-serialization" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + + +android { + namespace "com.github.jing332.alistflutter" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.github.jing332.alistflutter" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + + kotlinOptions { + jvmTarget = '17' + } + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + java.srcDirs = ['src/main/java', 'src/main/kotlin'] + } + } + +// kotlin { +// jvmToolchain = 17 +// } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + + debug { + ndk { + //noinspection ChromeOsAbiSupport + abiFilters "arm64-v8a" + } + } + } +} + +flutter { + source '../..' +} +////获取flutter的sdk路径 +//def flutterRoot = localProperties.getProperty('flutter.sdk') +//if (flutterRoot == null) { +// throw new Exception("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +//} +dependencies { +// compileOnly files("$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar") + + //noinspection GradleDependency + implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' + + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' + + implementation 'com.louiscad.splitties:splitties-systemservices:3.0.0' + + implementation 'com.github.cioccarellia:ksprefs:2.4.0' + + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + + +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..28e38fb --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/AListService.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/AListService.kt new file mode 100644 index 0000000..e4f8d22 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/AListService.kt @@ -0,0 +1,223 @@ +package com.github.jing332.alistflutter + +import android.annotation.SuppressLint +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.IBinder +import android.os.PowerManager +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.github.jing332.alistflutter.model.alist.AList +import com.github.jing332.alistflutter.BridgeUtils.invokeMethodSync +import com.github.jing332.alistflutter.config.AppConfig +import com.github.jing332.alistflutter.utils.AndroidUtils.registerReceiverCompat +import com.github.jing332.alistflutter.utils.ClipboardUtils +import com.github.jing332.alistflutter.utils.ToastUtils.toast +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import splitties.systemservices.powerManager + +class AListService : Service() { + companion object { + const val TAG = "AlistService" + const val ACTION_SHUTDOWN = + "com.github.jing332.alistandroid.service.AlistService.ACTION_SHUTDOWN" + + const val ACTION_COPY_ADDRESS = + "com.github.jing332.alistandroid.service.AlistService.ACTION_COPY_ADDRESS" + + const val ACTION_STATUS_CHANGED = + "com.github.jing332.alistandroid.service.AlistService.ACTION_STATUS_CHANGED" + + const val NOTIFICATION_CHAN_ID = "alist_server" + const val FOREGROUND_ID = 5224 + + var isRunning: Boolean = false + } + + private val mScope = CoroutineScope(Job()) + private val mNotificationReceiver = NotificationActionReceiver() + private val mReceiver = MyReceiver() + private var mWakeLock: PowerManager.WakeLock? = null + + override fun onBind(p0: Intent?): IBinder? = null + + private fun notifyStatusChanged() { + LocalBroadcastManager.getInstance(this) + .sendBroadcast(Intent(ACTION_STATUS_CHANGED)) + } + + @SuppressLint("WakelockTimeout") + override fun onCreate() { + super.onCreate() + + initNotification() + + if (AppConfig.isWakeLockEnabled) { + mWakeLock = powerManager.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, + "alist::service" + ) + mWakeLock?.acquire() + } + + LocalBroadcastManager.getInstance(this) + .registerReceiver(mReceiver, IntentFilter(ACTION_STATUS_CHANGED)) + registerReceiverCompat( + mNotificationReceiver, + ACTION_SHUTDOWN, + ACTION_COPY_ADDRESS + ) + } + + + @Suppress("DEPRECATION") + override fun onDestroy() { + super.onDestroy() + + mWakeLock?.release() + mWakeLock = null + + stopForeground(true) + + LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver) + unregisterReceiver(mNotificationReceiver) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (isRunning) { + AList.shutdown() + } else { + toast(getString(R.string.starting)) + isRunning = true + notifyStatusChanged() + mScope.launch(Dispatchers.IO) { + val ret = AList.startup() + isRunning = false + withContext(Dispatchers.Main) { + toast(getString(R.string.shutdowned, ret.toString())) + notifyStatusChanged() + } + } + } + + return super.onStartCommand(intent, flags, startId) + } + + @Suppress("DEPRECATION") + inner class MyReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == ACTION_STATUS_CHANGED) { + if (!isRunning) { + stopForeground(true) + stopSelf() + } + } + } + } + + private fun httpAddress(): String = runBlocking { + val channel = BridgeUtils.getMethodChannel(this@AListService) + +// return@runBlocking when (val ret = channel.invokeMethodSync("getHttpAddress", null)) { +// is BridgeUtils.MethodChannelResult.Success -> ret.result as String +// else -> "" +// } + return@runBlocking "" + } + + @Suppress("DEPRECATION") + private fun initNotification() { + // Android 12(S)+ 必须指定PendingIntent.FLAG_ + val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + PendingIntent.FLAG_IMMUTABLE + else + 0 + + /*点击通知跳转*/ + val pendingIntent = + PendingIntent.getActivity( + this, 0, Intent( + this, + MainActivity::class.java + ), + pendingIntentFlags + ) + /*当点击退出按钮时发送广播*/ + val shutdownAction: PendingIntent = + PendingIntent.getBroadcast( + this, + 0, + Intent(ACTION_SHUTDOWN), + pendingIntentFlags + ) + val copyAddressPendingIntent = + PendingIntent.getBroadcast( + this, + 0, + Intent(ACTION_COPY_ADDRESS), + pendingIntentFlags + ) + +// val color = com.github.jing332.alistandroid.ui.theme.seed.androidColor + val smallIconRes: Int + val builder = Notification.Builder(applicationContext) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {/*Android 8.0+ 要求必须设置通知信道*/ + val chan = NotificationChannel( + NOTIFICATION_CHAN_ID, + getString(R.string.alist_server), + NotificationManager.IMPORTANCE_NONE + ) +// chan.lightColor = color + chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + service.createNotificationChannel(chan) + smallIconRes = when ((0..1).random()) { + 0 -> R.drawable.server + 1 -> R.drawable.server2 + else -> R.drawable.server2 + } + + builder.setChannelId(NOTIFICATION_CHAN_ID) + } else { + smallIconRes = R.mipmap.ic_launcher_round + } + val notification = builder +// .setColor(color) + .setContentTitle(getString(R.string.alist_server_running)) + .setContentText(httpAddress()) + .setSmallIcon(smallIconRes) + .setContentIntent(pendingIntent) + .addAction(0, getString(R.string.shutdown), shutdownAction) + .addAction(0, getString(R.string.copy_address), copyAddressPendingIntent) + .build() + + // 前台服务 + startForeground(FOREGROUND_ID, notification) + } + + inner class NotificationActionReceiver : BroadcastReceiver() { + override fun onReceive(ctx: Context?, intent: Intent?) { + when (intent?.action) { +// ACTION_SHUTDOWN -> /*AList.shutdown()*/ + + ACTION_COPY_ADDRESS -> { + ClipboardUtils.copyText("AList", httpAddress()) + toast(R.string.address_copied) + } + } + } + } + +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/App.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/App.kt new file mode 100644 index 0000000..a27d4c6 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/App.kt @@ -0,0 +1,21 @@ +package com.github.jing332.alistflutter + +import android.app.Application +import com.github.jing332.alistflutter.utils.ToastUtils.longToast +import io.flutter.app.FlutterApplication + +val app by lazy { App.app } + +class App : FlutterApplication() { + companion object { + lateinit var app: Application + } + + + override fun onCreate() { + super.onCreate() + + app = this + longToast("init!") + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/BridgeUtils.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/BridgeUtils.kt new file mode 100644 index 0000000..4142113 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/BridgeUtils.kt @@ -0,0 +1,69 @@ +package com.github.jing332.alistflutter + +import android.content.Context +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint +import io.flutter.plugin.common.MethodChannel +import io.flutter.view.FlutterMain +import kotlinx.coroutines.Job +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.job +import kotlinx.coroutines.launch + + +object BridgeUtils { + private val CHANNEL = "main" + + fun getMethodChannel(context: Context): MethodChannel { + FlutterMain.startInitialization(context) + FlutterMain.ensureInitializationComplete(context, arrayOfNulls(0)) + val engine = FlutterEngine(context.applicationContext) + val entrypoint = DartEntrypoint("lib/main.dart", "widget") + engine.dartExecutor.executeDartEntrypoint(entrypoint) + + return MethodChannel(engine.dartExecutor.binaryMessenger, CHANNEL) + } + + suspend fun MethodChannel.invokeMethodSync( + method: String, + arguments: Any? = null + ): MethodChannelResult = coroutineScope { + var result: MethodChannelResult? = null + var waitJob: Job? = null + + invokeMethod( + method, arguments, + object : MethodChannel.Result { + override fun success(r: Any?) { + result = MethodChannelResult.Success(r) + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + result = MethodChannelResult.Error(errorCode, errorMessage, errorDetails) + } + + override fun notImplemented() { + result = MethodChannelResult.NotImplemented + } + } + ) + + + waitJob = launch { + awaitCancellation() + }.job + waitJob.start() + + + return@coroutineScope result!! + } + + sealed class MethodChannelResult { + data class Success(val result: Any?) : MethodChannelResult() + data class Error(val errorCode: String, val errorMessage: String?, val errorDetails: Any?) : + MethodChannelResult() + + object NotImplemented : MethodChannelResult() + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/MainActivity.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/MainActivity.kt new file mode 100644 index 0000000..540bc86 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/MainActivity.kt @@ -0,0 +1,106 @@ +package com.github.jing332.alistflutter + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import android.util.Log +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.github.jing332.alistflutter.config.AppConfig +import com.github.jing332.alistflutter.model.ShortCuts +import io.flutter.embedding.android.FlutterActivity +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugins.GeneratedPluginRegistrant + +class MainActivity : FlutterActivity(), MethodChannel.MethodCallHandler, + EventChannel.StreamHandler { + companion object { + private val TAG = "MainActivity" + const val BRIDGE_CHANNEL = "alistflutter/bridge" + const val CONFIG_CHANNEL = "alistflutter/config" + + const val EVENT_CHANNEL = "alistflutter/event" + } + + private val receiver by lazy { MyReceiver() } + private var mEventSink: EventChannel.EventSink? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + ShortCuts.buildShortCuts(this) + LocalBroadcastManager.getInstance(this) + .registerReceiver(receiver, IntentFilter(AListService.ACTION_STATUS_CHANGED)) + + GeneratedPluginRegistrant.registerWith(this.flutterEngine!!) + + MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger, BRIDGE_CHANNEL) + .setMethodCallHandler(this) + + MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger, CONFIG_CHANNEL) + .setMethodCallHandler { call, result -> + when (call.method) { + "isWakeLockEnabled" -> result.success(AppConfig.isWakeLockEnabled) + "setWakeLockEnabled" -> { + AppConfig.isWakeLockEnabled = call.arguments as Boolean + result.success(null) + } + + "isStartAtBootEnabled" -> result.success(AppConfig.isStartAtBootEnabled) + "setStartAtBootEnabled" -> { + AppConfig.isStartAtBootEnabled = call.arguments as Boolean + result.success(null) + } + + "isAutoCheckUpdateEnabled" -> result.success(AppConfig.isAutoCheckUpdateEnabled) + "setAutoCheckUpdateEnabled" -> { + AppConfig.isAutoCheckUpdateEnabled = call.arguments as Boolean + result.success(null) + } + } + } + EventChannel(flutterEngine!!.dartExecutor.binaryMessenger, EVENT_CHANNEL) + .setStreamHandler(this) + } + + override fun onDestroy() { + super.onDestroy() + + LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) + } + + + inner class MyReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == AListService.ACTION_STATUS_CHANGED) { + mEventSink?.success(AListService.isRunning) + } + } + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "startService" -> { + Log.d(TAG, "startService") + startService(Intent(this, AListService::class.java)) + result.success(null) + } + + "isRunning" -> result.success(AListService.isRunning) + + else -> result.notImplemented() + } + } + + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + mEventSink = events + } + + override fun onCancel(arguments: Any?) { + + } + +} diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/SwitchServerActivity.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/SwitchServerActivity.kt new file mode 100644 index 0000000..7f3813f --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/SwitchServerActivity.kt @@ -0,0 +1,24 @@ +package com.github.jing332.alistflutter + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import com.github.jing332.alistflutter.utils.ToastUtils.toast + +class SwitchServerActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (AListService.isRunning) { + toast(R.string.alist_shut_downing) + startService(Intent(this, AListService::class.java).apply { + action = AListService.ACTION_SHUTDOWN + }) + } else { + toast(R.string.alist_starting) + startService(Intent(this, AListService::class.java)) + } + + finish() + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/config/AppConfig.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/config/AppConfig.kt new file mode 100644 index 0000000..1baad24 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/config/AppConfig.kt @@ -0,0 +1,18 @@ +package com.github.jing332.alistflutter.config + +import android.content.Context +import com.cioccarellia.ksprefs.KsPrefs +import com.cioccarellia.ksprefs.dynamic +import com.github.jing332.alistflutter.app + +object AppConfig { + val prefs by lazy { KsPrefs(app, "app") } + + var isWakeLockEnabled: Boolean by prefs.dynamic("isWakeLockEnabled", fallback = false) + var isStartAtBootEnabled: Boolean by prefs.dynamic("isStartAtBootEnabled", fallback = false) + var isAutoCheckUpdateEnabled: Boolean by prefs.dynamic( + "isAutoCheckUpdateEnabled", + fallback = false + ) + +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/constant/AppConst.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/constant/AppConst.kt new file mode 100644 index 0000000..e84f5cb --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/constant/AppConst.kt @@ -0,0 +1,23 @@ +package com.github.jing332.alistflutter.constant + +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.github.jing332.alistflutter.app +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json + +object AppConst { + @OptIn(ExperimentalSerializationApi::class) + val json = Json { + ignoreUnknownKeys = true + allowStructuredMapKeys = true + prettyPrint = true + isLenient = true + explicitNulls = false + } + + val localBroadcast by lazy { + LocalBroadcastManager.getInstance(app) + } + +// val fileProviderAuthor = BuildConfig.APPLICATION_ID + ".fileprovider" +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/ShortCuts.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/ShortCuts.kt new file mode 100644 index 0000000..f74f74d --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/ShortCuts.kt @@ -0,0 +1,40 @@ +package com.github.jing332.alistflutter.model + +import android.content.Context +import android.content.Intent +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import com.github.jing332.alistflutter.R +import com.github.jing332.alistflutter.SwitchServerActivity + + +object ShortCuts { + private inline fun buildIntent(context: Context): Intent { + val intent = Intent(context, T::class.java) + intent.action = Intent.ACTION_VIEW + return intent + } + + + private fun buildAlistSwitchShortCutInfo(context: Context): ShortcutInfoCompat { + val msSwitchIntent = buildIntent(context) + return ShortcutInfoCompat.Builder(context, "alist_switch") + .setShortLabel(context.getString(R.string.app_switch)) + .setLongLabel(context.getString(R.string.app_switch)) + .setIcon(IconCompat.createWithResource(context, R.drawable.alist_switch)) + .setIntent(msSwitchIntent) + .build() + } + + + fun buildShortCuts(context: Context) { + ShortcutManagerCompat.setDynamicShortcuts( + context, listOf( + buildAlistSwitchShortCutInfo(context), + ) + ) + } + + +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/UpdateResult.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/UpdateResult.kt new file mode 100644 index 0000000..edb15ba --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/UpdateResult.kt @@ -0,0 +1,11 @@ +package com.github.jing332.alistandroid.model + +data class UpdateResult( + val version: String = "", + val time: String = "", + val content: String = "", + val downloadUrl: String = "", + val size: Long = 0, +) { + fun hasUpdate() = version.isNotBlank() && downloadUrl.isNotBlank() +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AList.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AList.kt new file mode 100644 index 0000000..38391c4 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AList.kt @@ -0,0 +1,148 @@ +package com.github.jing332.alistflutter.model.alist + +import android.annotation.SuppressLint +import android.util.Log +import com.github.jing332.alistflutter.R +import com.github.jing332.alistflutter.app +import com.github.jing332.alistflutter.utils.FileUtils.readAllText +import com.github.jing332.alistflutter.utils.ToastUtils.longToast +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.io.File +import java.io.IOException +import kotlin.coroutines.coroutineContext + +object AList { + const val TAG = "AList" + + private val execPath by lazy { + context.applicationInfo.nativeLibraryDir + File.separator + "libalist.so" + } + + val context = app + + val dataPath: String + get() = context.getExternalFilesDir("data")?.absolutePath!! + + val configPath: String + get() = "$dataPath${File.separator}config.json" + + + fun setAdminPassword(pwd: String) { + val log = execWithParams( + redirect = true, + params = arrayOf("admin", "set", pwd, "--data", dataPath) + ).inputStream.readAllText() +// appDb.serverLogDao.insert(ServerLog(level = LogLevel.INFO, message = log.removeAnsiCodes())) + } + + + fun shutdown() { + runCatching { + mProcess?.destroy() + }.onFailure { + context.longToast(R.string.server_shutdown_failed, it.toString()) + } + } + + private var mProcess: Process? = null + + private suspend fun errorLogWatcher(onNewLine: (String) -> Unit) { + mProcess?.apply { + errorStream.bufferedReader().use { + while (coroutineContext.isActive) { + val line = it.readLine() ?: break + Log.d(TAG, "Process errorStream: $line") + onNewLine(line) + } + } + } + } + + private suspend fun logWatcher(onNewLine: (String) -> Unit) { + mProcess?.apply { + inputStream.bufferedReader().use { + while (coroutineContext.isActive) { + val line = it.readLine() ?: break + Log.d(TAG, "Process inputStream: $line") + onNewLine(line) + } + } + } + } + + private val mScope = CoroutineScope(Dispatchers.IO + Job()) + private fun initOutput() { +// val dao = appDb.serverLogDao + mScope.launch { + runCatching { + logWatcher { msg -> +// msg.removeAnsiCodes().evalLog()?.let { +// dao.insert( +// ServerLog( +// level = it.level, +// message = it.message +// ) +// ) +// return@logWatcher +// } + +// dao.insert( +// ServerLog( +// level = if (msg.startsWith("fail")) LogLevel.ERROR else LogLevel.INFO, +// message = msg +// ) +// ) + + } + }.onFailure { + it.printStackTrace() + } + } + mScope.launch { + runCatching { + errorLogWatcher { msg -> +// val log = msg.removeAnsiCodes().evalLog() ?: return@errorLogWatcher +// dao.insert( +// ServerLog( +// level = log.level, +// message = log.message, +//// description = log.time + "\n" + log.code +// ) +// ) + } + }.onFailure { + it.printStackTrace() + } + } + } + + + @SuppressLint("SdCardPath") + fun startup( + dataFolder: String = context.getExternalFilesDir("data")?.absolutePath + ?: "/data/data/${context.packageName}/files/data" + ): Int { +// appDb.serverLogDao.deleteAll() + mProcess = + execWithParams(params = arrayOf("server", "--data", dataFolder)) + initOutput() + + return mProcess!!.waitFor() + } + + + private fun execWithParams( + redirect: Boolean = false, + vararg params: String + ): Process { + val cmdline = arrayOfNulls(params.size + 1) + cmdline[0] = execPath + System.arraycopy(params, 0, cmdline, 1, params.size) + return ProcessBuilder(*cmdline).redirectErrorStream(redirect).start() + ?: throw IOException("Process is null!") + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AListConfig.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AListConfig.kt new file mode 100644 index 0000000..758bf83 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AListConfig.kt @@ -0,0 +1,93 @@ +package com.github.jing332.alistflutter.model.alist + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +data class AListConfig( + @SerialName("bleve_dir") + val bleveDir: String = "", // /storage/emulated/0/Android/data/com.github.jing332.alistandroid.debug/files/data/bleve + @SerialName("cdn") + val cdn: String = "", +// @SerialName("database") +// val database: Database = Database(), + @SerialName("delayed_start") + val delayedStart: Int = 0, // 0 + @SerialName("force") + val force: Boolean = false, // false + @SerialName("jwt_secret") + val jwtSecret: String = "", // +// @SerialName("log") +// val log: Log = Log(), + @SerialName("max_connections") + val maxConnections: Int = 0, // 0 + @SerialName("scheme") + val scheme: Scheme = Scheme(), + @SerialName("site_url") + val siteUrl: String = "", + @SerialName("temp_dir") + val tempDir: String = "", // /storage/emulated/0/Android/data/com.github.jing332.alistandroid.debug/files/data/temp + @SerialName("tls_insecure_skip_verify") + val tlsInsecureSkipVerify: Boolean = true, // true + @SerialName("token_expires_in") + val tokenExpiresIn: Int = 48 // 48 +) { + @Serializable + data class Database( + @SerialName("db_file") + val dbFile: String = "", // /storage/emulated/0/Android/data/com.github.jing332.alistandroid.debug/files/data/data.db + @SerialName("host") + val host: String = "", + @SerialName("name") + val name: String = "", + @SerialName("password") + val password: String = "", + @SerialName("port") + val port: Int = 0, // 0 + @SerialName("ssl_mode") + val sslMode: String = "", + @SerialName("table_prefix") + val tablePrefix: String = "x_", // x_ + @SerialName("type") + val type: String = "sqlite3", // sqlite3 + @SerialName("user") + val user: String = "" + ) + + @Serializable + data class Log( + @SerialName("compress") + val compress: Boolean = false, // false + @SerialName("enable") + val enable: Boolean = true, // true + @SerialName("max_age") + val maxAge: Int = 28, // 28 + @SerialName("max_backups") + val maxBackups: Int = 5, // 5 + @SerialName("max_size") + val maxSize: Int = 10, // 10 + @SerialName("name") + val name: String = "" // /storage/emulated/0/Android/data/com.github.jing332.alistandroid.debug/files/data/log/log.log + ) + + @Serializable + data class Scheme( + @SerialName("address") + val address: String = "0.0.0.0", // 0.0.0.0 + @SerialName("cert_file") + val certFile: String = "", + @SerialName("force_https") + val forceHttps: Boolean = false, // false + @SerialName("http_port") + val httpPort: Int = 5244, // 5244 + @SerialName("https_port") + val httpsPort: Int = -1, // -1 + @SerialName("key_file") + val keyFile: String = "", + @SerialName("unix_file") + val unixFile: String = "", + @SerialName("unix_file_perm") + val unixFilePerm: String = "" + ) +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AListConfigManager.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AListConfigManager.kt new file mode 100644 index 0000000..29e9c00 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AListConfigManager.kt @@ -0,0 +1,74 @@ +package com.github.jing332.alistflutter.model.alist + +import android.os.FileObserver +import android.util.Log +import com.github.jing332.alistflutter.app +import com.github.jing332.alistflutter.constant.AppConst +import com.github.jing332.alistflutter.utils.ToastUtils.longToast +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.encodeToStream +import java.io.File + +@Suppress("DEPRECATION") +object AListConfigManager { + const val TAG = "AListConfigManager" + + val context + get() = app + + suspend fun flowConfig(): Flow = channelFlow { + val obs = object : FileObserver(AList.configPath) { + override fun onEvent(event: Int, p1: String?) { + if (listOf(CLOSE_NOWRITE, CLOSE_WRITE).contains(event)) + runBlocking { + Log.d(TAG, "config.json changed: $event") + send((config())) + } + } + } + coroutineScope { + val waitJob = launch { + obs.startWatching() + try { + awaitCancellation() + } catch (_: CancellationException) { + } + + obs.stopWatching() + } + waitJob.join() + } + } + + @OptIn(ExperimentalSerializationApi::class) + fun config(): AListConfig { + try { + File(AList.configPath).inputStream().use { + return AppConst.json.decodeFromStream(it) + } + } catch (e: Exception) { + AList.context.longToast("读取 config.json 失败:\n$e") + return AListConfig() + } + } + + @OptIn(ExperimentalSerializationApi::class) + fun update(cfg: AListConfig) { + try { + File(AList.configPath).outputStream().use { + AppConst.json.encodeToStream(cfg, it) + } + } catch (e: Exception) { + AList.context.longToast("更新 config.json 失败:\n$e") + } + } + +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/AndroidUtils.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/AndroidUtils.kt new file mode 100644 index 0000000..458ddc9 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/AndroidUtils.kt @@ -0,0 +1,22 @@ +package com.github.jing332.alistflutter.utils + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Context.RECEIVER_EXPORTED +import android.content.IntentFilter +import android.os.Build + +object AndroidUtils { + fun Context.registerReceiverCompat( + receiver: BroadcastReceiver, + vararg actions: String + ) { + val intentFilter = IntentFilter() + actions.forEach { intentFilter.addAction(it) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(receiver, intentFilter, RECEIVER_EXPORTED) + } else { + registerReceiver(receiver, intentFilter) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/ClipBoardUtils.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/ClipBoardUtils.kt new file mode 100644 index 0000000..82002ee --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/ClipBoardUtils.kt @@ -0,0 +1,97 @@ +package com.github.jing332.alistflutter.utils + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.ClipboardManager.OnPrimaryClipChangedListener +import android.content.Context +import com.github.jing332.alistflutter.app + + +/** + *
+ * author: Blankj
+ * blog  : http://blankj.com
+ * time  : 2016/09/25
+ * desc  : utils about clipboard
+
* + */ +object ClipboardUtils { + + /** + * Copy the text to clipboard. + * + * The label equals name of package. + * + * @param text The text. + */ + fun copyText(text: CharSequence?) { + val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.setPrimaryClip(ClipData.newPlainText(app.getPackageName(), text)) + } + + /** + * Copy the text to clipboard. + * + * @param label The label. + * @param text The text. + */ + fun copyText(label: CharSequence?, text: CharSequence?) { + val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.setPrimaryClip(ClipData.newPlainText(label, text)) + } + + /** + * Clear the clipboard. + */ + fun clear() { + val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.setPrimaryClip(ClipData.newPlainText(null, "")) + } + + /** + * Return the label for clipboard. + * + * @return the label for clipboard + */ + fun getLabel(): CharSequence { + val cm = app + .getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val des = cm.primaryClipDescription ?: return "" + return des.label ?: return "" + } + + /** + * Return the text for clipboard. + * + * @return the text for clipboard + */ + val text: CharSequence + get() { + val cm = + app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = cm.primaryClip + if (clip != null && clip.itemCount > 0) { + val text = clip.getItemAt(0).coerceToText(app) + if (text != null) { + return text + } + } + return "" + } + + /** + * Add the clipboard changed listener. + */ + fun addChangedListener(listener: OnPrimaryClipChangedListener?) { + val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.addPrimaryClipChangedListener(listener) + } + + /** + * Remove the clipboard changed listener. + */ + fun removeChangedListener(listener: OnPrimaryClipChangedListener?) { + val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.removePrimaryClipChangedListener(listener) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/FileUtils.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/FileUtils.kt new file mode 100644 index 0000000..a219acc --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/FileUtils.kt @@ -0,0 +1,42 @@ +package com.github.jing332.alistflutter.utils + +import java.io.File +import java.io.InputStream +import java.net.URLConnection + +object FileUtils { + val File.mimeType: String? + get() { + val fileNameMap = URLConnection.getFileNameMap() + return fileNameMap.getContentTypeFor(name) + } + + /** + * 按行读取txt + */ + fun InputStream.readAllText(): String { + val bufferedReader = this.bufferedReader() + val buffer = StringBuffer("") + var str: String? + while (bufferedReader.readLine().also { str = it } != null) { + buffer.append(str) + buffer.append("\n") + } + return buffer.toString() + } + + fun copyFolder(src: File, target: File, overwrite: Boolean = true) { + val folder = File(target.absolutePath + File.separator + src.name) + folder.mkdirs() + + src.listFiles()?.forEach { + if (it.isFile) { + val newFile = File(folder.absolutePath + File.separator + it.name) + it.copyTo(newFile, overwrite) + } else if (it.isDirectory) { + copyFolder(it, folder) + } + } + + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/StringUtils.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/StringUtils.kt new file mode 100644 index 0000000..6e4836a --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/StringUtils.kt @@ -0,0 +1,40 @@ +package com.github.jing332.alistflutter.utils + +object StringUtils { + private fun paramsParseInternal(params: String): HashMap { + val parameters: HashMap = hashMapOf() + if (params.isBlank()) return parameters + + for (param in params.split("&")) { + val entry = param.split("=".toRegex()).dropLastWhile { it.isEmpty() } + if (entry.size > 1) { + parameters[entry[0]] = entry[1] + } else { + parameters[entry[0]] = "" + } + } + return parameters + } + + fun String.paramsParse() = paramsParseInternal(this) + + fun String.toNumberInt(): Int { + return this.replace(Regex("[^0-9]"), "").toIntOrNull() ?: 0 + } + + fun String.removeAnsiCodes(): String { + val ansiRegex = Regex("\\x1B\\[[0-9;]*[m|K]") + return this.replace(ansiRegex, "") + } + + fun String.parseToMap(): Map { + return this.split(";").associate { + val ss = it.trim().split("=") + if (ss.size != 2) return@associate "" to "" + + val key = ss[0] + val value = ss[1] + key.trim() to value.trim() + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/ToastUtils.kt b/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/ToastUtils.kt new file mode 100644 index 0000000..0517dd6 --- /dev/null +++ b/android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/ToastUtils.kt @@ -0,0 +1,56 @@ +package com.github.jing332.alistflutter.utils + +import android.content.Context +import android.widget.Toast +import androidx.annotation.StringRes +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +object ToastUtils { + @OptIn(DelicateCoroutinesApi::class) + fun runMain(block: () -> Unit) { + GlobalScope.launch(Dispatchers.Main) { + block() + } + } + + fun Context.toast(str: String) { + runMain { + Toast.makeText(this, str, Toast.LENGTH_SHORT).show() + } + } + + fun Context.toast(@StringRes strId: Int, vararg args: Any) { + runMain { + Toast.makeText( + this, + getString(strId, *args), + Toast.LENGTH_SHORT + ).show() + } + } + + fun Context.longToast(str: String) { + runMain { + Toast.makeText(this, str, Toast.LENGTH_LONG).show() + } + } + + fun Context.longToast(@StringRes strId: Int) { + runMain { + Toast.makeText(this, strId, Toast.LENGTH_LONG).show() + } + } + + fun Context.longToast(@StringRes strId: Int, vararg args: Any) { + runMain { + Toast.makeText( + this, + getString(strId, *args), + Toast.LENGTH_LONG + ).show() + } + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/alist_logo.xml b/android/app/src/main/res/drawable/alist_logo.xml new file mode 100644 index 0000000..0cea76b --- /dev/null +++ b/android/app/src/main/res/drawable/alist_logo.xml @@ -0,0 +1,6 @@ + + + + diff --git a/android/app/src/main/res/drawable/alist_switch.xml b/android/app/src/main/res/drawable/alist_switch.xml new file mode 100644 index 0000000..91c57e5 --- /dev/null +++ b/android/app/src/main/res/drawable/alist_switch.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_female.xml b/android/app/src/main/res/drawable/ic_female.xml new file mode 100644 index 0000000..fbc5cee --- /dev/null +++ b/android/app/src/main/res/drawable/ic_female.xml @@ -0,0 +1,12 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..27d37e5 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/server.xml b/android/app/src/main/res/drawable/server.xml new file mode 100644 index 0000000..fcb3879 --- /dev/null +++ b/android/app/src/main/res/drawable/server.xml @@ -0,0 +1,5 @@ + + + diff --git a/android/app/src/main/res/drawable/server2.xml b/android/app/src/main/res/drawable/server2.xml new file mode 100644 index 0000000..5855567 --- /dev/null +++ b/android/app/src/main/res/drawable/server2.xml @@ -0,0 +1,5 @@ + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..7353dbd --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..7353dbd --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..bc9dc02812ebbb2b51ce7dcc485c7ca052cf1528 GIT binary patch literal 1278 zcmV zbtP%rcI?gmS5e#62;a}xwrzbF#qXC5)YhHnNNu}UtpRKS0Lw=C@Uw0E?ltIa+qP{R zxfFsV$)>G-e(&41ZR~$n)1&VH1i+DOTeWgVghpgP#pc8in1m1DXGrAkA*s9DyV)0~^5`8y> zp;>N3z>vZ;a0*0B*)&bxbWP9Ky1%|85;z26Y;*Q-GY9|Qc;{hT0;>%X@8V(h!}4bM zMx<=moXKS6W_6#X_Zde^RnDx47;SNx`%I&K^lEoKINoQnI)nDrwH?tt6D~(TP3xFL z&y-Ko#-(em`sR1{CT2Y|N@W3FNE%$xomqnepnY zSCS(nvGp}u{muRZFUKZ|lfTAKpwbo6?P_-bRqlw>n*=6g#Wprk1R%Z4N(tY-#%Fwn z94R4z*pezuZ%gMJ$X9#75S!XS-iJc1ugtLW5j^Cr>59uxw-bbrB-UFk&D>M#(~uF` z>SyC!LL)MAJW8py!YwF%sNV|5r8zc zRGxxxzq63;4R-Yn?)Owmx?OBJG_xHy?_-mRBy|=F$X9Y=Cf9t5GK+hVP-ctZA5wp3 zvE}sB%xXeAnjSLT0P4ee+p*3evo$+^J@sYHTI-MdaPzH8x9frlWJL5Sq^afbz}u7As?GnT_(Ia# z;eL-z74ndlFL{vM>8`X(L%qHV3Cyl?M*vc><0NH zIsKK~iFlM*94Ixrq=D{Y^XZq_r*ikcr1o6Bfrdy*3K$AC1nNWKPFJZCgpd~62T8}d z8#6of{t&Q%G(J;C;*damB1tb&oWY-wVY}i}zh)Z_zT0wZl4^Y6 zAwr>s0mHP*mCgXlEbdBEEEAA$Wnw1j-k!%-Gdn4~3i*LF)KiKW@S~?vGrk4hj11+* z#9V2CK?1X*P!WJs<=P z*Fd{9eSAJHu^tv#kH)4GO&{v(RBb5d{PuLnpin>+P-saI3m<{9 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..adab4e152a997ed0138434053e4713339b60f87f GIT binary patch literal 2636 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 KcqP1n{VM??=)0Z( literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..bc0455a37ba75bcb94772dbbf7f487d4512c5309 GIT binary patch literal 1708 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..73c5cde352733b480f83a4153670c5f6800bc1f1 GIT binary patch literal 3540 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 Q%88hV5c6P(SUqaS0IJT+4*&oF literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..3b65b4d --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,74 @@ + + + AList + + 开源许可 + 返回 + 未选择文件 + ❌ 错误 + 确定 + 描述 + 日志 + AList服务器 + 添加桌面快捷方式 + 关闭 + 复制地址 + AList运行中 + 取消 + admin 密码已设为:\n %1$s + admin 密码 + 关闭失败:%1$s + 已复制地址 + ⚠️启动服务器才可设置admin密码 + AList配置 + 设置 + 密码 + 启动中 + 关闭中 + 开关 + 更多选项 + 关于 + 监听地址 + 编辑 config.json + 请至少启用一个服务器! + AList提供者 + account + 路径已复制 + 检查更新 + 启动 + 所有文件访问权限 + 挂载本地存储时必须打开,否则无权限读写文件。 + 读取存储权限 + 写入存储权限 + 请求电池优化白名单 + 如果程序在后台运行时被系统杀死,可以尝试设置。 + 关闭 + 自动检查更新 + 打开程序主界面时从Github检查更新 + 唤醒锁 + 打开可防止锁屏后CPU休眠,但在部分系统可能会导致杀后台 + 重要设置 + 打开data文件夹 + 点按上方路径选择“MT管理器”打开data文件夹 + 网页 + 跳转失败: %1$s + 清空网页数据 + 清空网页缓存 + 清空网页数据库、Cookie、DomStorage。 + 仅清空资源缓存,不影响用户数据。 + 已清除 + 确定 + 自动打开网页界面 + 打开主界面时,自动跳转到网页界面。 + 下载文件 + 系统下载器 + 打开链接 + 已复制链接 + 启动中 + 已关闭: %1$s + 浏览器 + 选择下载器 + 上次使用 + 开机自启动服务 + 在开机时自动开启AList服务。 + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..26adcb6 --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,6 @@ + + +