├── .github ├── scripts │ ├── check_alist.sh │ └── lzy_web.py └── workflows │ ├── build.yaml │ ├── release.yaml │ └── sync_alist.yaml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── README.md ├── alist-lib ├── alistlib │ ├── common.go │ ├── internal │ │ └── log.go │ ├── server.go │ └── settings.go └── scripts │ ├── clear.sh │ ├── gobind.sh │ ├── init_alist.sh │ ├── init_gomobile.sh │ └── init_web.sh ├── alist_version ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── github │ │ │ │ └── jing332 │ │ │ │ └── pigeon │ │ │ │ └── GeneratedApi.java │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── github │ │ │ │ └── jing332 │ │ │ │ └── alistflutter │ │ │ │ ├── AListService.kt │ │ │ │ ├── App.kt │ │ │ │ ├── BootReceiver.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── SwitchServerActivity.kt │ │ │ │ ├── bridge │ │ │ │ ├── AndroidBridge.kt │ │ │ │ ├── AppConfigBridge.kt │ │ │ │ └── CommonBridge.kt │ │ │ │ ├── config │ │ │ │ └── AppConfig.kt │ │ │ │ ├── constant │ │ │ │ ├── AppConst.kt │ │ │ │ └── LogLevel.kt │ │ │ │ ├── data │ │ │ │ ├── AppDatabase.kt │ │ │ │ └── entities │ │ │ │ │ └── ServerLog.kt │ │ │ │ ├── model │ │ │ │ ├── ShortCuts.kt │ │ │ │ ├── UpdateResult.kt │ │ │ │ └── alist │ │ │ │ │ ├── AList.kt │ │ │ │ │ ├── AListConfig.kt │ │ │ │ │ ├── AListConfigManager.kt │ │ │ │ │ └── Logger.kt │ │ │ │ └── utils │ │ │ │ ├── AndroidUtils.kt │ │ │ │ ├── ClipBoardUtils.kt │ │ │ │ ├── FileUtils.kt │ │ │ │ ├── MyTools.kt │ │ │ │ ├── StringUtils.kt │ │ │ │ └── ToastUtils.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ ├── alist_logo.xml │ │ │ ├── alist_switch.xml │ │ │ ├── ic_female.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ ├── launch_background.xml │ │ │ ├── server.xml │ │ │ └── server2.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ ├── values │ │ │ ├── ic_launcher_background.xml │ │ │ ├── strings.xml │ │ │ ├── styles.xml │ │ │ └── themes.xml │ │ │ └── xml │ │ │ ├── backup_rules.xml │ │ │ ├── data_extraction_rules.xml │ │ │ ├── file_path_data.xml │ │ │ ├── file_paths.xml │ │ │ └── network_security_config.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── utils │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── github │ │ └── jing332 │ │ └── utils │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── cpp │ │ ├── CMakeLists.txt │ │ └── utils.cpp │ └── java │ │ └── com │ │ └── github │ │ └── jing332 │ │ └── utils │ │ └── NativeLib.kt │ └── test │ └── java │ └── com │ └── github │ └── jing332 │ └── utils │ └── ExampleUnitTest.kt ├── assets └── alist.svg ├── images └── alist.jpg ├── lib ├── contant │ ├── log_level.dart │ └── native_bridge.dart ├── generated │ ├── intl │ │ ├── messages_all.dart │ │ ├── messages_en.dart │ │ └── messages_zh.dart │ └── l10n.dart ├── generated_api.dart ├── l10n │ ├── intl_en.arb │ └── intl_zh.arb ├── main.dart ├── pages │ ├── alist │ │ ├── about_dialog.dart │ │ ├── alist.dart │ │ ├── log_level_view.dart │ │ ├── log_list_view.dart │ │ └── pwd_edit_dialog.dart │ ├── app_update_dialog.dart │ ├── settings │ │ ├── preference_widgets.dart │ │ └── settings.dart │ └── web │ │ └── web.dart ├── utils │ ├── intent_utils.dart │ └── update_checker.dart └── widgets │ └── switch_floating_action_button.dart ├── pigeons ├── pigeon.dart └── run.cmd ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.github/scripts/check_alist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | GIT_REPO="https://github.com/alist-org/alist.git" 4 | 5 | function to_int() { 6 | echo $(echo "$1" | grep -oE '[0-9]+' | tr -d '\n') 7 | } 8 | 9 | function get_latest_version() { 10 | echo $(git -c 'versionsort.suffix=-' ls-remote --exit-code --refs --sort='version:refname' --tags $GIT_REPO | tail --lines=1 | cut --delimiter='/' --fields=3) 11 | } 12 | 13 | LATEST_VER="" 14 | for index in $(seq 5) 15 | do 16 | echo "Try to get latest version, index=$index" 17 | LATEST_VER=$(get_latest_version) 18 | if [ -z "$LATEST_VER" ]; then 19 | if [ "$index" -ge 5 ]; then 20 | echo "Failed to get latest version, exit" 21 | exit 1 22 | fi 23 | echo "Failed to get latest version, sleep 15s and retry" 24 | sleep 15 25 | else 26 | break 27 | fi 28 | 29 | done 30 | 31 | LATEST_VER_INT=$(to_int "$LATEST_VER") 32 | echo "Latest AList version $LATEST_VER ${LATEST_VER_INT}" 33 | 34 | echo "alist_version=$LATEST_VER" >> "$GITHUB_ENV" 35 | # VERSION_FILE="$GITHUB_WORKSPACE/alist_version.txt" 36 | 37 | VER=$(cat "$VERSION_FILE") 38 | 39 | if [ -z "$VER" ]; then 40 | VER="v3.25.1" 41 | echo "No version file, use default version ${VER}" 42 | fi 43 | 44 | VER_INT=$(to_int $VER) 45 | 46 | echo "Current AList version: $VER ${VER_INT}" 47 | 48 | 49 | if [ "$VER_INT" -ge "$LATEST_VER_INT" ]; then 50 | echo "Current >= Latest" 51 | echo "alist_update=0" >> "$GITHUB_ENV" 52 | else 53 | echo "Current < Latest" 54 | echo "alist_update=1" >> "$GITHUB_ENV" 55 | fi 56 | -------------------------------------------------------------------------------- /.github/scripts/lzy_web.py: -------------------------------------------------------------------------------- 1 | import requests, os, datetime, sys 2 | 3 | # Cookie 中 phpdisk_info 的值 4 | cookie_phpdisk_info = os.environ.get('phpdisk_info') 5 | # Cookie 中 ylogin 的值 6 | cookie_ylogin = os.environ.get('ylogin') 7 | 8 | # 请求头 9 | headers = { 10 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36 Edg/89.0.774.45', 11 | 'Accept-Language': 'zh-CN,zh;q=0.9', 12 | 'Referer': 'https://pc.woozooo.com/account.php?action=login' 13 | } 14 | 15 | # 小饼干 16 | cookie = { 17 | 'ylogin': cookie_ylogin, 18 | 'phpdisk_info': cookie_phpdisk_info 19 | } 20 | 21 | 22 | # 日志打印 23 | def log(msg): 24 | utc_time = datetime.datetime.utcnow() 25 | china_time = utc_time + datetime.timedelta(hours=8) 26 | print(f"[{china_time.strftime('%Y.%m.%d %H:%M:%S')}] {msg}") 27 | 28 | 29 | # 检查是否已登录 30 | def login_by_cookie(): 31 | url_account = "https://pc.woozooo.com/account.php" 32 | if cookie['phpdisk_info'] is None: 33 | log('ERROR: 请指定 Cookie 中 phpdisk_info 的值!') 34 | return False 35 | if cookie['ylogin'] is None: 36 | log('ERROR: 请指定 Cookie 中 ylogin 的值!') 37 | return False 38 | res = requests.get(url_account, headers=headers, cookies=cookie, verify=True) 39 | if '网盘用户登录' in res.text: 40 | log('ERROR: 登录失败,请更新Cookie') 41 | return False 42 | else: 43 | log('登录成功') 44 | return True 45 | 46 | 47 | # 上传文件 48 | def upload_file(file_dir, folder_id): 49 | file_name = os.path.basename(file_dir) 50 | url_upload = "https://up.woozooo.com/fileup.php" 51 | headers['Referer'] = f'https://up.woozooo.com/mydisk.php?item=files&action=index&u={cookie_ylogin}' 52 | post_data = { 53 | "task": "1", 54 | "folder_id": folder_id, 55 | "id": "WU_FILE_0", 56 | "name": file_name, 57 | } 58 | files = {'upload_file': (file_name, open(file_dir, "rb"), 'application/octet-stream')} 59 | res = requests.post(url_upload, data=post_data, files=files, headers=headers, cookies=cookie, timeout=120).json() 60 | log(f"{file_dir} -> {res['info']}") 61 | return res['zt'] == 1 62 | 63 | 64 | # 上传文件夹内的文件 65 | def upload_folder(folder_dir, folder_id): 66 | file_list = sorted(os.listdir(folder_dir), reverse=True) 67 | for file in file_list: 68 | path = os.path.join(folder_dir, file) 69 | if os.path.isfile(path): 70 | upload_file(path, folder_id) 71 | else: 72 | upload_folder(path, folder_id) 73 | 74 | 75 | # 上传 76 | def upload(dir, folder_id): 77 | if dir is None: 78 | log('ERROR: 请指定上传的文件路径') 79 | return 80 | if folder_id is None: 81 | log('ERROR: 请指定蓝奏云的文件夹id') 82 | return 83 | if os.path.isfile(dir): 84 | upload_file(dir, str(folder_id)) 85 | else: 86 | upload_folder(dir, str(folder_id)) 87 | 88 | 89 | if __name__ == '__main__': 90 | argv = sys.argv[1:] 91 | if len(argv) != 2: 92 | log('ERROR: 参数错误,请以这种格式重新尝试\npython lzy_web.py 需上传的路径 蓝奏云文件夹id') 93 | # 需上传的路径 94 | upload_path = argv[0] 95 | # 蓝奏云文件夹id 96 | lzy_folder_id = argv[1] 97 | if login_by_cookie(): 98 | upload(upload_path, lzy_folder_id) -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | paths-ignore: 8 | - "*.md" 9 | - "*.sh" 10 | - "release.yaml" 11 | # - "sync_frp.yaml" 12 | 13 | workflow_dispatch: 14 | 15 | jobs: 16 | android: 17 | runs-on: ubuntu-latest 18 | env: 19 | output: "${{ github.workspace }}/build/app/outputs/apk/release" 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Download AList Source Code 26 | run: | 27 | cd $GITHUB_WORKSPACE/alist-lib/scripts 28 | chmod +x *.sh 29 | ./init_alist.sh 30 | ./init_web.sh 31 | 32 | - uses: actions/setup-go@v4 33 | with: 34 | go-version: 1.22 35 | cache-dependency-path: ${{ github.workspace }}/alist-lib/alist/go.sum 36 | 37 | - uses: actions/setup-java@v3 38 | with: 39 | distribution: temurin 40 | java-version: 17 41 | 42 | - uses: nttld/setup-ndk@v1 43 | id: setup-ndk 44 | with: 45 | ndk-version: r25c 46 | 47 | - name: Setup Gradle 48 | uses: gradle/gradle-build-action@v2.4.2 49 | 50 | - name: Build AList 51 | run: | 52 | cd $GITHUB_WORKSPACE/alist-lib/scripts 53 | chmod +x *.sh 54 | ./init_gomobile.sh 55 | ./gobind.sh 56 | env: 57 | ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} 58 | 59 | - name: Upload AAR 60 | uses: actions/upload-artifact@v4 61 | with: 62 | name: "AList" 63 | path: "${{ github.workspace }}/android/app/libs/*.aar" 64 | 65 | - name: Init Signature 66 | run: | 67 | touch local.properties 68 | cd android 69 | echo ALIAS_NAME='${{ secrets.ALIAS_NAME }}' >> local.properties 70 | echo ALIAS_PASSWORD='${{ secrets.ALIAS_PASSWORD }}' >> local.properties 71 | echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' >> local.properties 72 | echo KEY_PATH='./key.jks' >> local.properties 73 | # 从Secrets读取无换行符Base64解码, 然后保存到到app/key.jks 74 | echo ${{ secrets.KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/android/app/key.jks 75 | 76 | - uses: subosito/flutter-action@v2 77 | with: 78 | flutter-version: '3.19.6' 79 | - run: flutter build apk --split-per-abi --release 80 | 81 | - name: Upload missing_rules.txt 82 | if: failure() && steps.gradle.outcome != 'success' 83 | uses: actions/upload-artifact@v4 84 | with: 85 | name: "missing_rules" 86 | path: "${{ github.workspace }}/build/app/outputs/mapping/release/missing_rules.txt" 87 | 88 | - name: Init APP Version Name 89 | run: | 90 | echo "ver_name=$(grep -m 1 'versionName' ${{ env.output }}/output-metadata.json | cut -d\" -f4)" >> $GITHUB_ENV 91 | 92 | - name: Upload App To Artifact arm64-v8a 93 | if: success () || failure () 94 | uses: actions/upload-artifact@v4 95 | with: 96 | name: "AListFlutter-v${{ env.ver_name }}_arm64-v8a" 97 | path: "${{ env.output }}/*-v8a.apk" 98 | 99 | - name: Upload App To Artifact arm-v7a 100 | if: success () || failure () 101 | uses: actions/upload-artifact@v4 102 | with: 103 | name: "AListFlutter-v${{ env.ver_name }}_arm-v7a" 104 | path: "${{ env.output }}/*-v7a.apk" 105 | 106 | - name: Upload App To Artifact x86 107 | if: success () || failure () 108 | uses: actions/upload-artifact@v4 109 | with: 110 | name: "AListFlutter-v${{ env.ver_name }}_x86_64" 111 | path: "${{ env.output }}/*64.apk" 112 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | paths: 8 | - "CHANGELOG.md" 9 | workflow_dispatch: 10 | 11 | jobs: 12 | android: 13 | runs-on: ubuntu-latest 14 | env: 15 | output: "${{ github.workspace }}/build/app/outputs/apk/release" 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Download AList Source Code 22 | run: | 23 | cd $GITHUB_WORKSPACE/alist-lib/scripts 24 | chmod +x *.sh 25 | ./init_alist.sh 26 | ./init_web.sh 27 | 28 | - uses: actions/setup-go@v5 29 | with: 30 | go-version: 1.22 31 | cache-dependency-path: ${{ github.workspace }}/alist-lib/alist/go.sum 32 | 33 | - uses: actions/setup-java@v4 34 | with: 35 | distribution: temurin 36 | java-version: 17 37 | 38 | - uses: nttld/setup-ndk@v1 39 | id: setup-ndk 40 | with: 41 | ndk-version: r25c 42 | 43 | - name: Setup Gradle 44 | uses: gradle/gradle-build-action@v3 45 | 46 | - name: Build AList 47 | run: | 48 | cd $GITHUB_WORKSPACE/alist-lib/scripts 49 | chmod +x *.sh 50 | ./init_gomobile.sh 51 | ./gobind.sh 52 | env: 53 | ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} 54 | 55 | - name: Upload AAR 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: "AList" 59 | path: "${{ github.workspace }}/android/app/libs/*.aar" 60 | 61 | - name: Init Signature 62 | run: | 63 | touch local.properties 64 | cd android 65 | echo ALIAS_NAME='${{ secrets.ALIAS_NAME }}' >> local.properties 66 | echo ALIAS_PASSWORD='${{ secrets.ALIAS_PASSWORD }}' >> local.properties 67 | echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' >> local.properties 68 | echo KEY_PATH='./key.jks' >> local.properties 69 | # 从Secrets读取无换行符Base64解码, 然后保存到到app/key.jks 70 | echo ${{ secrets.KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/android/app/key.jks 71 | 72 | - uses: subosito/flutter-action@v2 73 | with: 74 | flutter-version: '3.19.6' 75 | - run: flutter build apk --split-per-abi --release 76 | 77 | - name: Upload missing_rules.txt 78 | if: failure() && steps.gradle.outcome != 'success' 79 | uses: actions/upload-artifact@v4 80 | with: 81 | name: "missing_rules" 82 | path: "${{ github.workspace }}/build/app/outputs/mapping/release/missing_rules.txt" 83 | 84 | - name: Init APP Version Name 85 | run: | 86 | echo "ver_name=$(grep -m 1 'versionName' ${{ env.output }}/output-metadata.json | cut -d\" -f4)" >> $GITHUB_ENV 87 | 88 | - uses: softprops/action-gh-release@v1 89 | with: 90 | name: ${{ env.ver_name }} 91 | tag_name: ${{ env.ver_name }} 92 | body_path: ${{ github.workspace }}/CHANGELOG.md 93 | draft: false 94 | prerelease: false 95 | files: ${{ env.output }}/*.apk 96 | env: 97 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 98 | -------------------------------------------------------------------------------- /.github/workflows/sync_alist.yaml: -------------------------------------------------------------------------------- 1 | name: CheckAList 2 | 3 | on: 4 | schedule: 5 | - cron: "0 5,17 * * *" # 每日5点和17点执行 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - "master" 10 | paths: 11 | - "sync_alist.yaml" 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | env: 17 | VERSION_FILE: ${{ github.workspace }}/alist_version 18 | steps: 19 | - uses: actions/checkout@v3 20 | - run: | 21 | cd $GITHUB_WORKSPACE/.github/scripts 22 | chmod +x ./*.sh 23 | 24 | touch ${{ env.VERSION_FILE }} 25 | ./check_alist.sh 26 | 27 | - name: Shell 28 | run: | 29 | echo "alist_version=${{ env.alist_version }}" 30 | echo "alist_update=${{ env.alist_update }}" 31 | 32 | # 用于测试 33 | # echo "alist_update=1" >> $GITHUB_ENV 34 | 35 | if [ ${{ env.alist_update }} -eq 0 ] 36 | then 37 | echo "无更新" 38 | else 39 | echo -e "[自动同步AList] ${{ env.alist_version }}" > $GITHUB_WORKSPACE/CHANGELOG.md 40 | echo -e "${{ env.alist_version }}" > ${{ env.VERSION_FILE }} 41 | 42 | git config user.name "github-actions" 43 | git config user.email "42014615+jing332@users.noreply.github.com" 44 | git add . 45 | git commit -m "[bot] Update alist to ${{ env.alist_version }}" 46 | git push 47 | fi 48 | 49 | - name: Run workflow release 50 | if: env.alist_update == 1 && ( success() || failure() ) 51 | run: | 52 | gh workflow run release.yaml -R jing332/AListFlutter 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Symbolication related 35 | app.*.symbols 36 | 37 | # Obfuscation related 38 | app.*.map.json 39 | 40 | # Android Studio will place build artifacts here 41 | /android/app/debug 42 | /android/app/profile 43 | /android/app/release 44 | 45 | local.properties 46 | 47 | *.aar 48 | *.exe 49 | *.tgz 50 | *.jar 51 | *.zip 52 | *.so 53 | 54 | alist-lib 55 | !alist-lib/alistlib 56 | !alist-lib/scripts 57 | 58 | 59 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "ef1af02aead6fe2414f3aafa5a61087b610e1332" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 17 | base_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 18 | - platform: android 19 | create_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 20 | base_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | [自动同步AList] v3.45.0 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Release](https://github.com/jing332/AListFlutter/actions/workflows/release.yaml/badge.svg)](https://github.com/jing332/AListFlutter/actions/workflows/release.yaml) 2 | [![Test](https://github.com/jing332/AListFlutter/actions/workflows/build.yaml/badge.svg)](https://github.com/jing332/AListFlutter/actions/workflows/build.yaml) 3 | [![CheckAList](https://github.com/jing332/AListFlutter/actions/workflows/sync_alist.yaml/badge.svg)](https://github.com/jing332/AListFlutter/actions/workflows/sync_alist.yaml) 4 | 5 | #### 🚩 [FRP](https://github.com/fatedier/frp) 安卓版本:https://github.com/jing332/FrpAndroid 6 | #### 🚩 [AListAndroid](https://github.com/jing332/AlistAndroid) Compose版本,已停更 7 | 8 | # AListFlutter 9 | 10 | AListFlutter是一个基于AList的Android服务端,使用Google Flutter作为UI框架。 11 | 12 | > [Github Actions](https://github.com/jing332/AListFlutter/actions/workflows/sync_alist.yaml) 13 | > 每日早晚五点钟检查最新的 [AList](https://github.com/alist-org/alist/releases) 14 | > 并自动构建APK,发布到 [Release](https://github.com/jing332/AListFlutter/releases) 15 | > 中,您只需耐心等待片刻并在应用内检查更新即可。 16 | 17 | 18 | 19 | ### Bug 20 | - Android4.4闪退 https://github.com/jing332/AListFlutter/issues/5 21 | - 部分设备无法添加本地存储 https://github.com/jing332/AListFlutter/issues/2 22 | 23 | ### 关于IOS 24 | 理论上 [Gomobile](https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile?utm_source=godoc#hdr-Build_a_library_for_Android_and_iOS) 支持IOS,但由于本人无IOS相关开发设备,故无法支持。 25 | 26 | # Download 27 | 28 | - [Github Action (DEV)](https://github.com/jing332/AListFlutter/actions/workflows/build.yaml) 开发版 29 | 30 | -------------------------------------------------------------------------------- /alist-lib/alistlib/common.go: -------------------------------------------------------------------------------- 1 | package alistlib 2 | 3 | import "net" 4 | 5 | func GetOutboundIP() (net.IP, error) { 6 | conn, err := net.Dial("udp", "8.8.8.8:80") 7 | if err != nil { 8 | return nil, err 9 | } 10 | defer conn.Close() 11 | 12 | localAddr := conn.LocalAddr().(*net.UDPAddr) 13 | return localAddr.IP, nil 14 | } 15 | 16 | func GetOutboundIPString() string { 17 | netIp, err := GetOutboundIP() 18 | if err != nil { 19 | return "localhost" 20 | } 21 | 22 | return netIp.String() 23 | } 24 | 25 | -------------------------------------------------------------------------------- /alist-lib/alistlib/internal/log.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import log "github.com/sirupsen/logrus" 4 | 5 | type MyFormatter struct { 6 | log.Formatter 7 | OnLog func(entry *log.Entry) 8 | } 9 | 10 | func (f *MyFormatter) Format(entry *log.Entry) ([]byte, error) { 11 | f.OnLog(entry) 12 | return nil, nil 13 | } 14 | -------------------------------------------------------------------------------- /alist-lib/alistlib/server.go: -------------------------------------------------------------------------------- 1 | package alistlib 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "os" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/alist-org/alist/v3/alistlib/internal" 14 | "github.com/alist-org/alist/v3/cmd" 15 | "github.com/alist-org/alist/v3/cmd/flags" 16 | "github.com/alist-org/alist/v3/internal/bootstrap" 17 | "github.com/alist-org/alist/v3/internal/conf" 18 | "github.com/alist-org/alist/v3/pkg/utils" 19 | "github.com/alist-org/alist/v3/server" 20 | "github.com/gin-gonic/gin" 21 | log "github.com/sirupsen/logrus" 22 | ) 23 | 24 | type LogCallback interface { 25 | OnLog(level int16, time int64, message string) 26 | } 27 | 28 | type Event interface { 29 | OnStartError(t string, err string) 30 | OnShutdown(t string) 31 | OnProcessExit(code int) 32 | } 33 | 34 | var event Event 35 | var logFormatter *internal.MyFormatter 36 | 37 | func Init(e Event, cb LogCallback) error { 38 | event = e 39 | cmd.Init() 40 | logFormatter = &internal.MyFormatter{ 41 | OnLog: func(entry *log.Entry) { 42 | cb.OnLog(int16(entry.Level), entry.Time.UnixMilli(), entry.Message) 43 | }, 44 | } 45 | if utils.Log == nil { 46 | return errors.New("utils.log is nil") 47 | } else { 48 | utils.Log.SetFormatter(logFormatter) 49 | utils.Log.ExitFunc = event.OnProcessExit 50 | } 51 | return nil 52 | } 53 | 54 | var httpSrv, httpsSrv, unixSrv *http.Server 55 | 56 | func listenAndServe(t string, srv *http.Server) { 57 | err := srv.ListenAndServe() 58 | if err != nil && err != http.ErrServerClosed { 59 | event.OnStartError(t, err.Error()) 60 | } else { 61 | event.OnShutdown(t) 62 | } 63 | } 64 | 65 | func IsRunning(t string) bool { 66 | switch t { 67 | case "http": 68 | return httpSrv != nil 69 | case "https": 70 | return httpsSrv != nil 71 | case "unix": 72 | return unixSrv != nil 73 | } 74 | 75 | return httpSrv != nil && httpsSrv != nil && unixSrv != nil 76 | } 77 | 78 | // Start starts the server 79 | func Start() { 80 | if conf.Conf.DelayedStart != 0 { 81 | utils.Log.Infof("delayed start for %d seconds", conf.Conf.DelayedStart) 82 | time.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second) 83 | } 84 | bootstrap.InitOfflineDownloadTools() 85 | bootstrap.LoadStorages() 86 | bootstrap.InitTaskManager() 87 | if !flags.Debug && !flags.Dev { 88 | gin.SetMode(gin.ReleaseMode) 89 | } 90 | r := gin.New() 91 | r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out)) 92 | server.Init(r) 93 | 94 | if conf.Conf.Scheme.HttpPort != -1 { 95 | httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort) 96 | utils.Log.Infof("start HTTP server @ %s", httpBase) 97 | httpSrv = &http.Server{Addr: httpBase, Handler: r} 98 | go func() { 99 | listenAndServe("http", httpSrv) 100 | httpSrv = nil 101 | }() 102 | } 103 | if conf.Conf.Scheme.HttpsPort != -1 { 104 | httpsBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpsPort) 105 | utils.Log.Infof("start HTTPS server @ %s", httpsBase) 106 | httpsSrv = &http.Server{Addr: httpsBase, Handler: r} 107 | go func() { 108 | listenAndServe("https", httpsSrv) 109 | httpsSrv = nil 110 | }() 111 | } 112 | if conf.Conf.Scheme.UnixFile != "" { 113 | utils.Log.Infof("start unix server @ %s", conf.Conf.Scheme.UnixFile) 114 | unixSrv = &http.Server{Handler: r} 115 | go func() { 116 | listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile) 117 | if err != nil { 118 | //utils.Log.Fatalf("failed to listenAndServe unix: %+v", err) 119 | event.OnStartError("unix", err.Error()) 120 | } else { 121 | // set socket file permission 122 | mode, err := strconv.ParseUint(conf.Conf.Scheme.UnixFilePerm, 8, 32) 123 | if err != nil { 124 | utils.Log.Errorf("failed to parse socket file permission: %+v", err) 125 | } else { 126 | err = os.Chmod(conf.Conf.Scheme.UnixFile, os.FileMode(mode)) 127 | if err != nil { 128 | utils.Log.Errorf("failed to chmod socket file: %+v", err) 129 | } 130 | } 131 | err = unixSrv.Serve(listener) 132 | if err != nil && err != http.ErrServerClosed { 133 | event.OnStartError("unix", err.Error()) 134 | } 135 | } 136 | 137 | unixSrv = nil 138 | }() 139 | } 140 | } 141 | 142 | func shutdown(srv *http.Server, timeout time.Duration) error { 143 | if srv == nil { 144 | return nil 145 | } 146 | 147 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 148 | defer cancel() 149 | 150 | err := srv.Shutdown(ctx) 151 | 152 | return err 153 | } 154 | 155 | // Shutdown timeout 毫秒 156 | func Shutdown(timeout int64) (err error) { 157 | timeoutDuration := time.Duration(timeout) * time.Millisecond 158 | utils.Log.Println("Shutdown server...") 159 | if conf.Conf.Scheme.HttpPort != -1 { 160 | err := shutdown(httpSrv, timeoutDuration) 161 | if err != nil { 162 | return err 163 | } 164 | httpSrv = nil 165 | utils.Log.Println("Server HTTP Shutdown") 166 | } 167 | if conf.Conf.Scheme.HttpsPort != -1 { 168 | err := shutdown(httpsSrv, timeoutDuration) 169 | if err != nil { 170 | return err 171 | } 172 | httpsSrv = nil 173 | utils.Log.Println("Server HTTPS Shutdown") 174 | } 175 | if conf.Conf.Scheme.UnixFile != "" { 176 | err := shutdown(unixSrv, timeoutDuration) 177 | if err != nil { 178 | return err 179 | } 180 | unixSrv = nil 181 | utils.Log.Println("Server UNIX Shutdown") 182 | } 183 | 184 | //cmd.Release() 185 | return nil 186 | } 187 | -------------------------------------------------------------------------------- /alist-lib/alistlib/settings.go: -------------------------------------------------------------------------------- 1 | package alistlib 2 | 3 | import ( 4 | "github.com/alist-org/alist/v3/cmd" 5 | "github.com/alist-org/alist/v3/cmd/flags" 6 | "github.com/alist-org/alist/v3/internal/op" 7 | "github.com/alist-org/alist/v3/pkg/utils" 8 | ) 9 | 10 | func SetConfigData(path string) { 11 | flags.DataDir = path 12 | } 13 | 14 | func SetConfigLogStd(b bool) { 15 | flags.LogStd = b 16 | } 17 | 18 | func SetConfigDebug(b bool) { 19 | flags.Debug = b 20 | } 21 | 22 | func SetConfigNoPrefix(b bool) { 23 | flags.NoPrefix = b 24 | } 25 | 26 | func SetAdminPassword(pwd string) { 27 | admin, err := op.GetAdmin() 28 | if err != nil { 29 | utils.Log.Errorf("failed get admin user: %+v", err) 30 | return 31 | } 32 | admin.SetPassword(pwd) 33 | if err := op.UpdateUser(admin); err != nil { 34 | utils.Log.Errorf("failed update admin user: %+v", err) 35 | return 36 | } 37 | utils.Log.Infof("admin user has been updated:") 38 | utils.Log.Infof("username: %s", admin.Username) 39 | utils.Log.Infof("password: %s", pwd) 40 | cmd.DelAdminCacheOnline() 41 | } 42 | -------------------------------------------------------------------------------- /alist-lib/scripts/clear.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir /tmp/alist 4 | rm -rf /tmp/alist/* 5 | cp -r ../scripts /tmp/alist 6 | cp -r ../alistlib /tmp/alist 7 | 8 | rm -rf ../* 9 | 10 | cp -r /tmp/alist/* ../ -------------------------------------------------------------------------------- /alist-lib/scripts/gobind.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd ../alistlib || exit 4 | if [ "$1" == "debug" ]; then 5 | gomobile bind -ldflags "-s -w" -v -androidapi 19 -target="android/arm64" 6 | else 7 | gomobile bind -ldflags "-s -w" -v -androidapi 19 8 | fi 9 | 10 | echo "Moving aar and jar files to android/app/libs" 11 | mkdir -p ../../android/app/libs 12 | mv -f ./*.aar ../../android/app/libs 13 | mv -f ./*.jar ../../android/app/libs 14 | -------------------------------------------------------------------------------- /alist-lib/scripts/init_alist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | GIT_REPO="https://github.com/alist-org/alist.git" 4 | TAG_NAME=$(git -c 'versionsort.suffix=-' ls-remote --exit-code --refs --sort='version:refname' --tags $GIT_REPO | tail --lines=1 | cut --delimiter='/' --fields=3) 5 | 6 | echo "AList - ${TAG_NAME}" 7 | rm -rf ./src 8 | unset GIT_WORK_TREE 9 | git clone --branch "$TAG_NAME" https://github.com/alist-org/alist.git ./src 10 | rm -rf ./src/.git 11 | 12 | mv -f ./src/* ../ 13 | rm -rf ./src 14 | 15 | cd ../ 16 | go mod edit -replace github.com/djherbis/times@v1.6.0=github.com/jing332/times@latest 17 | -------------------------------------------------------------------------------- /alist-lib/scripts/init_gomobile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go install golang.org/x/mobile/cmd/gomobile@latest 4 | gomobile init 5 | go get golang.org/x/mobile/bind -------------------------------------------------------------------------------- /alist-lib/scripts/init_web.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl -L https://github.com/alist-org/alist-web/releases/latest/download/dist.tar.gz -o dist.tar.gz 4 | tar -zxvf dist.tar.gz 5 | rm -rf ../public/dist 6 | mv -f dist ../public 7 | rm -rf dist.tar.gz -------------------------------------------------------------------------------- /alist_version: -------------------------------------------------------------------------------- 1 | v3.45.0 2 | -------------------------------------------------------------------------------- /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 https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | 15 | *.aar 16 | *.exe 17 | *.tgz 18 | *.jar 19 | *.zip 20 | *.so 21 | 22 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "kotlin-parcelize" 5 | id "kotlinx-serialization" 6 | id "com.google.devtools.ksp" 7 | id "dev.flutter.flutter-gradle-plugin" 8 | } 9 | 10 | def pro = new Properties() 11 | def localPropertiesFile = rootProject.file('local.properties') 12 | if (localPropertiesFile.exists()) { 13 | localPropertiesFile.withReader('UTF-8') { reader -> 14 | pro.load(reader) 15 | } 16 | } 17 | 18 | static def releaseTime() { 19 | return new Date().format("yy.MMddHH", TimeZone.getTimeZone("GMT+8")) 20 | } 21 | 22 | def version = "1." + releaseTime() 23 | def gitCommits = Integer.parseInt('git rev-list HEAD --count'.execute().text.trim()) 24 | 25 | def alistVersion = rootProject.file("../alist_version").readLines()[0] 26 | 27 | android { 28 | namespace "com.github.jing332.alistflutter" 29 | compileSdkVersion 34 30 | ndkVersion "25.1.8937393" 31 | 32 | signingConfigs { 33 | release { 34 | storeFile file(pro["KEY_PATH"]) 35 | storePassword pro["KEY_PASSWORD"] 36 | keyAlias pro["ALIAS_NAME"] 37 | keyPassword pro["ALIAS_PASSWORD"] 38 | } 39 | } 40 | 41 | defaultConfig { 42 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 43 | applicationId "com.github.jing332.alistflutter" 44 | // You can update the following values to match your application needs. 45 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 46 | minSdkVersion flutter.minSdkVersion 47 | targetSdkVersion flutter.targetSdkVersion 48 | versionCode gitCommits 49 | versionName version 50 | 51 | ksp { 52 | arg("room.schemaLocation", "$projectDir/schemas".toString()) 53 | arg("room.incremental", "true") 54 | arg("room.expandProjection", "true") 55 | } 56 | 57 | buildConfigField("String", "ALIST_VERSION", "\"${alistVersion}\"") 58 | } 59 | 60 | 61 | buildFeatures { 62 | buildConfig true 63 | } 64 | 65 | 66 | compileOptions { 67 | sourceCompatibility JavaVersion.VERSION_17 68 | targetCompatibility JavaVersion.VERSION_17 69 | } 70 | 71 | 72 | kotlinOptions { 73 | jvmTarget = '17' 74 | } 75 | 76 | sourceSets { 77 | main { 78 | jniLibs.srcDirs = ['libs'] 79 | java.srcDirs = ['src/main/java', 'src/main/kotlin'] 80 | } 81 | } 82 | 83 | splits { 84 | abi { 85 | enable gradle.startParameter.taskNames.any { it.contains("Release") } 86 | reset() 87 | include 'armeabi-v7a', 'arm64-v8a', 'x86_64' 88 | universalApk true 89 | } 90 | } 91 | 92 | android.applicationVariants.all { variant -> 93 | variant.outputs.all { output -> 94 | //noinspection GrDeprecatedAPIUsage 95 | def abiName = output.getFilter(com.android.build.OutputFile.ABI) 96 | if (abiName == null) 97 | output.outputFileName = "AListF-v${variant.versionName}.apk" 98 | else 99 | output.outputFileName = "AListF-v${variant.versionName}_${abiName}.apk" 100 | } 101 | } 102 | 103 | // kotlin { 104 | // jvmToolchain = 17 105 | // } 106 | 107 | buildTypes { 108 | release { 109 | // TODO: Add your own signing config for the release build. 110 | // Signing with the debug keys for now, so `flutter run --release` works. 111 | signingConfig signingConfigs.release 112 | } 113 | 114 | debug { 115 | ndk { 116 | //noinspection ChromeOsAbiSupport 117 | // abiFilters "arm64-v8a" 118 | // abiFilters "x86" 119 | } 120 | } 121 | } 122 | } 123 | 124 | flutter { 125 | source '../..' 126 | } 127 | ////获取flutter的sdk路径 128 | //def flutterRoot = localProperties.getProperty('flutter.sdk') 129 | //if (flutterRoot == null) { 130 | // throw new Exception("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 131 | //} 132 | dependencies { 133 | implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') 134 | 135 | // compileOnly files("$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar") 136 | implementation project(":utils") 137 | //noinspection GradleDependency 138 | implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' 139 | 140 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' 141 | 142 | implementation 'com.louiscad.splitties:splitties-systemservices:3.0.0' 143 | 144 | implementation 'com.github.cioccarellia:ksprefs:2.4.0' 145 | 146 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") 147 | 148 | // Room 149 | // implementation("androidx.room:room-runtime:$room_version") 150 | // implementation("androidx.room:room-ktx:$room_version") 151 | // ksp("androidx.room:room-compiler:$room_version") 152 | // androidTestImplementation("androidx.room:room-testing:$room_version") 153 | 154 | 155 | } 156 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 42 | 48 | 56 | 60 | 63 | 64 | 65 | 66 | 67 | 68 | 70 | 73 | 74 | 78 | 79 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/AListService.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter 2 | 3 | import alistlib.Alistlib 4 | import android.annotation.SuppressLint 5 | import android.app.Notification 6 | import android.app.NotificationChannel 7 | import android.app.NotificationManager 8 | import android.app.PendingIntent 9 | import android.app.Service 10 | import android.content.BroadcastReceiver 11 | import android.content.Context 12 | import android.content.Intent 13 | import android.content.IntentFilter 14 | import android.os.Build 15 | import android.os.IBinder 16 | import android.os.PowerManager 17 | import androidx.localbroadcastmanager.content.LocalBroadcastManager 18 | import com.github.jing332.alistflutter.config.AppConfig 19 | import com.github.jing332.alistflutter.model.alist.AList 20 | import com.github.jing332.alistflutter.utils.AndroidUtils.registerReceiverCompat 21 | import com.github.jing332.alistflutter.utils.ClipboardUtils 22 | import com.github.jing332.alistflutter.utils.ToastUtils.toast 23 | import kotlinx.coroutines.CoroutineScope 24 | import kotlinx.coroutines.Job 25 | import splitties.systemservices.powerManager 26 | 27 | class AListService : Service(), AList.Listener { 28 | companion object { 29 | const val TAG = "AlistService" 30 | const val ACTION_SHUTDOWN = 31 | "com.github.jing332.alistandroid.service.AlistService.ACTION_SHUTDOWN" 32 | 33 | const val ACTION_COPY_ADDRESS = 34 | "com.github.jing332.alistandroid.service.AlistService.ACTION_COPY_ADDRESS" 35 | 36 | const val ACTION_STATUS_CHANGED = 37 | "com.github.jing332.alistandroid.service.AlistService.ACTION_STATUS_CHANGED" 38 | 39 | const val NOTIFICATION_CHAN_ID = "alist_server" 40 | const val FOREGROUND_ID = 5224 41 | 42 | var isRunning: Boolean = false 43 | } 44 | 45 | private val mScope = CoroutineScope(Job()) 46 | private val mNotificationReceiver = NotificationActionReceiver() 47 | private val mReceiver = MyReceiver() 48 | private var mWakeLock: PowerManager.WakeLock? = null 49 | private var mLocalAddress: String = "" 50 | 51 | override fun onBind(p0: Intent?): IBinder? = null 52 | 53 | @Suppress("DEPRECATION") 54 | private fun notifyStatusChanged() { 55 | LocalBroadcastManager.getInstance(this) 56 | .sendBroadcast(Intent(ACTION_STATUS_CHANGED)) 57 | 58 | if (!isRunning) { 59 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 60 | stopForeground(STOP_FOREGROUND_REMOVE) 61 | } else 62 | stopForeground(true) 63 | 64 | stopSelf() 65 | } 66 | } 67 | 68 | @SuppressLint("WakelockTimeout") 69 | override fun onCreate() { 70 | super.onCreate() 71 | 72 | initOrUpdateNotification() 73 | 74 | if (AppConfig.isWakeLockEnabled) { 75 | mWakeLock = powerManager.newWakeLock( 76 | PowerManager.PARTIAL_WAKE_LOCK, 77 | "alist::service" 78 | ) 79 | mWakeLock?.acquire() 80 | } 81 | 82 | LocalBroadcastManager.getInstance(this) 83 | .registerReceiver( 84 | mReceiver, 85 | IntentFilter(ACTION_STATUS_CHANGED) 86 | ) 87 | registerReceiverCompat( 88 | mNotificationReceiver, 89 | ACTION_SHUTDOWN, 90 | ACTION_COPY_ADDRESS 91 | ) 92 | 93 | AList.addListener(this) 94 | } 95 | 96 | 97 | @Suppress("DEPRECATION") 98 | override fun onDestroy() { 99 | super.onDestroy() 100 | 101 | mWakeLock?.release() 102 | mWakeLock = null 103 | 104 | stopForeground(true) 105 | 106 | LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver) 107 | unregisterReceiver(mNotificationReceiver) 108 | 109 | AList.removeListener(this) 110 | } 111 | 112 | override fun onShutdown(type: String) { 113 | if (!AList.isRunning()) { 114 | isRunning = false 115 | notifyStatusChanged() 116 | } 117 | } 118 | 119 | private fun startOrShutdown() { 120 | if (isRunning) { 121 | AList.shutdown() 122 | } else { 123 | toast(getString(R.string.starting)) 124 | isRunning = true 125 | AList.startup() 126 | notifyStatusChanged() 127 | } 128 | } 129 | 130 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 131 | startOrShutdown() 132 | 133 | return super.onStartCommand(intent, flags, startId) 134 | } 135 | 136 | inner class MyReceiver : BroadcastReceiver() { 137 | override fun onReceive(context: Context?, intent: Intent) { 138 | when (intent.action) { 139 | ACTION_STATUS_CHANGED -> { 140 | 141 | } 142 | } 143 | 144 | } 145 | } 146 | 147 | private fun localAddress(): String = Alistlib.getOutboundIPString() 148 | 149 | 150 | @Suppress("DEPRECATION") 151 | private fun initOrUpdateNotification() { 152 | // Android 12(S)+ 必须指定PendingIntent.FLAG_ 153 | val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) 154 | PendingIntent.FLAG_IMMUTABLE 155 | else 156 | 0 157 | 158 | /*点击通知跳转*/ 159 | val pendingIntent = 160 | PendingIntent.getActivity( 161 | this, 0, Intent( 162 | this, 163 | MainActivity::class.java 164 | ), 165 | pendingIntentFlags 166 | ) 167 | /*当点击退出按钮时发送广播*/ 168 | val shutdownAction: PendingIntent = 169 | PendingIntent.getBroadcast( 170 | this, 171 | 0, 172 | Intent(ACTION_SHUTDOWN), 173 | pendingIntentFlags 174 | ) 175 | val copyAddressPendingIntent = 176 | PendingIntent.getBroadcast( 177 | this, 178 | 0, 179 | Intent(ACTION_COPY_ADDRESS), 180 | pendingIntentFlags 181 | ) 182 | 183 | // val color = com.github.jing332.alistandroid.ui.theme.seed.androidColor 184 | val smallIconRes: Int 185 | val builder = Notification.Builder(applicationContext) 186 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {/*Android 8.0+ 要求必须设置通知信道*/ 187 | val chan = NotificationChannel( 188 | NOTIFICATION_CHAN_ID, 189 | getString(R.string.alist_server), 190 | NotificationManager.IMPORTANCE_NONE 191 | ) 192 | // chan.lightColor = color 193 | chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE 194 | val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 195 | service.createNotificationChannel(chan) 196 | smallIconRes = when ((0..1).random()) { 197 | 0 -> R.drawable.server 198 | 1 -> R.drawable.server2 199 | else -> R.drawable.server2 200 | } 201 | 202 | builder.setChannelId(NOTIFICATION_CHAN_ID) 203 | } else { 204 | smallIconRes = R.mipmap.ic_launcher_round 205 | } 206 | val notification = builder 207 | // .setColor(color) 208 | .setContentTitle(getString(R.string.alist_server_running)) 209 | .setContentText(localAddress()) 210 | .setSmallIcon(smallIconRes) 211 | .setContentIntent(pendingIntent) 212 | .addAction(0, getString(R.string.shutdown), shutdownAction) 213 | .addAction(0, getString(R.string.copy_address), copyAddressPendingIntent) 214 | 215 | .build() 216 | 217 | startForeground(FOREGROUND_ID, notification) 218 | } 219 | 220 | inner class NotificationActionReceiver : BroadcastReceiver() { 221 | override fun onReceive(ctx: Context?, intent: Intent?) { 222 | when (intent?.action) { 223 | ACTION_SHUTDOWN -> { 224 | startOrShutdown() 225 | } 226 | 227 | ACTION_COPY_ADDRESS -> { 228 | ClipboardUtils.copyText("AList", localAddress()) 229 | toast(R.string.address_copied) 230 | } 231 | } 232 | } 233 | } 234 | 235 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/App.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter 2 | 3 | import android.app.Application 4 | import com.github.jing332.alistflutter.utils.ToastUtils.longToast 5 | import io.flutter.app.FlutterApplication 6 | 7 | val app by lazy { App.app } 8 | 9 | class App : FlutterApplication() { 10 | companion object { 11 | lateinit var app: Application 12 | } 13 | 14 | 15 | override fun onCreate() { 16 | super.onCreate() 17 | 18 | app = this 19 | } 20 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/BootReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import com.github.jing332.alistflutter.config.AppConfig 7 | 8 | class BootReceiver : BroadcastReceiver() { 9 | override fun onReceive(context: Context, intent: Intent) { 10 | if (intent.action == Intent.ACTION_BOOT_COMPLETED && AppConfig.isStartAtBootEnabled) { 11 | context.startService(Intent(context, AListService::class.java)) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.IntentFilter 7 | import android.os.Bundle 8 | import android.util.Log 9 | import androidx.localbroadcastmanager.content.LocalBroadcastManager 10 | import com.github.jing332.alistflutter.bridge.AndroidBridge 11 | import com.github.jing332.alistflutter.bridge.AppConfigBridge 12 | import com.github.jing332.alistflutter.bridge.CommonBridge 13 | import com.github.jing332.alistflutter.model.ShortCuts 14 | import com.github.jing332.alistflutter.model.alist.Logger 15 | import com.github.jing332.pigeon.GeneratedApi 16 | import com.github.jing332.pigeon.GeneratedApi.VoidResult 17 | import io.flutter.embedding.android.FlutterActivity 18 | import io.flutter.plugins.GeneratedPluginRegistrant 19 | import kotlinx.coroutines.DelicateCoroutinesApi 20 | import kotlinx.coroutines.Dispatchers 21 | import kotlinx.coroutines.GlobalScope 22 | import kotlinx.coroutines.launch 23 | 24 | class MainActivity : FlutterActivity() { 25 | companion object { 26 | private const val TAG = "MainActivity" 27 | } 28 | 29 | private val receiver by lazy { MyReceiver() } 30 | private var mEvent: GeneratedApi.Event? = null 31 | 32 | @OptIn(DelicateCoroutinesApi::class) 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | super.onCreate(savedInstanceState) 35 | 36 | ShortCuts.buildShortCuts(this) 37 | LocalBroadcastManager.getInstance(this) 38 | .registerReceiver(receiver, IntentFilter(AListService.ACTION_STATUS_CHANGED)) 39 | 40 | GeneratedPluginRegistrant.registerWith(this.flutterEngine!!) 41 | 42 | val binaryMessage = flutterEngine!!.dartExecutor.binaryMessenger 43 | GeneratedApi.AppConfig.setUp(binaryMessage, AppConfigBridge) 44 | GeneratedApi.Android.setUp(binaryMessage, AndroidBridge(this)) 45 | GeneratedApi.NativeCommon.setUp(binaryMessage, CommonBridge(this)) 46 | mEvent = GeneratedApi.Event(binaryMessage) 47 | 48 | Logger.addListener(object : Logger.Listener { 49 | override fun onLog(level: Int, time: String, msg: String) { 50 | GlobalScope.launch(Dispatchers.Main) { 51 | mEvent?.onServerLog(level.toLong(), time, msg, object : VoidResult { 52 | override fun success() { 53 | 54 | } 55 | 56 | override fun error(error: Throwable) { 57 | } 58 | 59 | }) 60 | } 61 | } 62 | 63 | }) 64 | } 65 | 66 | override fun onDestroy() { 67 | super.onDestroy() 68 | 69 | LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) 70 | } 71 | 72 | 73 | inner class MyReceiver : BroadcastReceiver() { 74 | override fun onReceive(context: Context, intent: Intent) { 75 | when (intent.action) { 76 | AListService.ACTION_STATUS_CHANGED -> { 77 | Log.d(TAG, "onReceive: ACTION_STATUS_CHANGED") 78 | 79 | mEvent?.onServiceStatusChanged(AListService.isRunning, object : VoidResult { 80 | override fun success() {} 81 | override fun error(error: Throwable) { 82 | } 83 | }) 84 | } 85 | 86 | 87 | 88 | } 89 | 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/SwitchServerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import com.github.jing332.alistflutter.utils.ToastUtils.toast 7 | 8 | class SwitchServerActivity : Activity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | 12 | if (AListService.isRunning) { 13 | startService(Intent(this, AListService::class.java).apply { 14 | action = AListService.ACTION_SHUTDOWN 15 | }) 16 | } else { 17 | startService(Intent(this, AListService::class.java)) 18 | } 19 | 20 | finish() 21 | } 22 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/bridge/AndroidBridge.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.bridge 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Build 6 | import com.github.jing332.alistflutter.AListService 7 | import com.github.jing332.alistflutter.BuildConfig 8 | import com.github.jing332.alistflutter.R 9 | import com.github.jing332.alistflutter.SwitchServerActivity 10 | import com.github.jing332.alistflutter.model.alist.AList 11 | import com.github.jing332.alistflutter.utils.MyTools 12 | import com.github.jing332.alistflutter.utils.ToastUtils.longToast 13 | import com.github.jing332.alistflutter.utils.ToastUtils.toast 14 | import com.github.jing332.pigeon.GeneratedApi 15 | 16 | class AndroidBridge(private val context: Context) : GeneratedApi.Android { 17 | override fun addShortcut() { 18 | MyTools.addShortcut( 19 | context, 20 | context.getString(R.string.app_switch), 21 | "alist_flutter_switch", 22 | R.drawable.alist_switch, 23 | Intent(context, SwitchServerActivity::class.java) 24 | ) 25 | } 26 | 27 | override fun startService() { 28 | context.startService(Intent(context, AListService::class.java)) 29 | } 30 | 31 | override fun setAdminPwd(pwd: String) { 32 | AList.setAdminPassword(pwd) 33 | } 34 | 35 | override fun getAListHttpPort(): Long { 36 | return AList.getHttpPort().toLong() 37 | } 38 | 39 | override fun isRunning() = AListService.isRunning 40 | 41 | 42 | override fun getAListVersion() = BuildConfig.ALIST_VERSION 43 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/bridge/AppConfigBridge.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.bridge 2 | 3 | import com.github.jing332.alistflutter.config.AppConfig 4 | import com.github.jing332.pigeon.GeneratedApi 5 | 6 | 7 | object AppConfigBridge : GeneratedApi.AppConfig { 8 | override fun isWakeLockEnabled() = AppConfig.isWakeLockEnabled 9 | 10 | override fun isStartAtBootEnabled() = AppConfig.isStartAtBootEnabled 11 | 12 | override fun isAutoCheckUpdateEnabled() = AppConfig.isAutoCheckUpdateEnabled 13 | override fun isAutoOpenWebPageEnabled() = AppConfig.isAutoOpenWebPageEnabled 14 | override fun getDataDir() = AppConfig.dataDir 15 | 16 | override fun setDataDir(dir: String) { 17 | AppConfig.dataDir = dir 18 | } 19 | 20 | override fun isSilentJumpAppEnabled(): Boolean = AppConfig.isSilentJumpAppEnabled 21 | 22 | override fun setSilentJumpAppEnabled(enabled: Boolean) { 23 | AppConfig.isSilentJumpAppEnabled = enabled 24 | } 25 | 26 | override fun setAutoOpenWebPageEnabled(enabled: Boolean) { 27 | AppConfig.isAutoOpenWebPageEnabled = enabled 28 | } 29 | 30 | override fun setAutoCheckUpdateEnabled(enabled: Boolean) { 31 | AppConfig.isAutoCheckUpdateEnabled = enabled 32 | } 33 | 34 | override fun setStartAtBootEnabled(enabled: Boolean) { 35 | AppConfig.isStartAtBootEnabled = enabled 36 | } 37 | 38 | override fun setWakeLockEnabled(enabled: Boolean) { 39 | AppConfig.isWakeLockEnabled = enabled 40 | } 41 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/bridge/CommonBridge.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.bridge 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Build 6 | import com.github.jing332.alistflutter.BuildConfig 7 | import com.github.jing332.alistflutter.utils.ToastUtils.longToast 8 | import com.github.jing332.alistflutter.utils.ToastUtils.toast 9 | import com.github.jing332.pigeon.GeneratedApi 10 | 11 | class CommonBridge(private val context: Context) : GeneratedApi.NativeCommon { 12 | override fun startActivityFromUri(intentUri: String): Boolean { 13 | val intent = Intent.parseUri(intentUri, Intent.URI_INTENT_SCHEME) 14 | return if (intent.resolveActivity(context.packageManager) != null){ 15 | context.startActivity(intent) 16 | true 17 | }else{ 18 | false 19 | } 20 | } 21 | 22 | override fun getDeviceSdkInt(): Long { 23 | return Build.VERSION.SDK_INT.toLong() 24 | } 25 | 26 | 27 | override fun getDeviceCPUABI(): String { 28 | return Build.SUPPORTED_ABIS[0] 29 | } 30 | 31 | override fun getVersionName() = BuildConfig.VERSION_NAME 32 | override fun getVersionCode() = BuildConfig.VERSION_CODE.toLong() 33 | 34 | 35 | override fun toast(msg: String) { 36 | context.toast(msg) 37 | } 38 | 39 | override fun longToast(msg: String) { 40 | context.longToast(msg) 41 | } 42 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/config/AppConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.config 2 | 3 | import com.cioccarellia.ksprefs.KsPrefs 4 | import com.cioccarellia.ksprefs.dynamic 5 | import com.github.jing332.alistflutter.app 6 | 7 | object AppConfig { 8 | val prefs by lazy { KsPrefs(app, "app") } 9 | 10 | var isSilentJumpAppEnabled by prefs.dynamic("isSilentJumpAppEnabled", fallback = false) 11 | 12 | var isWakeLockEnabled: Boolean by prefs.dynamic("isWakeLockEnabled", fallback = false) 13 | var isStartAtBootEnabled: Boolean by prefs.dynamic("isStartAtBootEnabled", fallback = false) 14 | var isAutoCheckUpdateEnabled: Boolean by prefs.dynamic( 15 | "isAutoCheckUpdateEnabled", 16 | fallback = false 17 | ) 18 | 19 | var isAutoOpenWebPageEnabled: Boolean by prefs.dynamic( 20 | "isAutoOpenWebPageEnabled", 21 | fallback = false 22 | ) 23 | 24 | val defaultDataDir by lazy { app.getExternalFilesDir("data")?.absolutePath!! } 25 | 26 | private var mDataDir: String by prefs.dynamic("dataDir", fallback = defaultDataDir) 27 | 28 | 29 | var dataDir: String 30 | get() { 31 | if (mDataDir.isBlank()) mDataDir = defaultDataDir 32 | return mDataDir 33 | } 34 | set(value) { 35 | if (value.isBlank()) { 36 | mDataDir = defaultDataDir 37 | return 38 | } 39 | 40 | mDataDir = value 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/constant/AppConst.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.constant 2 | 3 | import androidx.localbroadcastmanager.content.LocalBroadcastManager 4 | import com.github.jing332.alistflutter.app 5 | import kotlinx.serialization.ExperimentalSerializationApi 6 | import kotlinx.serialization.json.Json 7 | 8 | object AppConst { 9 | @OptIn(ExperimentalSerializationApi::class) 10 | val json = Json { 11 | ignoreUnknownKeys = true 12 | allowStructuredMapKeys = true 13 | prettyPrint = true 14 | isLenient = true 15 | explicitNulls = false 16 | } 17 | 18 | val localBroadcast by lazy { 19 | LocalBroadcastManager.getInstance(app) 20 | } 21 | 22 | // val fileProviderAuthor = BuildConfig.APPLICATION_ID + ".fileprovider" 23 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/constant/LogLevel.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.constant 2 | 3 | import androidx.annotation.IntDef 4 | 5 | @IntDef( 6 | LogLevel.PANIC, 7 | LogLevel.FATAL, 8 | LogLevel.ERROR, 9 | LogLevel.WARN, 10 | LogLevel.INFO, 11 | LogLevel.DEBUG, 12 | LogLevel.TRACE 13 | ) 14 | annotation class LogLevel { 15 | companion object { 16 | const val PANIC = 0 17 | const val FATAL = 1 18 | const val ERROR = 2 19 | const val WARN = 3 20 | const val INFO = 4 21 | const val DEBUG = 5 22 | const val TRACE = 6 23 | 24 | fun Int.toLevelString(): String { 25 | return when (this) { 26 | PANIC -> "PANIC" 27 | FATAL -> "FATAL" 28 | ERROR -> "ERROR" 29 | WARN -> "WARN" 30 | INFO -> "INFO" 31 | DEBUG -> "DEBUG" 32 | TRACE -> "TRACE" 33 | else -> "UNKNOWN" 34 | } 35 | } 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/data/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | package com.github.jing332.alistflutter.data 3 | 4 | import androidx.room.AutoMigration 5 | import androidx.room.Database 6 | import androidx.room.Room 7 | import androidx.room.RoomDatabase 8 | import com.github.jing332.alistandroid.data.dao.ServerLogDao 9 | import com.github.jing332.alistflutter.data.entities.ServerLog 10 | import com.github.jing332.alistflutter.App.Companion.app 11 | 12 | val appDb by lazy { AppDatabase.create() } 13 | 14 | @Database( 15 | version = 2, 16 | entities = [ServerLog::class], 17 | autoMigrations = [ 18 | AutoMigration(from = 1, to = 2) 19 | ] 20 | ) 21 | abstract class AppDatabase : RoomDatabase() { 22 | abstract val serverLogDao: ServerLogDao 23 | 24 | companion object { 25 | fun create() = Room.databaseBuilder( 26 | app, 27 | AppDatabase::class.java, 28 | "alistandroid.db" 29 | ) 30 | .allowMainThreadQueries() 31 | .build() 32 | } 33 | }*/ 34 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/data/entities/ServerLog.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.data.entities 2 | 3 | import com.github.jing332.alistflutter.constant.LogLevel 4 | 5 | data class ServerLog( 6 | 7 | @LogLevel val level: Int, 8 | val message: String, 9 | val time: String, 10 | ) { 11 | companion object { 12 | 13 | @Suppress("RegExpRedundantEscape") 14 | fun String.evalLog(): ServerLog? { 15 | val logPattern = """(\w+)\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (.+)""".toRegex() 16 | val result = logPattern.find(this) 17 | if (result != null) { 18 | val (level, time, msg) = result.destructured 19 | val l = when (level[0].toString()) { 20 | "D" -> LogLevel.DEBUG 21 | "I" -> LogLevel.INFO 22 | "W" -> LogLevel.WARN 23 | "E" -> LogLevel.ERROR 24 | else -> LogLevel.INFO 25 | } 26 | return ServerLog(level = l, message = msg, time = time) 27 | } 28 | return null 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/model/ShortCuts.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.model 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.core.content.pm.ShortcutInfoCompat 6 | import androidx.core.content.pm.ShortcutManagerCompat 7 | import androidx.core.graphics.drawable.IconCompat 8 | import com.github.jing332.alistflutter.R 9 | import com.github.jing332.alistflutter.SwitchServerActivity 10 | 11 | 12 | object ShortCuts { 13 | private inline fun buildIntent(context: Context): Intent { 14 | val intent = Intent(context, T::class.java) 15 | intent.action = Intent.ACTION_VIEW 16 | return intent 17 | } 18 | 19 | 20 | private fun buildAlistSwitchShortCutInfo(context: Context): ShortcutInfoCompat { 21 | val msSwitchIntent = buildIntent(context) 22 | return ShortcutInfoCompat.Builder(context, "alist_switch") 23 | .setShortLabel(context.getString(R.string.app_switch)) 24 | .setLongLabel(context.getString(R.string.app_switch)) 25 | .setIcon(IconCompat.createWithResource(context, R.drawable.alist_switch)) 26 | .setIntent(msSwitchIntent) 27 | .build() 28 | } 29 | 30 | 31 | fun buildShortCuts(context: Context) { 32 | ShortcutManagerCompat.setDynamicShortcuts( 33 | context, listOf( 34 | buildAlistSwitchShortCutInfo(context), 35 | ) 36 | ) 37 | } 38 | 39 | 40 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/model/UpdateResult.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistandroid.model 2 | 3 | data class UpdateResult( 4 | val version: String = "", 5 | val time: String = "", 6 | val content: String = "", 7 | val downloadUrl: String = "", 8 | val size: Long = 0, 9 | ) { 10 | fun hasUpdate() = version.isNotBlank() && downloadUrl.isNotBlank() 11 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AList.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.model.alist 2 | 3 | import alistlib.Alistlib 4 | import alistlib.Event 5 | import alistlib.LogCallback 6 | import android.annotation.SuppressLint 7 | import android.util.Log 8 | import com.github.jing332.alistflutter.R 9 | import com.github.jing332.alistflutter.app 10 | import com.github.jing332.alistflutter.config.AppConfig 11 | import com.github.jing332.alistflutter.constant.LogLevel 12 | import com.github.jing332.alistflutter.utils.ToastUtils.longToast 13 | import java.io.File 14 | import java.text.SimpleDateFormat 15 | import java.util.Locale 16 | 17 | object AList : Event, LogCallback { 18 | const val TAG = "AList" 19 | 20 | val context = app 21 | 22 | val dataDir: String 23 | get() = AppConfig.dataDir 24 | 25 | val configPath: String 26 | get() = "$dataDir${File.separator}config.json" 27 | 28 | 29 | fun init() { 30 | runCatching { 31 | Alistlib.setConfigData(dataDir) 32 | Alistlib.setConfigLogStd(true) 33 | Alistlib.init(this, this) 34 | }.onFailure { 35 | Log.e(TAG, "init:", it) 36 | } 37 | } 38 | 39 | interface Listener { 40 | fun onShutdown(type: String) 41 | } 42 | 43 | private val mListeners = mutableListOf() 44 | 45 | fun addListener(listener: Listener) { 46 | mListeners.add(listener) 47 | } 48 | 49 | fun removeListener(listener: Listener) { 50 | mListeners.remove(listener) 51 | } 52 | 53 | override fun onShutdown(p0: String) { 54 | Log.d(TAG, "onShutdown: $p0") 55 | mListeners.forEach { it.onShutdown(p0) } 56 | } 57 | 58 | override fun onStartError(type: String, msg: String) { 59 | Log.e(TAG, "onStartError: $type, $msg") 60 | Logger.log(LogLevel.FATAL, type, msg) 61 | } 62 | 63 | private val mDateFormatter by lazy { SimpleDateFormat("MM-dd HH:mm:ss", Locale.getDefault())} 64 | 65 | override fun onLog(level: Short, time: Long, log: String) { 66 | Log.d(TAG, "onLog: $level, $time, $log") 67 | Logger.log(level.toInt(), mDateFormatter.format(time), log) 68 | } 69 | 70 | override fun onProcessExit(code: Long) { 71 | 72 | } 73 | 74 | fun isRunning(): Boolean { 75 | return Alistlib.isRunning("") 76 | } 77 | 78 | fun setAdminPassword(pwd: String) { 79 | if (!isRunning()) init() 80 | 81 | Log.d(TAG, "setAdminPassword: $dataDir") 82 | Alistlib.setConfigData(dataDir) 83 | Alistlib.setAdminPassword(pwd) 84 | } 85 | 86 | 87 | fun shutdown() { 88 | Log.d(TAG, "shutdown") 89 | runCatching { 90 | Alistlib.shutdown(5000) 91 | }.onFailure { 92 | context.longToast(R.string.shutdown_failed) 93 | } 94 | } 95 | 96 | @SuppressLint("SdCardPath") 97 | fun startup() { 98 | Log.d(TAG, "startup: $dataDir") 99 | init() 100 | Alistlib.start() 101 | } 102 | 103 | fun getHttpPort(): Int { 104 | return AListConfigManager.config().scheme.httpPort 105 | } 106 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AListConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.model.alist 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | 7 | @Serializable 8 | data class AListConfig( 9 | @SerialName("bleve_dir") 10 | val bleveDir: String = "", // /storage/emulated/0/Android/data/com.github.jing332.alistandroid.debug/files/data/bleve 11 | @SerialName("cdn") 12 | val cdn: String = "", 13 | // @SerialName("database") 14 | // val database: Database = Database(), 15 | @SerialName("delayed_start") 16 | val delayedStart: Int = 0, // 0 17 | @SerialName("force") 18 | val force: Boolean = false, // false 19 | @SerialName("jwt_secret") 20 | val jwtSecret: String = "", // 21 | // @SerialName("log") 22 | // val log: Log = Log(), 23 | @SerialName("max_connections") 24 | val maxConnections: Int = 0, // 0 25 | @SerialName("scheme") 26 | val scheme: Scheme = Scheme(), 27 | @SerialName("site_url") 28 | val siteUrl: String = "", 29 | @SerialName("temp_dir") 30 | val tempDir: String = "", // /storage/emulated/0/Android/data/com.github.jing332.alistandroid.debug/files/data/temp 31 | @SerialName("tls_insecure_skip_verify") 32 | val tlsInsecureSkipVerify: Boolean = true, // true 33 | @SerialName("token_expires_in") 34 | val tokenExpiresIn: Int = 48 // 48 35 | ) { 36 | @Serializable 37 | data class Database( 38 | @SerialName("db_file") 39 | val dbFile: String = "", // /storage/emulated/0/Android/data/com.github.jing332.alistandroid.debug/files/data/data.db 40 | @SerialName("host") 41 | val host: String = "", 42 | @SerialName("name") 43 | val name: String = "", 44 | @SerialName("password") 45 | val password: String = "", 46 | @SerialName("port") 47 | val port: Int = 0, // 0 48 | @SerialName("ssl_mode") 49 | val sslMode: String = "", 50 | @SerialName("table_prefix") 51 | val tablePrefix: String = "x_", // x_ 52 | @SerialName("type") 53 | val type: String = "sqlite3", // sqlite3 54 | @SerialName("user") 55 | val user: String = "" 56 | ) 57 | 58 | @Serializable 59 | data class Log( 60 | @SerialName("compress") 61 | val compress: Boolean = false, // false 62 | @SerialName("enable") 63 | val enable: Boolean = true, // true 64 | @SerialName("max_age") 65 | val maxAge: Int = 28, // 28 66 | @SerialName("max_backups") 67 | val maxBackups: Int = 5, // 5 68 | @SerialName("max_size") 69 | val maxSize: Int = 10, // 10 70 | @SerialName("name") 71 | val name: String = "" // /storage/emulated/0/Android/data/com.github.jing332.alistandroid.debug/files/data/log/log.log 72 | ) 73 | 74 | @Serializable 75 | data class Scheme( 76 | @SerialName("address") 77 | val address: String = "0.0.0.0", // 0.0.0.0 78 | @SerialName("cert_file") 79 | val certFile: String = "", 80 | @SerialName("force_https") 81 | val forceHttps: Boolean = false, // false 82 | @SerialName("http_port") 83 | val httpPort: Int = 5244, // 5244 84 | @SerialName("https_port") 85 | val httpsPort: Int = -1, // -1 86 | @SerialName("key_file") 87 | val keyFile: String = "", 88 | @SerialName("unix_file") 89 | val unixFile: String = "", 90 | @SerialName("unix_file_perm") 91 | val unixFilePerm: String = "" 92 | ) 93 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/AListConfigManager.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.model.alist 2 | 3 | import android.os.FileObserver 4 | import android.util.Log 5 | import com.github.jing332.alistflutter.app 6 | import com.github.jing332.alistflutter.constant.AppConst 7 | import com.github.jing332.alistflutter.utils.ToastUtils.longToast 8 | import kotlinx.coroutines.CancellationException 9 | import kotlinx.coroutines.awaitCancellation 10 | import kotlinx.coroutines.coroutineScope 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.channelFlow 13 | import kotlinx.coroutines.launch 14 | import kotlinx.coroutines.runBlocking 15 | import kotlinx.serialization.ExperimentalSerializationApi 16 | import kotlinx.serialization.json.decodeFromStream 17 | import kotlinx.serialization.json.encodeToStream 18 | import java.io.File 19 | 20 | @Suppress("DEPRECATION") 21 | object AListConfigManager { 22 | const val TAG = "AListConfigManager" 23 | 24 | val context 25 | get() = app 26 | 27 | suspend fun flowConfig(): Flow = channelFlow { 28 | val obs = object : FileObserver(AList.configPath) { 29 | override fun onEvent(event: Int, p1: String?) { 30 | if (listOf(CLOSE_NOWRITE, CLOSE_WRITE).contains(event)) 31 | runBlocking { 32 | Log.d(TAG, "config.json changed: $event") 33 | send((config())) 34 | } 35 | } 36 | } 37 | coroutineScope { 38 | val waitJob = launch { 39 | obs.startWatching() 40 | try { 41 | awaitCancellation() 42 | } catch (_: CancellationException) { 43 | } 44 | 45 | obs.stopWatching() 46 | } 47 | waitJob.join() 48 | } 49 | } 50 | 51 | @OptIn(ExperimentalSerializationApi::class) 52 | fun config(): AListConfig { 53 | try { 54 | File(AList.configPath).inputStream().use { 55 | return AppConst.json.decodeFromStream(it) 56 | } 57 | } catch (e: Exception) { 58 | AList.context.longToast("读取 config.json 失败:\n$e") 59 | return AListConfig() 60 | } 61 | } 62 | 63 | @OptIn(ExperimentalSerializationApi::class) 64 | fun update(cfg: AListConfig) { 65 | try { 66 | File(AList.configPath).outputStream().use { 67 | AppConst.json.encodeToStream(cfg, it) 68 | } 69 | } catch (e: Exception) { 70 | AList.context.longToast("更新 config.json 失败:\n$e") 71 | } 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/model/alist/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.model.alist 2 | 3 | object Logger { 4 | private var listeners = mutableListOf() 5 | 6 | fun addListener(listener: Listener) { 7 | listeners.add(listener) 8 | } 9 | 10 | fun removeListener(listener: Listener) { 11 | listeners.remove(listener) 12 | } 13 | 14 | interface Listener { 15 | fun onLog(level: Int, time: String, msg: String) 16 | } 17 | 18 | fun log(level: Int, time: String, msg: String) { 19 | listeners.forEach { 20 | it.onLog(level, time, msg) 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/AndroidUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.utils 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Context.RECEIVER_EXPORTED 6 | import android.content.IntentFilter 7 | import android.os.Build 8 | 9 | object AndroidUtils { 10 | fun Context.registerReceiverCompat( 11 | receiver: BroadcastReceiver, 12 | vararg actions: String 13 | ) { 14 | val intentFilter = IntentFilter() 15 | actions.forEach { intentFilter.addAction(it) } 16 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 17 | registerReceiver(receiver, intentFilter, RECEIVER_EXPORTED) 18 | } else { 19 | registerReceiver(receiver, intentFilter) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/ClipBoardUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.utils 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.ClipboardManager.OnPrimaryClipChangedListener 6 | import android.content.Context 7 | import com.github.jing332.alistflutter.app 8 | 9 | 10 | /** 11 | *
12 |  * author: Blankj
13 |  * blog  : http://blankj.com
14 |  * time  : 2016/09/25
15 |  * desc  : utils about clipboard
16 | 
* 17 | */ 18 | object ClipboardUtils { 19 | 20 | /** 21 | * Copy the text to clipboard. 22 | * 23 | * The label equals name of package. 24 | * 25 | * @param text The text. 26 | */ 27 | fun copyText(text: CharSequence?) { 28 | val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 29 | cm.setPrimaryClip(ClipData.newPlainText(app.getPackageName(), text)) 30 | } 31 | 32 | /** 33 | * Copy the text to clipboard. 34 | * 35 | * @param label The label. 36 | * @param text The text. 37 | */ 38 | fun copyText(label: CharSequence?, text: CharSequence?) { 39 | val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 40 | cm.setPrimaryClip(ClipData.newPlainText(label, text)) 41 | } 42 | 43 | /** 44 | * Clear the clipboard. 45 | */ 46 | fun clear() { 47 | val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 48 | cm.setPrimaryClip(ClipData.newPlainText(null, "")) 49 | } 50 | 51 | /** 52 | * Return the label for clipboard. 53 | * 54 | * @return the label for clipboard 55 | */ 56 | fun getLabel(): CharSequence { 57 | val cm = app 58 | .getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 59 | val des = cm.primaryClipDescription ?: return "" 60 | return des.label ?: return "" 61 | } 62 | 63 | /** 64 | * Return the text for clipboard. 65 | * 66 | * @return the text for clipboard 67 | */ 68 | val text: CharSequence 69 | get() { 70 | val cm = 71 | app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 72 | val clip = cm.primaryClip 73 | if (clip != null && clip.itemCount > 0) { 74 | val text = clip.getItemAt(0).coerceToText(app) 75 | if (text != null) { 76 | return text 77 | } 78 | } 79 | return "" 80 | } 81 | 82 | /** 83 | * Add the clipboard changed listener. 84 | */ 85 | fun addChangedListener(listener: OnPrimaryClipChangedListener?) { 86 | val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 87 | cm.addPrimaryClipChangedListener(listener) 88 | } 89 | 90 | /** 91 | * Remove the clipboard changed listener. 92 | */ 93 | fun removeChangedListener(listener: OnPrimaryClipChangedListener?) { 94 | val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 95 | cm.removePrimaryClipChangedListener(listener) 96 | } 97 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.utils 2 | 3 | import java.io.File 4 | import java.io.InputStream 5 | import java.net.URLConnection 6 | 7 | object FileUtils { 8 | val File.mimeType: String? 9 | get() { 10 | val fileNameMap = URLConnection.getFileNameMap() 11 | return fileNameMap.getContentTypeFor(name) 12 | } 13 | 14 | /** 15 | * 按行读取txt 16 | */ 17 | fun InputStream.readAllText(): String { 18 | val bufferedReader = this.bufferedReader() 19 | val buffer = StringBuffer("") 20 | var str: String? 21 | while (bufferedReader.readLine().also { str = it } != null) { 22 | buffer.append(str) 23 | buffer.append("\n") 24 | } 25 | return buffer.toString() 26 | } 27 | 28 | fun copyFolder(src: File, target: File, overwrite: Boolean = true) { 29 | val folder = File(target.absolutePath + File.separator + src.name) 30 | folder.mkdirs() 31 | 32 | src.listFiles()?.forEach { 33 | if (it.isFile) { 34 | val newFile = File(folder.absolutePath + File.separator + it.name) 35 | it.copyTo(newFile, overwrite) 36 | } else if (it.isDirectory) { 37 | copyFolder(it, folder) 38 | } 39 | } 40 | 41 | } 42 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/MyTools.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.PendingIntent 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.pm.ShortcutInfo 8 | import android.content.pm.ShortcutManager 9 | import android.graphics.drawable.Icon 10 | import android.net.Uri 11 | import android.os.Build 12 | import android.provider.Settings 13 | import com.github.jing332.alistflutter.utils.ToastUtils.longToast 14 | import splitties.systemservices.powerManager 15 | 16 | object MyTools { 17 | fun Context.isIgnoringBatteryOptimizations(): Boolean { 18 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && 19 | powerManager.isIgnoringBatteryOptimizations(packageName) 20 | } 21 | 22 | @SuppressLint("BatteryLife") 23 | fun Context.killBattery() { 24 | runCatching { 25 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !isIgnoringBatteryOptimizations()) { 26 | startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { 27 | data = Uri.parse("package:$packageName") 28 | }) 29 | } 30 | } 31 | } 32 | 33 | /* 添加快捷方式 */ 34 | @SuppressLint("UnspecifiedImmutableFlag") 35 | @Suppress("DEPRECATION") 36 | fun addShortcut( 37 | ctx: Context, 38 | name: String, 39 | id: String, 40 | iconResId: Int, 41 | launcherIntent: Intent 42 | ) { 43 | ctx.longToast("如失败 请手动授予权限") 44 | if (Build.VERSION.SDK_INT < 26) { /* Android8.0 */ 45 | val addShortcutIntent = Intent("com.android.launcher.action.INSTALL_SHORTCUT") 46 | // 不允许重复创建 47 | addShortcutIntent.putExtra("duplicate", false) // 经测试不是根据快捷方式的名字判断重复的 48 | addShortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name) 49 | addShortcutIntent.putExtra( 50 | Intent.EXTRA_SHORTCUT_ICON_RESOURCE, 51 | Intent.ShortcutIconResource.fromContext( 52 | ctx, iconResId 53 | ) 54 | ) 55 | 56 | launcherIntent.action = Intent.ACTION_MAIN 57 | launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER) 58 | addShortcutIntent 59 | .putExtra(Intent.EXTRA_SHORTCUT_INTENT, launcherIntent) 60 | 61 | // 发送广播 62 | ctx.sendBroadcast(addShortcutIntent) 63 | } else { 64 | val shortcutManager: ShortcutManager = ctx.getSystemService(ShortcutManager::class.java) 65 | if (shortcutManager.isRequestPinShortcutSupported) { 66 | launcherIntent.action = Intent.ACTION_VIEW 67 | val pinShortcutInfo = ShortcutInfo.Builder(ctx, id) 68 | .setIcon( 69 | Icon.createWithResource(ctx, iconResId) 70 | ) 71 | .setIntent(launcherIntent) 72 | .setShortLabel(name) 73 | .build() 74 | val pinnedShortcutCallbackIntent = shortcutManager 75 | .createShortcutResultIntent(pinShortcutInfo) 76 | //Get notified when a shortcut is pinned successfully// 77 | val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 78 | PendingIntent.FLAG_IMMUTABLE 79 | } else { 80 | 0 81 | } 82 | val successCallback = PendingIntent.getBroadcast( 83 | ctx, 0, pinnedShortcutCallbackIntent, pendingIntentFlags 84 | ) 85 | shortcutManager.requestPinShortcut( 86 | pinShortcutInfo, successCallback.intentSender 87 | ) 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/StringUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.utils 2 | 3 | object StringUtils { 4 | private fun paramsParseInternal(params: String): HashMap { 5 | val parameters: HashMap = hashMapOf() 6 | if (params.isBlank()) return parameters 7 | 8 | for (param in params.split("&")) { 9 | val entry = param.split("=".toRegex()).dropLastWhile { it.isEmpty() } 10 | if (entry.size > 1) { 11 | parameters[entry[0]] = entry[1] 12 | } else { 13 | parameters[entry[0]] = "" 14 | } 15 | } 16 | return parameters 17 | } 18 | 19 | fun String.paramsParse() = paramsParseInternal(this) 20 | 21 | fun String.toNumberInt(): Int { 22 | return this.replace(Regex("[^0-9]"), "").toIntOrNull() ?: 0 23 | } 24 | 25 | private val mAnsiRegex = Regex("""\x1b(\[.*?[@-~]|].*?(\x07|\x1b\\))""") 26 | fun String.removeAnsiCodes(): String { 27 | return mAnsiRegex.replace(this, "") 28 | } 29 | 30 | fun String.parseToMap(): Map { 31 | return this.split(";").associate { 32 | val ss = it.trim().split("=") 33 | if (ss.size != 2) return@associate "" to "" 34 | 35 | val key = ss[0] 36 | val value = ss[1] 37 | key.trim() to value.trim() 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/jing332/alistflutter/utils/ToastUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.alistflutter.utils 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import androidx.annotation.StringRes 6 | import kotlinx.coroutines.DelicateCoroutinesApi 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.launch 10 | 11 | object ToastUtils { 12 | @OptIn(DelicateCoroutinesApi::class) 13 | fun runMain(block: () -> Unit) { 14 | GlobalScope.launch(Dispatchers.Main) { 15 | block() 16 | } 17 | } 18 | 19 | fun Context.toast(str: String) { 20 | runMain { 21 | Toast.makeText(this, str, Toast.LENGTH_SHORT).show() 22 | } 23 | } 24 | 25 | fun Context.toast(@StringRes strId: Int, vararg args: Any) { 26 | runMain { 27 | Toast.makeText( 28 | this, 29 | getString(strId, *args), 30 | Toast.LENGTH_SHORT 31 | ).show() 32 | } 33 | } 34 | 35 | fun Context.longToast(str: String) { 36 | runMain { 37 | Toast.makeText(this, str, Toast.LENGTH_LONG).show() 38 | } 39 | } 40 | 41 | fun Context.longToast(@StringRes strId: Int) { 42 | runMain { 43 | Toast.makeText(this, strId, Toast.LENGTH_LONG).show() 44 | } 45 | } 46 | 47 | fun Context.longToast(@StringRes strId: Int, vararg args: Any) { 48 | runMain { 49 | Toast.makeText( 50 | this, 51 | getString(strId, *args), 52 | Toast.LENGTH_LONG 53 | ).show() 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/alist_logo.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/alist_switch.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_female.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/server.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/server2.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/AListFlutter/be263b52ecadae34e8f21c60c89c71dc1a74a3d7/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/AListFlutter/be263b52ecadae34e8f21c60c89c71dc1a74a3d7/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/AListFlutter/be263b52ecadae34e8f21c60c89c71dc1a74a3d7/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/AListFlutter/be263b52ecadae34e8f21c60c89c71dc1a74a3d7/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/AListFlutter/be263b52ecadae34e8f21c60c89c71dc1a74a3d7/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/AListFlutter/be263b52ecadae34e8f21c60c89c71dc1a74a3d7/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/AListFlutter/be263b52ecadae34e8f21c60c89c71dc1a74a3d7/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/AListFlutter/be263b52ecadae34e8f21c60c89c71dc1a74a3d7/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/AListFlutter/be263b52ecadae34e8f21c60c89c71dc1a74a3d7/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/AListFlutter/be263b52ecadae34e8f21c60c89c71dc1a74a3d7/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | AList 4 | 5 | 开源许可 6 | 返回 7 | 未选择文件 8 | ❌ 错误 9 | 确定 10 | 描述 11 | 日志 12 | AList服务器 13 | 添加桌面快捷方式 14 | 关闭 15 | 复制地址 16 | AList运行中 17 | 取消 18 | admin 密码已设为:\n %1$s 19 | admin 密码 20 | 关闭失败:%1$s 21 | 已复制地址 22 | ⚠️启动服务器才可设置admin密码 23 | AList配置 24 | 设置 25 | 密码 26 | 启动中 27 | 关闭中 28 | 开关 29 | 更多选项 30 | 关于 31 | 监听地址 32 | 编辑 config.json 33 | 请至少启用一个服务器! 34 | AList提供者 35 | account 36 | 路径已复制 37 | 检查更新 38 | 启动 39 | 所有文件访问权限 40 | 挂载本地存储时必须打开,否则无权限读写文件。 41 | 读取存储权限 42 | 写入存储权限 43 | 请求电池优化白名单 44 | 如果程序在后台运行时被系统杀死,可以尝试设置。 45 | 关闭 46 | 自动检查更新 47 | 打开程序主界面时从Github检查更新 48 | 唤醒锁 49 | 打开可防止锁屏后CPU休眠,但在部分系统可能会导致杀后台 50 | 重要设置 51 | 打开data文件夹 52 | 点按上方路径选择“MT管理器”打开data文件夹 53 | 网页 54 | 跳转失败: %1$s 55 | 清空网页数据 56 | 清空网页缓存 57 | 清空网页数据库、Cookie、DomStorage。 58 | 仅清空资源缓存,不影响用户数据。 59 | 已清除 60 | 确定 61 | 自动打开网页界面 62 | 打开主界面时,自动跳转到网页界面。 63 | 下载文件 64 | 系统下载器 65 | 打开链接 66 | 已复制链接 67 | 启动中 68 | 已关闭: %1$s 69 | 浏览器 70 | 选择下载器 71 | 上次使用 72 | 开机自启动服务 73 | 在开机时自动开启AList服务。 74 | 关闭失败 75 | 76 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |