Repository: OpenListTeam/OpenList-Mobile Branch: main Commit: 48aab08afc2d Files: 164 Total size: 818.7 KB Directory structure: gitextract_oqnk2mes/ ├── .github/ │ ├── scripts/ │ │ ├── check_openlist.sh │ │ ├── lzy_web.py │ │ └── update_pubspec_version.sh │ └── workflows/ │ ├── build.yaml │ ├── build_openlist.yaml │ ├── release.yaml │ └── sync_openlist.yaml ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── README_EN.md ├── analysis_options.yaml ├── android/ │ ├── .gitignore │ ├── app/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── debug/ │ │ │ └── AndroidManifest.xml │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── openlist/ │ │ │ │ └── pigeon/ │ │ │ │ └── GeneratedApi.java │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── openlist/ │ │ │ │ └── mobile/ │ │ │ │ ├── App.kt │ │ │ │ ├── BootReceiver.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── OpenListService.kt │ │ │ │ ├── OpenListTileService.kt │ │ │ │ ├── SwitchServerActivity.kt │ │ │ │ ├── bridge/ │ │ │ │ │ ├── AndroidBridge.kt │ │ │ │ │ ├── AppConfigBridge.kt │ │ │ │ │ ├── CommonBridge.kt │ │ │ │ │ └── ServiceBridge.kt │ │ │ │ ├── config/ │ │ │ │ │ └── AppConfig.kt │ │ │ │ ├── constant/ │ │ │ │ │ ├── AppConst.kt │ │ │ │ │ └── LogLevel.kt │ │ │ │ ├── data/ │ │ │ │ │ ├── AppDatabase.kt │ │ │ │ │ └── entities/ │ │ │ │ │ └── ServerLog.kt │ │ │ │ ├── model/ │ │ │ │ │ ├── ShortCuts.kt │ │ │ │ │ ├── UpdateResult.kt │ │ │ │ │ └── openlist/ │ │ │ │ │ ├── Logger.kt │ │ │ │ │ ├── OpenList.kt │ │ │ │ │ ├── OpenListConfig.kt │ │ │ │ │ └── OpenListConfigManager.kt │ │ │ │ └── utils/ │ │ │ │ ├── AndroidUtils.kt │ │ │ │ ├── BatteryOptimizationUtils.kt │ │ │ │ ├── ClipBoardUtils.kt │ │ │ │ ├── FileUtils.kt │ │ │ │ ├── MyTools.kt │ │ │ │ ├── StringUtils.kt │ │ │ │ └── ToastUtils.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ ├── ic_download.xml │ │ │ │ ├── ic_female.xml │ │ │ │ ├── ic_launcher_foreground.xml │ │ │ │ ├── launch_background.xml │ │ │ │ ├── openlist_logo.xml │ │ │ │ ├── openlist_switch.xml │ │ │ │ ├── server.xml │ │ │ │ └── server2.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values/ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── strings.xml │ │ │ │ ├── styles.xml │ │ │ │ └── themes.xml │ │ │ ├── values-en/ │ │ │ │ └── strings.xml │ │ │ ├── values-night/ │ │ │ │ └── styles.xml │ │ │ └── xml/ │ │ │ ├── backup_rules.xml │ │ │ ├── data_extraction_rules.xml │ │ │ ├── file_path_data.xml │ │ │ ├── file_paths.xml │ │ │ └── network_security_config.xml │ │ └── profile/ │ │ └── AndroidManifest.xml │ ├── build/ │ │ └── reports/ │ │ └── problems/ │ │ └── problems-report.html │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradle.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 ├── ios/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── README_iOS_CONFIG.md │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── LaunchImage.imageset/ │ │ │ ├── Contents.json │ │ │ └── README.md │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Bridges/ │ │ │ ├── AppConfigBridge.swift │ │ │ ├── AppStoreUpdateBridge.swift │ │ │ ├── CommonBridge.swift │ │ │ └── OpenListBridge.swift │ │ ├── Info.plist │ │ ├── OpenListManager.swift │ │ ├── PigeonApi.swift │ │ └── Runner-Bridging-Header.h │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ ├── RunnerTests/ │ │ └── RunnerTests.swift │ └── scripts/ │ └── generate_ios_icons.py ├── 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/ │ │ ├── app_update_dialog.dart │ │ ├── download_manager_page.dart │ │ ├── openlist/ │ │ │ ├── about_dialog.dart │ │ │ ├── config_editor_page.dart │ │ │ ├── log_level_view.dart │ │ │ ├── log_list_view.dart │ │ │ ├── openlist.dart │ │ │ └── pwd_edit_dialog.dart │ │ ├── settings/ │ │ │ ├── preference_widgets.dart │ │ │ ├── settings.dart │ │ │ └── troubleshooting_page.dart │ │ └── web/ │ │ └── web.dart │ ├── utils/ │ │ ├── app_store_update.dart │ │ ├── download_examples.dart │ │ ├── download_manager.dart │ │ ├── download_test.dart │ │ ├── intent_utils.dart │ │ ├── language_controller.dart │ │ ├── language_manager.dart │ │ ├── notification_manager.dart │ │ ├── service_manager.dart │ │ └── update_checker.dart │ └── widgets/ │ └── switch_floating_action_button.dart ├── openlist-lib/ │ ├── openlistlib/ │ │ ├── common.go │ │ ├── internal/ │ │ │ └── log.go │ │ ├── server.go │ │ └── settings.go │ └── scripts/ │ ├── clear.sh │ ├── fix_ios_dependencies.sh │ ├── gobind.sh │ ├── gobind_ios.sh │ ├── init_gomobile.sh │ ├── init_openlist.sh │ ├── init_web.sh │ └── init_web_ios.sh ├── openlist_version ├── pigeon_config.yaml ├── pigeons/ │ ├── pigeon.dart │ └── run.cmd ├── pubspec.yaml └── test/ └── widget_test.dart ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/scripts/check_openlist.sh ================================================ #!/bin/bash GIT_REPO="https://github.com/OpenListTeam/OpenList.git" function compare_versions() { local v1="${1#v}" local v2="${2#v}" local max_len=0 IFS='.' read -r -a v1_parts <<< "$v1" IFS='.' read -r -a v2_parts <<< "$v2" if [ "${#v1_parts[@]}" -gt "${#v2_parts[@]}" ]; then max_len="${#v1_parts[@]}" else max_len="${#v2_parts[@]}" fi for ((i=0; i 10#$p2)); then echo 1 return fi if ((10#$p1 < 10#$p2)); then echo -1 return fi done echo 0 } function get_latest_version() { echo $(git -c 'versionsort.suffix=-' ls-remote --exit-code --refs --sort='version:refname' --tags $GIT_REPO | tail --lines=1 | cut --delimiter='/' --fields=3) } LATEST_VER="" for index in $(seq 5) do echo "Try to get latest version, index=$index" LATEST_VER=$(get_latest_version) if [ -z "$LATEST_VER" ]; then if [ "$index" -ge 5 ]; then echo "Failed to get latest version, exit" exit 1 fi echo "Failed to get latest version, sleep 15s and retry" sleep 15 else break fi done echo "Latest OpenList version $LATEST_VER" echo "openlist_version=$LATEST_VER" >> "$GITHUB_ENV" # VERSION_FILE="$GITHUB_WORKSPACE/openlist_version.txt" VER=$(cat "$VERSION_FILE") if [ -z "$VER" ]; then VER="v3.25.1" echo "No version file, use default version ${VER}" fi echo "Current OpenList version: $VER" COMPARE_RESULT=$(compare_versions "$VER" "$LATEST_VER") if [ "$COMPARE_RESULT" -ge 0 ]; then echo "Current >= Latest" echo "openlist_update=0" >> "$GITHUB_ENV" else echo "Current < Latest" echo "openlist_update=1" >> "$GITHUB_ENV" fi ================================================ FILE: .github/scripts/lzy_web.py ================================================ import requests, os, datetime, sys # Cookie 中 phpdisk_info 的值 cookie_phpdisk_info = os.environ.get('phpdisk_info') # Cookie 中 ylogin 的值 cookie_ylogin = os.environ.get('ylogin') # 请求头 headers = { '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', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Referer': 'https://pc.woozooo.com/account.php?action=login' } # 小饼干 cookie = { 'ylogin': cookie_ylogin, 'phpdisk_info': cookie_phpdisk_info } # 日志打印 def log(msg): utc_time = datetime.datetime.utcnow() china_time = utc_time + datetime.timedelta(hours=8) print(f"[{china_time.strftime('%Y.%m.%d %H:%M:%S')}] {msg}") # 检查是否已登录 def login_by_cookie(): url_account = "https://pc.woozooo.com/account.php" if cookie['phpdisk_info'] is None: log('ERROR: 请指定 Cookie 中 phpdisk_info 的值!') return False if cookie['ylogin'] is None: log('ERROR: 请指定 Cookie 中 ylogin 的值!') return False res = requests.get(url_account, headers=headers, cookies=cookie, verify=True) if '网盘用户登录' in res.text: log('ERROR: 登录失败,请更新Cookie') return False else: log('登录成功') return True # 上传文件 def upload_file(file_dir, folder_id): file_name = os.path.basename(file_dir) url_upload = "https://up.woozooo.com/fileup.php" headers['Referer'] = f'https://up.woozooo.com/mydisk.php?item=files&action=index&u={cookie_ylogin}' post_data = { "task": "1", "folder_id": folder_id, "id": "WU_FILE_0", "name": file_name, } files = {'upload_file': (file_name, open(file_dir, "rb"), 'application/octet-stream')} res = requests.post(url_upload, data=post_data, files=files, headers=headers, cookies=cookie, timeout=120).json() log(f"{file_dir} -> {res['info']}") return res['zt'] == 1 # 上传文件夹内的文件 def upload_folder(folder_dir, folder_id): file_list = sorted(os.listdir(folder_dir), reverse=True) for file in file_list: path = os.path.join(folder_dir, file) if os.path.isfile(path): upload_file(path, folder_id) else: upload_folder(path, folder_id) # 上传 def upload(dir, folder_id): if dir is None: log('ERROR: 请指定上传的文件路径') return if folder_id is None: log('ERROR: 请指定蓝奏云的文件夹id') return if os.path.isfile(dir): upload_file(dir, str(folder_id)) else: upload_folder(dir, str(folder_id)) if __name__ == '__main__': argv = sys.argv[1:] if len(argv) != 2: log('ERROR: 参数错误,请以这种格式重新尝试\npython lzy_web.py 需上传的路径 蓝奏云文件夹id') # 需上传的路径 upload_path = argv[0] # 蓝奏云文件夹id lzy_folder_id = argv[1] if login_by_cookie(): upload(upload_path, lzy_folder_id) ================================================ FILE: .github/scripts/update_pubspec_version.sh ================================================ #!/bin/bash VERSION_FILE="$GITHUB_WORKSPACE/openlist_version" PUBSPEC_FILE="$GITHUB_WORKSPACE/pubspec.yaml" if [ ! -f "$VERSION_FILE" ]; then echo "Error: openlist_version file not found" exit 1 fi if [ ! -f "$PUBSPEC_FILE" ]; then echo "Error: pubspec.yaml file not found" exit 1 fi OPENLIST_VERSION=$(cat "$VERSION_FILE") BASE_VERSION=${OPENLIST_VERSION#v} echo "Updating pubspec.yaml version to: $BASE_VERSION" sed -i "s/^version: [0-9]\+\.[0-9]\+\.[0-9]\++[0-9]\+.*/version: ${BASE_VERSION}+1/" "$PUBSPEC_FILE" echo "pubspec.yaml version updated successfully" ================================================ FILE: .github/workflows/build.yaml ================================================ name: Build on: push: branches: - "main" paths-ignore: - "*.md" - "*.sh" - "release.yaml" # - "sync_frp.yaml" pull_request: branches: ["main"] workflow_dispatch: jobs: version: name: Get OpenList Version Information runs-on: ubuntu-latest outputs: version_name: ${{ steps.generate.outputs.version_name }} openlist_version: ${{ steps.openlist_version.outputs.openlist_version }} openlist_git_commit: ${{ steps.openlist_version.outputs.openlist_git_commit }} openlist_web_version: ${{ steps.openlist_version.outputs.openlist_web_version }} openlist_built_at: ${{ steps.openlist_version.outputs.openlist_built_at }} steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Generate Version Name id: generate run: | # Generate unified version with timestamp for entire workflow BASE_VERSION=$(cat openlist_version) TIMESTAMP=$(date +%y%m%d%H) VERSION_NAME="${BASE_VERSION}.${TIMESTAMP}" echo "version_name=${VERSION_NAME}" >> $GITHUB_OUTPUT - name: Download OpenList Source Code run: | cd $GITHUB_WORKSPACE/openlist-lib/scripts chmod +x *.sh ./init_openlist.sh - name: Extract OpenList Version Info id: openlist_version run: | # After init_openlist.sh, OpenList backend source is in openlist-lib/ with .git directory removed # We need to get version info from the latest remote tag that was cloned cd $GITHUB_WORKSPACE/openlist-lib/scripts # Re-fetch the tag information from remote (since .git was removed by init_openlist.sh) GIT_REPO="https://github.com/OpenListTeam/OpenList.git" OPENLIST_VERSION=$(git -c 'versionsort.suffix=-' ls-remote --exit-code --refs --sort='version:refname' --tags $GIT_REPO | tail -n 1 | cut -d'/' -f3) # Get the commit hash for this tag from remote OPENLIST_GIT_COMMIT=$(git ls-remote $GIT_REPO "refs/tags/${OPENLIST_VERSION}" | cut -f1 | cut -c1-7) # Get frontend version from OpenList-Frontend latest release OPENLIST_WEB_VERSION=$(curl -fsSL --max-time 10 "https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/"//g;s/,//g;s/ //g' || echo "rolling") # Build timestamp OPENLIST_BUILT_AT=$(date +'%F %T %z') echo "openlist_version=${OPENLIST_VERSION}" >> $GITHUB_OUTPUT echo "openlist_git_commit=${OPENLIST_GIT_COMMIT}" >> $GITHUB_OUTPUT echo "openlist_web_version=${OPENLIST_WEB_VERSION}" >> $GITHUB_OUTPUT echo "openlist_built_at=${OPENLIST_BUILT_AT}" >> $GITHUB_OUTPUT android: name: Build OpenList Android APK (Dev) needs: [version] runs-on: ubuntu-latest env: release_output: "${{ github.workspace }}/build/app/outputs/apk/release" debug_output: "${{ github.workspace }}/build/app/outputs/flutter-apk" steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Download OpenList Android AAR from latest build uses: dawidd6/action-download-artifact@v6 with: workflow: sync_openlist.yaml name: openlist-android-aar path: ${{ github.workspace }}/android/app/libs/ check_artifacts: true search_artifacts: true - uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - name: Setup Gradle uses: gradle/gradle-build-action@v3 - name: Init Signature (Release only) if: github.event_name != 'pull_request' run: | touch local.properties cd android echo ALIAS_NAME='${{ secrets.ALIAS_NAME }}' >> local.properties echo ALIAS_PASSWORD='${{ secrets.ALIAS_PASSWORD }}' >> local.properties echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' >> local.properties echo KEY_PATH='./key.jks' >> local.properties # 从Secrets读取无换行符Base64解码, 然后保存到到app/key.jks echo ${{ secrets.KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/android/app/key.jks - uses: subosito/flutter-action@v2 with: flutter-version: '3.32.7' - name: Generate iOS Build Number run: | IOS_BUILD_NUMBER=$(date +%y%m%d%H) echo "ios_build_number=$IOS_BUILD_NUMBER" >> $GITHUB_ENV - name: Build Release APK if: github.event_name != 'pull_request' env: BUILD_VERSION_NAME: ${{ needs.version.outputs.version_name }} run: flutter build apk --split-per-abi --release --target-platform android-arm,android-arm64,android-x64 - name: Build Debug APK if: github.event_name == 'pull_request' env: BUILD_VERSION_NAME: ${{ needs.version.outputs.version_name }} run: flutter build apk --split-per-abi --debug --target-platform android-arm,android-arm64,android-x64 - name: Upload missing_rules.txt if: failure() uses: actions/upload-artifact@v4 with: name: "missing_rules" path: "${{ github.workspace }}/build/app/outputs/mapping/release/missing_rules.txt" - name: Set Build Paths and Version run: | if [ "${{ github.event_name }}" == "pull_request" ]; then OUTPUT_DIR="${{ env.debug_output }}" BUILD_TYPE="debug" echo "ver_name=${{ needs.version.outputs.version_name }}-debug" >> $GITHUB_ENV else OUTPUT_DIR="${{ env.release_output }}" BUILD_TYPE="release" echo "ver_name=${{ needs.version.outputs.version_name }}" >> $GITHUB_ENV fi echo "output_path=$OUTPUT_DIR" >> $GITHUB_ENV echo "build_type=$BUILD_TYPE" >> $GITHUB_ENV - name: Upload App To Artifact arm64-v8a if: success () || failure () uses: actions/upload-artifact@v4 with: name: "OpenList-Mobile-${{ env.ver_name }}_arm64-v8a" path: | ${{ env.output_path }}/*arm64-v8a*.apk ${{ env.output_path }}/*v8a.apk - name: Upload App To Artifact arm-v7a if: success () || failure () uses: actions/upload-artifact@v4 with: name: "OpenList-Mobile-${{ env.ver_name }}_arm-v7a" path: | ${{ env.output_path }}/*armeabi-v7a*.apk ${{ env.output_path }}/*v7a.apk - name: Upload App To Artifact x86_64 if: success () || failure () uses: actions/upload-artifact@v4 with: name: "OpenList-Mobile-${{ env.ver_name }}_x86_64" path: | ${{ env.output_path }}/*x86_64*.apk ${{ env.output_path }}/*x64*.apk ios: name: Build OpenList iOS App (Dev) needs: [version] runs-on: macos-latest env: output: "${{ github.workspace }}/build/ios/ipa" testflight_ready: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && secrets.TESTFLIGHT_UPLOAD_ENABLED == 'true' && secrets.APP_STORE_CONNECT_KEY_ID != '' && secrets.APP_STORE_CONNECT_ISSUER_ID != '' && secrets.APP_STORE_CONNECT_KEY_P8 != '' && secrets.IOS_CERT_PFX != '' && secrets.IOS_CERT_PASSWORD != '' && secrets.IOS_PROVISION_PROFILE != '' }} steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Select Xcode 26.3.0 run: | XCODE_APP="/Applications/Xcode_26.3.0.app" if [ ! -d "$XCODE_APP" ]; then echo "Xcode_26.3.0 not found at $XCODE_APP" echo "Installed Xcode apps:" ls -1 /Applications | grep '^Xcode' || true exit 1 fi sudo xcode-select -s "$XCODE_APP/Contents/Developer" xcodebuild -version - name: Download OpenList iOS Framework from latest build uses: dawidd6/action-download-artifact@v6 with: workflow: sync_openlist.yaml name: openlist-ios-xcframework path: ${{ github.workspace }}/ios/Frameworks/ check_artifacts: true search_artifacts: true - uses: subosito/flutter-action@v2 with: flutter-version: '3.32.7' - name: Generate iOS Build Number run: | IOS_BUILD_NUMBER=$(date +%y%m%d%H) echo "ios_build_number=$IOS_BUILD_NUMBER" >> $GITHUB_ENV - name: Install iOS Signing Assets if: ${{ env.testflight_ready == 'true' }} env: IOS_CERT_PFX: ${{ secrets.IOS_CERT_PFX }} IOS_CERT_PASSWORD: ${{ secrets.IOS_CERT_PASSWORD }} IOS_PROVISION_PROFILE: ${{ secrets.IOS_PROVISION_PROFILE }} run: | KEYCHAIN_PATH="${RUNNER_TEMP}/openlist.keychain-db" KEYCHAIN_PASSWORD="temp_keychain_password" security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db security default-keychain -s "$KEYCHAIN_PATH" CERT_PATH="${RUNNER_TEMP}/ios_signing.pfx" CERT_PEM_PATH="${RUNNER_TEMP}/ios_signing.pem" CERT_P12_PATH="${RUNNER_TEMP}/ios_signing_mac.p12" echo "$IOS_CERT_PFX" | base64 --decode > "$CERT_PATH" if openssl pkcs12 -in "$CERT_PATH" -out "$CERT_PEM_PATH" -nodes -legacy -passin pass:"$IOS_CERT_PASSWORD"; then openssl pkcs12 -export -in "$CERT_PEM_PATH" -out "$CERT_P12_PATH" -passout pass:"$IOS_CERT_PASSWORD" -legacy else openssl pkcs12 -in "$CERT_PATH" -out "$CERT_PEM_PATH" -nodes -passin pass:"$IOS_CERT_PASSWORD" openssl pkcs12 -export -in "$CERT_PEM_PATH" -out "$CERT_P12_PATH" -passout pass:"$IOS_CERT_PASSWORD" fi security import "$CERT_P12_PATH" -k "$KEYCHAIN_PATH" -P "$IOS_CERT_PASSWORD" -A security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security find-identity -v -p codesigning "$KEYCHAIN_PATH" SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | head -n 1 | sed -E 's/.*"(.*)"/\1/') echo "signing_identity=$SIGNING_IDENTITY" >> $GITHUB_ENV echo "keychain_path=$KEYCHAIN_PATH" >> $GITHUB_ENV PROFILE_PATH="${RUNNER_TEMP}/openlist.mobileprovision" echo "$IOS_PROVISION_PROFILE" | base64 --decode > "$PROFILE_PATH" PROFILE_PLIST="${RUNNER_TEMP}/profile.plist" security cms -D -i "$PROFILE_PATH" > "$PROFILE_PLIST" PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print UUID" "$PROFILE_PLIST") PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print Name" "$PROFILE_PLIST") TEAM_ID=$(/usr/libexec/PlistBuddy -c "Print TeamIdentifier:0" "$PROFILE_PLIST") mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" cp "$PROFILE_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles/${PROFILE_UUID}.mobileprovision" echo "profile_name=$PROFILE_NAME" >> $GITHUB_ENV echo "team_id=$TEAM_ID" >> $GITHUB_ENV - name: Build iOS App (No CodeSign) if: ${{ env.testflight_ready != 'true' }} env: BUILD_VERSION_NAME: ${{ needs.version.outputs.version_name }} run: | echo "Building iOS app with CocoaPods..." flutter build ios --release --no-codesign --build-number "$ios_build_number" - name: Build iOS App (Signed for TestFlight) if: ${{ env.testflight_ready == 'true' }} env: BUILD_VERSION_NAME: ${{ needs.version.outputs.version_name }} run: | echo "Building signed iOS IPA for TestFlight..." BUNDLE_ID="${{ secrets.IOS_BUNDLE_ID }}" if [ -z "$BUNDLE_ID" ]; then BUNDLE_ID="org.oplist.app" fi flutter build ios --release --no-codesign --build-number "$ios_build_number" EXPORT_OPTIONS_PATH="${RUNNER_TEMP}/exportOptions.plist" /usr/libexec/PlistBuddy -c "Clear dict" "$EXPORT_OPTIONS_PATH" || true /usr/libexec/PlistBuddy -c "Add :method string app-store" "$EXPORT_OPTIONS_PATH" /usr/libexec/PlistBuddy -c "Add :signingStyle string manual" "$EXPORT_OPTIONS_PATH" /usr/libexec/PlistBuddy -c "Add :teamID string $team_id" "$EXPORT_OPTIONS_PATH" /usr/libexec/PlistBuddy -c "Add :signingCertificate string $signing_identity" "$EXPORT_OPTIONS_PATH" /usr/libexec/PlistBuddy -c "Add :provisioningProfiles dict" "$EXPORT_OPTIONS_PATH" /usr/libexec/PlistBuddy -c "Add :provisioningProfiles:$BUNDLE_ID string $profile_name" "$EXPORT_OPTIONS_PATH" ARCHIVE_PATH="${RUNNER_TEMP}/OpenList.xcarchive" xcodebuild \ -workspace ios/Runner.xcworkspace \ -scheme Runner \ -configuration Release \ -sdk iphoneos \ -archivePath "$ARCHIVE_PATH" \ archive \ CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_REQUIRED=NO xcodebuild \ -exportArchive \ -archivePath "$ARCHIVE_PATH" \ -exportOptionsPlist "$EXPORT_OPTIONS_PATH" \ -exportPath "${{ env.output }}" \ OTHER_CODE_SIGN_FLAGS="--keychain $keychain_path" - name: Create IPA if: ${{ env.testflight_ready != 'true' }} run: | mkdir -p ${{ env.output }} APP_PATH="" for candidate in \ "build/ios/iphoneos/Runner.app" \ "build/ios/Release-iphoneos/Runner.app" \ "build/ios/Release-unknown/Runner.app" \ "build/ios/archive/Runner.xcarchive/Products/Applications/Runner.app" do if [ -d "$candidate" ]; then APP_PATH="$candidate" break fi done if [ -z "$APP_PATH" ]; then echo "Runner.app not found in expected locations." echo "Available build/ios directories for diagnostics:" find build/ios -maxdepth 5 -type d || true exit 1 fi TMP_DIR=$(mktemp -d) mkdir -p "$TMP_DIR/Payload" cp -R "$APP_PATH" "$TMP_DIR/Payload/" ( cd "$TMP_DIR" zip -qry "${{ env.output }}/OpenList-Mobile.ipa" Payload/ ) - name: Normalize IPA Name if: ${{ env.testflight_ready == 'true' }} run: | mkdir -p ${{ env.output }} IPA_PATH=$(ls ${{ env.output }}/*.ipa | head -n 1) cp "$IPA_PATH" "${{ env.output }}/OpenList-Mobile.ipa" - name: Install Fastlane if: ${{ env.testflight_ready == 'true' }} run: sudo gem install fastlane -N - name: Prepare App Store Connect API Key if: ${{ env.testflight_ready == 'true' }} env: APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} run: | mkdir -p ${{ runner.temp }}/asc_key echo "${{ secrets.APP_STORE_CONNECT_KEY_P8 }}" | base64 --decode > ${{ runner.temp }}/asc_key/AuthKey.p8 python3 - <<'PY' import json import os key_path = os.path.join(os.environ['RUNNER_TEMP'], 'asc_key', 'AuthKey.p8') with open(key_path, 'r', encoding='utf-8') as f: key_content = f.read().strip() data = { 'key_id': os.environ.get('APP_STORE_CONNECT_KEY_ID', ''), 'issuer_id': os.environ.get('APP_STORE_CONNECT_ISSUER_ID', ''), 'key': key_content, } out_path = os.path.join(os.environ['RUNNER_TEMP'], 'asc_key', 'api_key.json') with open(out_path, 'w', encoding='utf-8') as f: json.dump(data, f) PY - name: Upload to TestFlight if: ${{ env.testflight_ready == 'true' }} run: | fastlane pilot upload \ --api_key_path "${{ runner.temp }}/asc_key/api_key.json" \ --ipa "${{ env.output }}/OpenList-Mobile.ipa" \ --skip_waiting_for_build_processing true - name: Set iOS Version run: | if [ "${{ github.event_name }}" == "pull_request" ]; then echo "ver_name=${{ needs.version.outputs.version_name }}-debug" >> $GITHUB_ENV else echo "ver_name=${{ needs.version.outputs.version_name }}" >> $GITHUB_ENV fi - name: Upload iOS App To Artifact if: success() || failure() uses: actions/upload-artifact@v4 with: name: "OpenList-Mobile-iOS-${{ env.ver_name }}" path: "${{ env.output }}/OpenList-Mobile.ipa" ================================================ FILE: .github/workflows/build_openlist.yaml ================================================ name: Build OpenList Libraries on: workflow_dispatch: workflow_call: permissions: contents: read jobs: version: name: Get OpenList Version Information runs-on: ubuntu-latest outputs: openlist_version: ${{ steps.openlist_version.outputs.openlist_version }} openlist_git_commit: ${{ steps.openlist_version.outputs.openlist_git_commit }} openlist_web_version: ${{ steps.openlist_version.outputs.openlist_web_version }} openlist_built_at: ${{ steps.openlist_version.outputs.openlist_built_at }} steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Download OpenList Source Code run: | cd $GITHUB_WORKSPACE/openlist-lib/scripts chmod +x *.sh ./init_openlist.sh - name: Extract OpenList Version Info id: openlist_version run: | cd $GITHUB_WORKSPACE/openlist-lib/scripts GIT_REPO="https://github.com/OpenListTeam/OpenList.git" OPENLIST_VERSION=$(git -c 'versionsort.suffix=-' ls-remote --exit-code --refs --sort='version:refname' --tags $GIT_REPO | tail -n 1 | cut -d'/' -f3) OPENLIST_GIT_COMMIT=$(git ls-remote $GIT_REPO "refs/tags/${OPENLIST_VERSION}" | cut -f1 | cut -c1-7) OPENLIST_WEB_VERSION=$(curl -fsSL --max-time 10 "https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/"//g;s/,//g;s/ //g' || echo "rolling") OPENLIST_BUILT_AT=$(date +'%F %T %z') echo "openlist_version=${OPENLIST_VERSION}" >> $GITHUB_OUTPUT echo "openlist_git_commit=${OPENLIST_GIT_COMMIT}" >> $GITHUB_OUTPUT echo "openlist_web_version=${OPENLIST_WEB_VERSION}" >> $GITHUB_OUTPUT echo "openlist_built_at=${OPENLIST_BUILT_AT}" >> $GITHUB_OUTPUT build_android: name: Build OpenList Android AAR needs: [version] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Download OpenList Source Code run: | cd $GITHUB_WORKSPACE/openlist-lib/scripts chmod +x *.sh ./init_openlist.sh ./init_web.sh - uses: actions/setup-go@v5 with: go-version: 1.25.0 - uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - uses: nttld/setup-ndk@v1 id: setup-ndk with: ndk-version: r25c - name: Setup Gradle uses: gradle/gradle-build-action@v3 - name: Build OpenList Android AAR run: | cd $GITHUB_WORKSPACE/openlist-lib/scripts chmod +x *.sh ./init_gomobile.sh ./gobind.sh env: ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} OPENLIST_VERSION: ${{ needs.version.outputs.openlist_version }} OPENLIST_WEB_VERSION: ${{ needs.version.outputs.openlist_web_version }} OPENLIST_BUILT_AT: ${{ needs.version.outputs.openlist_built_at }} OPENLIST_GIT_COMMIT: ${{ needs.version.outputs.openlist_git_commit }} OPENLIST_GIT_AUTHOR: The OpenList Projects Contributors - name: Upload OpenList Android AAR uses: actions/upload-artifact@v4 with: name: "openlist-android-aar" path: "${{ github.workspace }}/android/app/libs/*.aar" retention-days: 50 build_ios: name: Build OpenList iOS Framework needs: [version] runs-on: macos-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - uses: actions/setup-go@v5 with: go-version: 1.25.0 - name: Download OpenList Source Code run: | cd $GITHUB_WORKSPACE/openlist-lib/scripts chmod +x *.sh ./init_openlist.sh ./init_web_ios.sh - name: Build OpenList iOS Framework run: | cd $GITHUB_WORKSPACE/openlist-lib/scripts chmod +x *.sh ./init_gomobile.sh ./gobind_ios.sh env: OPENLIST_VERSION: ${{ needs.version.outputs.openlist_version }} OPENLIST_WEB_VERSION: ${{ needs.version.outputs.openlist_web_version }} OPENLIST_BUILT_AT: ${{ needs.version.outputs.openlist_built_at }} OPENLIST_GIT_COMMIT: ${{ needs.version.outputs.openlist_git_commit }} OPENLIST_GIT_AUTHOR: The OpenList Projects Contributors - name: Upload OpenList iOS Framework uses: actions/upload-artifact@v4 with: name: "openlist-ios-xcframework" path: "${{ github.workspace }}/ios/Frameworks/*.xcframework" retention-days: 50 ================================================ FILE: .github/workflows/release.yaml ================================================ name: Release on: workflow_dispatch: permissions: contents: write jobs: version: name: Get OpenList Version Information runs-on: ubuntu-latest outputs: version_name: ${{ steps.generate.outputs.version_name }} openlist_version: ${{ steps.openlist_version.outputs.openlist_version }} openlist_git_commit: ${{ steps.openlist_version.outputs.openlist_git_commit }} openlist_web_version: ${{ steps.openlist_version.outputs.openlist_web_version }} openlist_built_at: ${{ steps.openlist_version.outputs.openlist_built_at }} steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Generate Version Name id: generate run: | # Generate unified version with timestamp for entire workflow BASE_VERSION=$(cat openlist_version) TIMESTAMP=$(date +%y%m%d%H) VERSION_NAME="${BASE_VERSION}.${TIMESTAMP}" echo "version_name=${VERSION_NAME}" >> $GITHUB_OUTPUT - name: Download OpenList Source Code run: | cd $GITHUB_WORKSPACE/openlist-lib/scripts chmod +x *.sh ./init_openlist.sh - name: Extract OpenList Version Info id: openlist_version run: | # After init_openlist.sh, OpenList backend source is in openlist-lib/ with .git directory removed # We need to get version info from the latest remote tag that was cloned cd $GITHUB_WORKSPACE/openlist-lib/scripts # Re-fetch the tag information from remote (since .git was removed by init_openlist.sh) GIT_REPO="https://github.com/OpenListTeam/OpenList.git" OPENLIST_VERSION=$(git -c 'versionsort.suffix=-' ls-remote --exit-code --refs --sort='version:refname' --tags $GIT_REPO | tail -n 1 | cut -d'/' -f3) # Get the commit hash for this tag from remote OPENLIST_GIT_COMMIT=$(git ls-remote $GIT_REPO "refs/tags/${OPENLIST_VERSION}" | cut -f1 | cut -c1-7) # Get frontend version from OpenList-Frontend latest release OPENLIST_WEB_VERSION=$(curl -fsSL --max-time 10 "https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/"//g;s/,//g;s/ //g' || echo "rolling") # Build timestamp OPENLIST_BUILT_AT=$(date +'%F %T %z') echo "openlist_version=${OPENLIST_VERSION}" >> $GITHUB_OUTPUT echo "openlist_git_commit=${OPENLIST_GIT_COMMIT}" >> $GITHUB_OUTPUT echo "openlist_web_version=${OPENLIST_WEB_VERSION}" >> $GITHUB_OUTPUT echo "openlist_built_at=${OPENLIST_BUILT_AT}" >> $GITHUB_OUTPUT android: name: Build OpenList Android APK needs: [version] runs-on: ubuntu-latest env: output: "${{ github.workspace }}/build/app/outputs/apk/release" steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Download OpenList Android AAR from latest build uses: dawidd6/action-download-artifact@v6 with: workflow: sync_openlist.yaml name: openlist-android-aar path: ${{ github.workspace }}/android/app/libs/ check_artifacts: true search_artifacts: true - uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - name: Setup Gradle uses: gradle/gradle-build-action@v3 - name: Init Signature run: | touch local.properties cd android echo ALIAS_NAME='${{ secrets.ALIAS_NAME }}' >> local.properties echo ALIAS_PASSWORD='${{ secrets.ALIAS_PASSWORD }}' >> local.properties echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' >> local.properties echo KEY_PATH='./key.jks' >> local.properties # 从Secrets读取无换行符Base64解码, 然后保存到到app/key.jks echo ${{ secrets.KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/android/app/key.jks - uses: subosito/flutter-action@v2 with: flutter-version: '3.32.7' - name: Build APK with Version env: BUILD_VERSION_NAME: ${{ needs.version.outputs.version_name }} run: flutter build apk --split-per-abi --release - name: Upload missing_rules.txt if: failure() uses: actions/upload-artifact@v4 with: name: "missing_rules" path: "${{ github.workspace }}/build/app/outputs/mapping/release/missing_rules.txt" - name: Upload Android APKs for Release uses: actions/upload-artifact@v4 with: name: "android-release-files" path: "${{ env.output }}/*.apk" # ios: # name: Build OpenList iOS App # needs: [version] # runs-on: macos-latest # env: # output: "${{ github.workspace }}/build/ios/ipa" # steps: # - uses: actions/checkout@v3 # with: # fetch-depth: 0 # # - name: Download OpenList iOS Framework from latest build # uses: dawidd6/action-download-artifact@v6 # with: # workflow: sync_openlist.yaml # name: openlist-ios-xcframework # path: ${{ github.workspace }}/ios/Frameworks/ # check_artifacts: true # search_artifacts: true # - uses: subosito/flutter-action@v2 # with: # flutter-version: '3.32.7' # - name: Build iOS App # env: # BUILD_VERSION_NAME: ${{ needs.version.outputs.version_name }} # run: | # flutter build ios --release --no-codesign # - name: Create IPA # run: | # mkdir -p ${{ env.output }} # cd build/ios/iphoneos # mkdir Payload # cp -r Runner.app Payload/ # zip -r ${{ env.output }}/OpenList-Mobile.ipa Payload/ # - name: Upload iOS IPA for Release # uses: actions/upload-artifact@v4 # with: # name: "ios-release-files" # path: "${{ env.output }}/OpenList-Mobile.ipa" release: name: Create GitHub Release needs: [version, android] # Add ios when iOS build is enabled: [version, android, ios] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Setup Node uses: actions/setup-node@v4 with: node-version: "22" - name: Prepare changelog generation id: prepare_changelog run: | git tag -d rolling 2>/dev/null || true PRE_RELEASE_TAGS=$(git tag -l | grep -E "(-|\+)" || true) if [ -n "$PRE_RELEASE_TAGS" ]; then echo "$PRE_RELEASE_TAGS" | xargs -r git tag -d fi - name: Generate changelog id: generate_changelog run: | npx changelogithub --output ${{ github.workspace }}/GENERATED_CHANGELOG.txt || echo "" > ${{ github.workspace }}/GENERATED_CHANGELOG.txt - name: Create final changelog id: create_changelog run: | echo "[Auto Sync OpenList] ${{ needs.version.outputs.openlist_version }}" > ${{ github.workspace }}/FINAL_CHANGELOG.txt echo "" >> ${{ github.workspace }}/FINAL_CHANGELOG.txt echo "**OpenList Backend:** ${{ needs.version.outputs.openlist_version }} (${{ needs.version.outputs.openlist_git_commit }})" >> ${{ github.workspace }}/FINAL_CHANGELOG.txt echo "**OpenList Frontend:** ${{ needs.version.outputs.openlist_web_version }}" >> ${{ github.workspace }}/FINAL_CHANGELOG.txt echo "**Built at:** ${{ needs.version.outputs.openlist_built_at }}" >> ${{ github.workspace }}/FINAL_CHANGELOG.txt echo "" >> ${{ github.workspace }}/FINAL_CHANGELOG.txt echo "---" >> ${{ github.workspace }}/FINAL_CHANGELOG.txt echo "" >> ${{ github.workspace }}/FINAL_CHANGELOG.txt if [ -s "${{ github.workspace }}/GENERATED_CHANGELOG.txt" ]; then cat ${{ github.workspace }}/GENERATED_CHANGELOG.txt >> ${{ github.workspace }}/FINAL_CHANGELOG.txt else echo "No changes in this release." >> ${{ github.workspace }}/FINAL_CHANGELOG.txt fi - name: Download Android artifacts uses: actions/download-artifact@v4 with: name: android-release-files path: release_files/ # - name: Download iOS artifacts # uses: actions/download-artifact@v4 # with: # name: ios-release-files # path: release_files/ - uses: softprops/action-gh-release@v1 with: name: ${{ needs.version.outputs.version_name }} tag_name: ${{ needs.version.outputs.version_name }} body_path: ${{ github.workspace }}/FINAL_CHANGELOG.txt draft: false prerelease: false files: release_files/* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/sync_openlist.yaml ================================================ name: Check Updates on: schedule: - cron: "0 5,17 * * *" # 每日5点和17点执行 workflow_dispatch: inputs: force_build: description: 'Force build OpenList libraries' required: false type: boolean default: false push: branches: - "master" paths: - "sync_openlist.yaml" permissions: contents: write actions: write jobs: check_and_update: name: Check for version updates runs-on: ubuntu-latest outputs: openlist_update: ${{ steps.set_output.outputs.openlist_update }} openlist_version: ${{ steps.set_output.outputs.openlist_version }} env: VERSION_FILE: ${{ github.workspace }}/openlist_version steps: - uses: actions/checkout@v3 - name: Check OpenList Version run: | cd $GITHUB_WORKSPACE/.github/scripts chmod +x ./*.sh touch ${{ env.VERSION_FILE }} ./check_openlist.sh - name: Set Output Variables id: set_output run: | echo "openlist_version=${{ env.openlist_version }}" >> $GITHUB_OUTPUT echo "openlist_update=${{ env.openlist_update }}" >> $GITHUB_OUTPUT - name: Import GPG key if: env.openlist_update == '1' uses: crazy-max/ghaction-import-gpg@v6 with: gpg_private_key: ${{ secrets.BOT_GPG_PRIVATE_KEY }} passphrase: ${{ secrets.BOT_GPG_PASSPHRASE }} git_user_signingkey: true git_commit_gpgsign: true git_tag_gpgsign: true - name: Update Version Files if: env.openlist_update == '1' run: | echo -e "${{ env.openlist_version }}" > ${{ env.VERSION_FILE }} chmod +x $GITHUB_WORKSPACE/.github/scripts/update_pubspec_version.sh $GITHUB_WORKSPACE/.github/scripts/update_pubspec_version.sh git config user.name "${{ secrets.BOT_USERNAME }}" git config user.email "${{ secrets.BOT_USEREMAIL }}" git add . git commit -m "[bot] Update openlist to ${{ env.openlist_version }}" git push build_openlist_libraries: name: Build OpenList Libraries needs: [check_and_update] if: needs.check_and_update.outputs.openlist_update == '1' || inputs.force_build == true uses: ./.github/workflows/build_openlist.yaml trigger_release: name: Trigger Release Workflow needs: [check_and_update, build_openlist_libraries] if: needs.check_and_update.outputs.openlist_update == '1' && ( success() || failure() ) runs-on: ubuntu-latest steps: - name: Trigger Release Workflow run: | gh workflow run release.yaml -R ${{ github.repository }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ migrate_working_dir/ docs/ .github/prompts/ CHANGELOG.md # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release local.properties *.aar *.exe *.tgz *.jar *.zip *.so android/app/.cxx android/app/build ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "fcf2c11572af6f390246c056bc905eca609533a0" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: fcf2c11572af6f390246c056bc905eca609533a0 base_revision: fcf2c11572af6f390246c056bc905eca609533a0 - platform: ios create_revision: fcf2c11572af6f390246c056bc905eca609533a0 base_revision: fcf2c11572af6f390246c056bc905eca609533a0 # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: README.md ================================================
OpenList Logo

OpenList-Mobile

[![Release](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/release.yaml/badge.svg)](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/release.yaml) [![Build](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/build.yaml/badge.svg)](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/build.yaml) [![Sync OpenList](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/sync_openlist.yaml/badge.svg)](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/sync_openlist.yaml) [![License](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE) [![Flutter](https://img.shields.io/badge/Flutter-3.32.7-blue.svg)](https://flutter.dev/) [![OpenList](https://img.shields.io/github/v/release/OpenListTeam/OpenList?label=OpenList)](https://github.com/OpenListTeam/OpenList)
**OpenList-Mobile** 是一个基于 [OpenList](https://github.com/OpenListTeam/OpenList) 的移动端文件服务器应用,使用 Flutter 框架开发。支持局域网文件共享、远程访问和在线管理。 ### 下载安装 #### 稳定版本 - [📱 发布版](https://github.com/OpenListTeam/OpenList-Mobile/releases/latest) - 推荐使用 #### 开发版本 - [🔧 构建版](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/build.yaml) - 最新功能 > **自动更新**:[GitHub Actions](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/sync_openlist.yaml) 每日早晚五点自动检查最新的 [OpenList](https://github.com/OpenListTeam/OpenList/releases) 版本并构建发布,确保始终使用最新版本。 ### 支持平台 - Android - iOS (实验性,仍在开发) ================================================ FILE: README_EN.md ================================================
OpenList Logo

OpenList-Mobile

[![Release](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/release.yaml/badge.svg)](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/release.yaml) [![Build](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/build.yaml/badge.svg)](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/build.yaml) [![Sync OpenList](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/sync_openlist.yaml/badge.svg)](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/sync_openlist.yaml) [![License](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE) [![Flutter](https://img.shields.io/badge/Flutter-3.32.7-blue.svg)](https://flutter.dev/) [![OpenList](https://img.shields.io/github/v/release/OpenListTeam/OpenList?label=OpenList)](https://github.com/OpenListTeam/OpenList)
**OpenList-Mobile** is a mobile file server application based on [OpenList](https://github.com/OpenListTeam/OpenList), built with Flutter framework. Turn your phone into a powerful file server with LAN file sharing, remote access, and online management capabilities. ### Download & Installation #### Stable Release - [📱 Latest Release](https://github.com/OpenListTeam/OpenList-Mobile/releases/latest) - Recommended #### Development Build - [🔧 Development Build](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/build.yaml) - Latest Features > **Auto-Update**: [GitHub Actions](https://github.com/OpenListTeam/OpenList-Mobile/actions/workflows/sync_openlist.yaml) automatically checks for the latest [OpenList](https://github.com/OpenListTeam/OpenList/releases) version twice daily (5 AM & 5 PM) and builds releases, ensuring always have access to the latest version. ### Supported Platforms - Android - iOS (Experimental, still in development) ================================================ FILE: analysis_options.yaml ================================================ # This file configures the analyzer, which statically analyzes Dart code to # check for errors, warnings, and lints. # # The issues identified by the analyzer are surfaced in the UI of Dart-enabled # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at https://dart.dev/lints. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties **/*.keystore **/*.jks *.aar *.exe *.tgz *.jar *.zip *.so ================================================ FILE: android/app/build.gradle ================================================ plugins { id "com.android.application" id "kotlin-android" id "kotlin-parcelize" id "kotlin-kapt" id "dev.flutter.flutter-gradle-plugin" id "kotlinx-serialization" } def pro = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> pro.load(reader) } } static def releaseTime() { return new Date().format("yyMMddHH", TimeZone.getTimeZone("GMT+8")) } def openlistVersionFile = rootProject.file("../openlist_version") def openlistVersion = openlistVersionFile.exists() ? openlistVersionFile.readLines()[0] : "1.0.0" // Check if version is provided via environment variable (from CI/CD) def providedVersion = System.getenv('BUILD_VERSION_NAME') def baseVersion = openlistVersion.startsWith('v') ? openlistVersion.substring(1) : openlistVersion // Use provided version if available, otherwise generate with timestamp def version = providedVersion ?: (baseVersion + "." + releaseTime()) def gitCommits = Integer.parseInt('git rev-list HEAD --count'.execute().text.trim()) android { namespace "com.openlist.mobile" compileSdk = 35 ndkVersion = "27.0.12077973" signingConfigs { release { if (pro["KEY_PATH"] != null && pro["KEY_PASSWORD"] != null && pro["ALIAS_NAME"] != null && pro["ALIAS_PASSWORD"] != null) { storeFile file(pro["KEY_PATH"]) storePassword pro["KEY_PASSWORD"] keyAlias pro["ALIAS_NAME"] keyPassword pro["ALIAS_PASSWORD"] } } } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.openlist.mobile" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdk 21 targetSdk flutter.targetSdkVersion versionCode gitCommits versionName version buildConfigField("String", "OPENLIST_VERSION", "\"${openlistVersion}\"") } buildFeatures { buildConfig true } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 coreLibraryDesugaringEnabled true } kotlinOptions { jvmTarget = '17' } sourceSets { main { jniLibs.srcDirs = ['libs'] java.srcDirs = ['src/main/java', 'src/main/kotlin'] } } splits { abi { enable true // 改为始终启用,而不是根据任务名判断 reset() include 'armeabi-v7a', 'arm64-v8a', 'x86_64' universalApk false // 改为false避免生成通用APK } } // kotlin { // jvmToolchain = 17 // } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. if (pro["KEY_PATH"] != null && pro["KEY_PASSWORD"] != null && pro["ALIAS_NAME"] != null && pro["ALIAS_PASSWORD"] != null) { signingConfig signingConfigs.release } } debug { ndk { //noinspection ChromeOsAbiSupport // abiFilters "arm64-v8a" // abiFilters "x86" } } } } androidComponents { onVariants(selector().all()) { variant -> variant.outputs.forEach { output -> def versionName = output.versionName.orNull ?: "1.0.0" def abi = "" // 简化ABI过滤器检查逻辑 if (output.filters != null && !output.filters.isEmpty()) { for (filter in output.filters) { if (filter.filterType.name() == "ABI") { abi = "_${filter.identifier}" break } } } output.outputFileName.set("OpenList-Mobile-${versionName}${abi}.apk") } } } flutter { source '../..' } ////获取flutter的sdk路径 //def flutterRoot = localProperties.getProperty('flutter.sdk') //if (flutterRoot == null) { // throw new Exception("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") //} dependencies { implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') // compileOnly files("$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar") implementation project(":utils") //noinspection GradleDependency implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' implementation 'com.louiscad.splitties:splitties-systemservices:3.0.0' implementation 'com.github.cioccarellia:ksprefs:2.4.0' implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") // Core library desugaring for flutter_local_notifications coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' // WorkManager for background tasks implementation 'androidx.work:work-runtime-ktx:2.9.0' // Room // implementation("androidx.room:room-runtime:$room_version") // implementation("androidx.room:room-ktx:$room_version") // ksp("androidx.room:room-compiler:$room_version") // androidTestImplementation("androidx.room:room-testing:$room_version") } ================================================ FILE: android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/java/com/openlist/pigeon/GeneratedApi.java ================================================ // Autogenerated from Pigeon (v16.0.0), do not edit directly. // See also: https://pub.dev/packages/pigeon package com.openlist.pigeon; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.plugin.common.BasicMessageChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MessageCodec; import io.flutter.plugin.common.StandardMessageCodec; import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** Generated class from Pigeon. */ @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) public class GeneratedApi { /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ public static class FlutterError extends RuntimeException { /** The error code. */ public final String code; /** The error details. Must be a datatype supported by the api codec. */ public final Object details; public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) { super(message); this.code = code; this.details = details; } } @NonNull protected static ArrayList wrapError(@NonNull Throwable exception) { ArrayList errorList = new ArrayList(3); if (exception instanceof FlutterError) { FlutterError error = (FlutterError) exception; errorList.add(error.code); errorList.add(error.getMessage()); errorList.add(error.details); } else { errorList.add(exception.toString()); errorList.add(exception.getClass().getSimpleName()); errorList.add( "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); } return errorList; } @NonNull protected static FlutterError createConnectionError(@NonNull String channelName) { return new FlutterError("channel-error", "Unable to establish connection on channel: " + channelName + ".", ""); } /** Asynchronous error handling return type for non-nullable API method returns. */ public interface Result { /** Success case callback method for handling returns. */ void success(@NonNull T result); /** Failure case callback method for handling errors. */ void error(@NonNull Throwable error); } /** Asynchronous error handling return type for nullable API method returns. */ public interface NullableResult { /** Success case callback method for handling returns. */ void success(@Nullable T result); /** Failure case callback method for handling errors. */ void error(@NonNull Throwable error); } /** Asynchronous error handling return type for void API method returns. */ public interface VoidResult { /** Success case callback method for handling returns. */ void success(); /** Failure case callback method for handling errors. */ void error(@NonNull Throwable error); } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface AppConfig { @NonNull Boolean isWakeLockEnabled(); void setWakeLockEnabled(@NonNull Boolean enabled); @NonNull Boolean isStartAtBootEnabled(); void setStartAtBootEnabled(@NonNull Boolean enabled); @NonNull Boolean isAutoCheckUpdateEnabled(); void setAutoCheckUpdateEnabled(@NonNull Boolean enabled); @NonNull Boolean isAutoOpenWebPageEnabled(); void setAutoOpenWebPageEnabled(@NonNull Boolean enabled); @NonNull String getDataDir(); void setDataDir(@NonNull String dir); @NonNull Boolean isSilentJumpAppEnabled(); void setSilentJumpAppEnabled(@NonNull Boolean enabled); /** The codec used by AppConfig. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } /**Sets up an instance of `AppConfig` to handle messages through the `binaryMessenger`. */ static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable AppConfig api) { { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.AppConfig.isWakeLockEnabled", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { Boolean output = api.isWakeLockEnabled(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.AppConfig.setWakeLockEnabled", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; Boolean enabledArg = (Boolean) args.get(0); try { api.setWakeLockEnabled(enabledArg); wrapped.add(0, null); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.AppConfig.isStartAtBootEnabled", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { Boolean output = api.isStartAtBootEnabled(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.AppConfig.setStartAtBootEnabled", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; Boolean enabledArg = (Boolean) args.get(0); try { api.setStartAtBootEnabled(enabledArg); wrapped.add(0, null); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.AppConfig.isAutoCheckUpdateEnabled", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { Boolean output = api.isAutoCheckUpdateEnabled(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.AppConfig.setAutoCheckUpdateEnabled", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; Boolean enabledArg = (Boolean) args.get(0); try { api.setAutoCheckUpdateEnabled(enabledArg); wrapped.add(0, null); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.AppConfig.isAutoOpenWebPageEnabled", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { Boolean output = api.isAutoOpenWebPageEnabled(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.AppConfig.setAutoOpenWebPageEnabled", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; Boolean enabledArg = (Boolean) args.get(0); try { api.setAutoOpenWebPageEnabled(enabledArg); wrapped.add(0, null); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.AppConfig.getDataDir", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { String output = api.getDataDir(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.AppConfig.setDataDir", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; String dirArg = (String) args.get(0); try { api.setDataDir(dirArg); wrapped.add(0, null); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.AppConfig.isSilentJumpAppEnabled", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { Boolean output = api.isSilentJumpAppEnabled(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.AppConfig.setSilentJumpAppEnabled", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; Boolean enabledArg = (Boolean) args.get(0); try { api.setSilentJumpAppEnabled(enabledArg); wrapped.add(0, null); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface NativeCommon { @NonNull Boolean startActivityFromUri(@NonNull String intentUri); @NonNull Long getDeviceSdkInt(); @NonNull String getDeviceCPUABI(); @NonNull String getVersionName(); @NonNull Long getVersionCode(); void toast(@NonNull String msg); void longToast(@NonNull String msg); /** The codec used by NativeCommon. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } /**Sets up an instance of `NativeCommon` to handle messages through the `binaryMessenger`. */ static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable NativeCommon api) { { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.NativeCommon.startActivityFromUri", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; String intentUriArg = (String) args.get(0); try { Boolean output = api.startActivityFromUri(intentUriArg); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.NativeCommon.getDeviceSdkInt", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { Long output = api.getDeviceSdkInt(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.NativeCommon.getDeviceCPUABI", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { String output = api.getDeviceCPUABI(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.NativeCommon.getVersionName", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { String output = api.getVersionName(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.NativeCommon.getVersionCode", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { Long output = api.getVersionCode(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.NativeCommon.toast", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; String msgArg = (String) args.get(0); try { api.toast(msgArg); wrapped.add(0, null); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.NativeCommon.longToast", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; String msgArg = (String) args.get(0); try { api.longToast(msgArg); wrapped.add(0, null); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface Android { void addShortcut(); void startService(); void setAdminPwd(@NonNull String pwd); @NonNull Long getOpenListHttpPort(); @NonNull Boolean isRunning(); @NonNull String getOpenListVersion(); /** The codec used by Android. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } /**Sets up an instance of `Android` to handle messages through the `binaryMessenger`. */ static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable Android api) { { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.Android.addShortcut", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { api.addShortcut(); wrapped.add(0, null); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.Android.startService", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { api.startService(); wrapped.add(0, null); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.Android.setAdminPwd", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; String pwdArg = (String) args.get(0); try { api.setAdminPwd(pwdArg); wrapped.add(0, null); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.Android.getOpenListHttpPort", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { Long output = api.getOpenListHttpPort(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.Android.isRunning", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { Boolean output = api.isRunning(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.openlist_mobile.Android.getOpenListVersion", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); try { String output = api.getOpenListVersion(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; } reply.reply(wrapped); }); } else { channel.setMessageHandler(null); } } } } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class Event { private final @NonNull BinaryMessenger binaryMessenger; public Event(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } /** Public interface for sending reply. */ /** The codec used by Event. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } public void onServiceStatusChanged(@NonNull Boolean isRunningArg, @NonNull VoidResult result) { final String channelName = "dev.flutter.pigeon.openlist_mobile.Event.onServiceStatusChanged"; BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, channelName, getCodec()); channel.send( new ArrayList(Collections.singletonList(isRunningArg)), channelReply -> { if (channelReply instanceof List) { List listReply = (List) channelReply; if (listReply.size() > 1) { result.error(new FlutterError((String) listReply.get(0), (String) listReply.get(1), (String) listReply.get(2))); } else { result.success(); } } else { result.error(createConnectionError(channelName)); } }); } public void onServerLog(@NonNull Long levelArg, @NonNull String timeArg, @NonNull String logArg, @NonNull VoidResult result) { final String channelName = "dev.flutter.pigeon.openlist_mobile.Event.onServerLog"; BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, channelName, getCodec()); channel.send( new ArrayList(Arrays.asList(levelArg, timeArg, logArg)), channelReply -> { if (channelReply instanceof List) { List listReply = (List) channelReply; if (listReply.size() > 1) { result.error(new FlutterError((String) listReply.get(0), (String) listReply.get(1), (String) listReply.get(2))); } else { result.success(); } } else { result.error(createConnectionError(channelName)); } }); } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/App.kt ================================================ package com.openlist.mobile import android.app.Application import android.util.Log import com.openlist.mobile.model.openlist.OpenList import com.openlist.mobile.utils.ToastUtils.longToast import io.flutter.app.FlutterApplication val app by lazy { App.app } class App : FlutterApplication() { companion object { private const val TAG = "App" lateinit var app: Application } override fun onCreate() { super.onCreate() app = this // Early initialization of OpenList to prepare for boot startup try { Log.d(TAG, "Performing early OpenList initialization") OpenList.init() Log.d(TAG, "OpenList early initialization completed") } catch (e: Exception) { Log.e(TAG, "Failed to initialize OpenList early", e) } // Set global exception handler to catch uncaught exceptions Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> Log.e(TAG, "Uncaught exception in thread ${thread.name}", throwable) // Log detailed info for JNI related errors if (throwable.message?.contains("JNI") == true || throwable.message?.contains("native") == true || throwable is UnsatisfiedLinkError) { Log.e(TAG, "Native/JNI related crash detected") } // Call default exception handler val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() defaultHandler?.uncaughtException(thread, throwable) } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/BootReceiver.kt ================================================ package com.openlist.mobile import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build import android.util.Log import com.openlist.mobile.config.AppConfig /** * Boot receiver - handles device boot and package update events */ class BootReceiver : BroadcastReceiver() { companion object { private const val TAG = "BootReceiver" } override fun onReceive(context: Context?, intent: Intent?) { if (context == null || intent?.action == null) return Log.d(TAG, "Received broadcast: ${intent.action}") when (intent.action) { Intent.ACTION_BOOT_COMPLETED -> { handleBootCompleted(context) } Intent.ACTION_MY_PACKAGE_REPLACED -> { handlePackageReplaced(context) } } } private fun handleBootCompleted(context: Context) { if (!AppConfig.isStartAtBootEnabled) { Log.d(TAG, "Auto-start disabled, skipping") return } // Clear manual stop flag on boot AppConfig.isManuallyStoppedByUser = false Log.d(TAG, "Starting OpenList service") startService(context) } private fun handlePackageReplaced(context: Context) { if (!AppConfig.isStartAtBootEnabled) { Log.d(TAG, "Auto-start disabled, skipping package update restart") return } Log.d(TAG, "Starting OpenList service after package update") startService(context) } private fun startService(context: Context) { try { val serviceIntent = Intent(context, OpenListService::class.java) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(serviceIntent) } else { context.startService(serviceIntent) } Log.d(TAG, "Service start command sent") } catch (e: Exception) { Log.e(TAG, "Failed to start service", e) } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/MainActivity.kt ================================================ package com.openlist.mobile import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Bundle import android.util.Log import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.openlist.mobile.bridge.AndroidBridge import com.openlist.mobile.bridge.AppConfigBridge import com.openlist.mobile.bridge.CommonBridge import com.openlist.mobile.bridge.ServiceBridge import com.openlist.mobile.model.ShortCuts import com.openlist.mobile.model.openlist.Logger import com.openlist.pigeon.GeneratedApi import com.openlist.pigeon.GeneratedApi.VoidResult import io.flutter.embedding.android.FlutterActivity import io.flutter.plugin.common.MethodChannel import io.flutter.plugins.GeneratedPluginRegistrant import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch class MainActivity : FlutterActivity() { companion object { private const val TAG = "MainActivity" // 静态引用,供其他组件访问 @Volatile var serviceBridge: ServiceBridge? = null private set } private val receiver by lazy { MyReceiver() } private var mEvent: GeneratedApi.Event? = null @OptIn(DelicateCoroutinesApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ShortCuts.buildShortCuts(this) LocalBroadcastManager.getInstance(this) .registerReceiver(receiver, IntentFilter(OpenListService.ACTION_STATUS_CHANGED)) GeneratedPluginRegistrant.registerWith(this.flutterEngine!!) val binaryMessage = flutterEngine!!.dartExecutor.binaryMessenger GeneratedApi.AppConfig.setUp(binaryMessage, AppConfigBridge) GeneratedApi.Android.setUp(binaryMessage, AndroidBridge(this)) GeneratedApi.NativeCommon.setUp(binaryMessage, CommonBridge(this)) mEvent = GeneratedApi.Event(binaryMessage) // 设置服务桥接 val serviceChannel = MethodChannel(binaryMessage, "com.openlist.mobile/service") serviceBridge = ServiceBridge(this, serviceChannel) Logger.addListener(object : Logger.Listener { override fun onLog(level: Int, time: String, msg: String) { GlobalScope.launch(Dispatchers.Main) { mEvent?.onServerLog(level.toLong(), time, msg, object : VoidResult { override fun success() { } override fun error(error: Throwable) { } }) } } }) } override fun onPause() { super.onPause() // Trigger database sync when app goes to background triggerDatabaseSync("onPause") } override fun onStop() { super.onStop() // Trigger database sync when app is stopped triggerDatabaseSync("onStop") } override fun onTrimMemory(level: Int) { super.onTrimMemory(level) // Trigger database sync on memory pressure when (level) { TRIM_MEMORY_UI_HIDDEN, TRIM_MEMORY_BACKGROUND, TRIM_MEMORY_MODERATE, TRIM_MEMORY_COMPLETE -> { triggerDatabaseSync("onTrimMemory:$level") } } } override fun onDestroy() { super.onDestroy() // Trigger database sync before activity is destroyed triggerDatabaseSync("onDestroy") LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) } /** * Trigger database synchronization through the service */ private fun triggerDatabaseSync(reason: String) { try { val serviceInstance = OpenListService.serviceInstance if (serviceInstance != null && OpenListService.isRunning) { Log.d(TAG, "Triggering database sync due to: $reason") serviceInstance.forceImmediateDbSync() } else { Log.d(TAG, "Service not running, skipping database sync for: $reason") } } catch (e: Exception) { Log.e(TAG, "Failed to trigger database sync for $reason", e) } } inner class MyReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { OpenListService.ACTION_STATUS_CHANGED -> { Log.d(TAG, "onReceive: ACTION_STATUS_CHANGED") mEvent?.onServiceStatusChanged(OpenListService.isRunning, object : VoidResult { override fun success() {} override fun error(error: Throwable) { } }) } } } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/OpenListService.kt ================================================ package com.openlist.mobile import openlistlib.Openlistlib import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build import android.os.IBinder import android.os.PowerManager import android.util.Log import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.openlist.mobile.config.AppConfig import com.openlist.mobile.model.openlist.OpenList import com.openlist.mobile.utils.AndroidUtils.registerReceiverCompat import com.openlist.mobile.utils.ClipboardUtils import com.openlist.mobile.utils.ToastUtils.toast import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import splitties.systemservices.powerManager /** * OpenList后台服务 - 提供OpenList核心功能并实现保活机制 */ class OpenListService : Service(), OpenList.Listener { companion object { const val TAG = "OpenListService" const val ACTION_SHUTDOWN = "com.openlist.openlistandroid.service.OpenListService.ACTION_SHUTDOWN" const val ACTION_COPY_ADDRESS = "com.openlist.openlistandroid.service.OpenListService.ACTION_COPY_ADDRESS" const val ACTION_STATUS_CHANGED = "com.openlist.openlistandroid.service.OpenListService.ACTION_STATUS_CHANGED" const val NOTIFICATION_CHAN_ID = "openlist_server" const val FOREGROUND_ID = 5224 @Volatile var isRunning: Boolean = false private set @Volatile var serviceInstance: OpenListService? = null private set } private val mScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val mNotificationReceiver = NotificationActionReceiver() private val mReceiver = MyReceiver() private var mWakeLock: PowerManager.WakeLock? = null private var mLocalAddress: String = "" private var mDbSyncJob: Job? = null // Database sync interval in milliseconds (5 minutes) private val DB_SYNC_INTERVAL = 5 * 60 * 1000L override fun onBind(p0: Intent?): IBinder? = null @Suppress("DEPRECATION") private fun notifyStatusChanged() { Log.d(TAG, "notifyStatusChanged: isRunning=$isRunning") LocalBroadcastManager.getInstance(this) .sendBroadcast(Intent(ACTION_STATUS_CHANGED)) // Notify ServiceBridge of status change try { MainActivity.serviceBridge?.notifyServiceStatusChanged(isRunning) } catch (e: Exception) { Log.e(TAG, "Failed to notify ServiceBridge", e) } if (!isRunning) { // Stop foreground service and remove notification stopForeground(true) cancelNotification() stopSelf() } else { // Update notification with current status Log.d(TAG, "Updating notification after status change") updateNotification() } } @SuppressLint("WakelockTimeout") override fun onCreate() { super.onCreate() Log.d(TAG, "OpenListService created") serviceInstance = this // Android 8.0+ must start foreground notification immediately if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { initOrUpdateNotification() } // Register broadcast receivers try { LocalBroadcastManager.getInstance(this) .registerReceiver(mReceiver, IntentFilter(ACTION_STATUS_CHANGED)) registerReceiverCompat(mNotificationReceiver, ACTION_SHUTDOWN, ACTION_COPY_ADDRESS) } catch (e: Exception) { Log.e(TAG, "Failed to register receivers", e) } // Add OpenList listener OpenList.addListener(this) // Acquire wake lock if enabled if (AppConfig.isWakeLockEnabled) { try { mWakeLock = powerManager.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, "openlist::service" ) mWakeLock?.acquire() } catch (e: Exception) { Log.e(TAG, "Failed to acquire wake lock", e) } } Log.d(TAG, "Service onCreate completed") } @Suppress("DEPRECATION") override fun onDestroy() { super.onDestroy() Log.d(TAG, "OpenListService destroyed") serviceInstance = null // 取消所有协程作业 mScope.coroutineContext[Job]?.cancel() // 释放唤醒锁 try { mWakeLock?.release() mWakeLock = null Log.d(TAG, "Wake lock released") } catch (e: Exception) { Log.e(TAG, "Failed to release wake lock", e) } // 停止前台服务并取消通知 stopForeground(true) cancelNotification() // 注销广播接收器 try { LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver) unregisterReceiver(mNotificationReceiver) Log.d(TAG, "Broadcast receivers unregistered") } catch (e: Exception) { Log.e(TAG, "Failed to unregister receivers", e) } // 移除OpenList监听器 OpenList.removeListener(this) // Stop database sync task stopDatabaseSyncTask() } override fun onShutdown(type: String) { Log.d(TAG, "OpenList shutdown: $type") if (!OpenList.isRunning()) { isRunning = false // Stop database sync task when service shuts down stopDatabaseSyncTask() notifyStatusChanged() } } /** * Start periodic database synchronization task */ private fun startDatabaseSyncTask() { stopDatabaseSyncTask() // Stop any existing task first mDbSyncJob = mScope.launch(Dispatchers.IO) { while (isActive && isRunning) { try { delay(DB_SYNC_INTERVAL) if (isRunning && OpenList.isRunning()) { Log.d(TAG, "Performing periodic database sync") OpenList.forceDatabaseSync() } } catch (e: Exception) { Log.e(TAG, "Error during periodic database sync", e) } } } Log.d(TAG, "Database sync task started") } /** * Stop database synchronization task */ private fun stopDatabaseSyncTask() { mDbSyncJob?.cancel() mDbSyncJob = null Log.d(TAG, "Database sync task stopped") } /** * Force immediate database synchronization */ fun forceImmediateDbSync() { mScope.launch(Dispatchers.IO) { try { if (isRunning && OpenList.isRunning()) { Log.d(TAG, "Performing immediate database sync") OpenList.forceDatabaseSync() } } catch (e: Exception) { Log.e(TAG, "Error during immediate database sync", e) } } } /** * Public method: Stop OpenList service manually */ fun stopOpenListService() { if (isRunning) { Log.d(TAG, "User manually stopping service") // Set flag to indicate user manually stopped the service AppConfig.isManuallyStoppedByUser = true startOrShutdown() } } /** * Start or shutdown OpenList service */ private fun startOrShutdown() { if (isRunning) { Log.d(TAG, "Shutting down OpenList") mScope.launch(Dispatchers.IO) { try { if (OpenList.isRunning()) { Log.d(TAG, "Forcing database sync before shutdown") OpenList.forceDatabaseSync() } OpenList.shutdown() isRunning = false launch(Dispatchers.Main) { notifyStatusChanged() } } catch (e: Exception) { Log.e(TAG, "Shutdown error", e) launch(Dispatchers.Main) { toast("关闭失败: ${e.message}") } } } } else { Log.d(TAG, "Starting OpenList from user action") AppConfig.isManuallyStoppedByUser = false toast(getString(R.string.starting)) startOpenListBackend() } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d(TAG, "onStartCommand called") // Check manual stop flag if (AppConfig.isManuallyStoppedByUser) { Log.d(TAG, "Service was manually stopped by user, not starting") stopSelf() return START_NOT_STICKY } // Start OpenList if not running if (!isRunning) { Log.d(TAG, "Starting OpenList backend") startOpenListBackend() } return START_STICKY } /** * Start OpenList backend service */ private fun startOpenListBackend() { if (isRunning) { Log.d(TAG, "OpenList already running") return } Log.d(TAG, "Initializing and starting OpenList") isRunning = true mScope.launch(Dispatchers.IO) { try { // Initialize OpenList OpenList.init() delay(100) // Start OpenList OpenList.startup() // Clear cached address to force refresh mLocalAddress = "" // Update UI on success launch(Dispatchers.Main) { notifyStatusChanged() startDatabaseSyncTask() } Log.d(TAG, "OpenList started successfully") } catch (e: Exception) { Log.e(TAG, "Failed to start OpenList", e) isRunning = false launch(Dispatchers.Main) { toast("启动失败: ${e.message}") notifyStatusChanged() } } } } inner class MyReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent) { when (intent.action) { ACTION_STATUS_CHANGED -> { Log.d(TAG, "Status changed broadcast received") } } } } /** * Get local address safely */ private fun localAddress(): String { return try { if (mLocalAddress.isEmpty()) { Log.d(TAG, "Fetching local address...") mLocalAddress = Openlistlib.getOutboundIPString() Log.d(TAG, "Local address: $mLocalAddress") } mLocalAddress } catch (e: Exception) { Log.e(TAG, "Failed to get local address", e) "Initializing..." } } /** * Initialize or update notification */ @Suppress("DEPRECATION") private fun initOrUpdateNotification() { try { Log.d(TAG, "Creating/updating notification with address: ${localAddress()}") val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent.FLAG_IMMUTABLE } else { 0 } val pendingIntent = PendingIntent.getActivity( this, 0, Intent(this, MainActivity::class.java), pendingIntentFlags ) val shutdownAction = PendingIntent.getBroadcast( this, 0, Intent(ACTION_SHUTDOWN), pendingIntentFlags ) val copyAddressPendingIntent = PendingIntent.getBroadcast( this, 0, Intent(ACTION_COPY_ADDRESS), pendingIntentFlags ) val builder = Notification.Builder(applicationContext) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val chan = NotificationChannel( NOTIFICATION_CHAN_ID, getString(R.string.openlist_server), NotificationManager.IMPORTANCE_LOW ) chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE chan.setShowBadge(false) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { chan.setBlockable(false) } val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(chan) builder.setChannelId(NOTIFICATION_CHAN_ID) } val smallIconRes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { R.drawable.server } else { R.mipmap.ic_launcher_round } val notification = builder .setContentTitle(getString(R.string.openlist_server_running)) .setContentText("地址: ${localAddress()}") .setSmallIcon(smallIconRes) .setContentIntent(pendingIntent) .addAction(0, getString(R.string.shutdown), shutdownAction) .addAction(0, getString(R.string.copy_address), copyAddressPendingIntent) .setOngoing(true) .setAutoCancel(false) .build() notification.flags = notification.flags or Notification.FLAG_NO_CLEAR or Notification.FLAG_ONGOING_EVENT startForeground(FOREGROUND_ID, notification) } catch (e: Exception) { Log.e(TAG, "Failed to create notification", e) // Minimal fallback try { val minimal = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Notification.Builder(applicationContext, NOTIFICATION_CHAN_ID) } else { Notification.Builder(applicationContext) }.setContentTitle("OpenList") .setContentText("Starting...") .setSmallIcon(R.mipmap.ic_launcher_round) .build() startForeground(FOREGROUND_ID, minimal) } catch (fallbackError: Exception) { Log.e(TAG, "Failed to create minimal notification", fallbackError) } } } /** * 更新通知 */ private fun updateNotification() { initOrUpdateNotification() } /** * 取消通知 */ private fun cancelNotification() { try { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.cancel(FOREGROUND_ID) Log.d(TAG, "Notification cancelled") } catch (e: Exception) { Log.e(TAG, "Failed to cancel notification", e) } } inner class NotificationActionReceiver : BroadcastReceiver() { override fun onReceive(ctx: Context?, intent: Intent?) { when (intent?.action) { ACTION_SHUTDOWN -> { Log.d(TAG, "Shutdown action received from notification") startOrShutdown() } ACTION_COPY_ADDRESS -> { Log.d(TAG, "Copy address action received from notification") ClipboardUtils.copyText("OpenList", localAddress()) toast(R.string.address_copied) } } } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/OpenListTileService.kt ================================================ package com.openlist.mobile import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.graphics.drawable.Icon import android.os.Build import android.service.quicksettings.Tile import android.service.quicksettings.TileService import android.util.Log import androidx.annotation.RequiresApi import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.openlist.mobile.config.AppConfig @RequiresApi(Build.VERSION_CODES.N) class OpenListTileService : TileService() { companion object { private const val TAG = "OpenListTileService" private const val CLICK_DEBOUNCE_TIME = 2000L // 2秒防重复点击 } private var lastClickTime = 0L private val statusReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { OpenListService.ACTION_STATUS_CHANGED -> { Log.d(TAG, "Service status changed, updating tile") // 添加小延迟确保状态稳定 qsTile?.let { android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ updateTileState() }, 100) } } } } } override fun onStartListening() { super.onStartListening() Log.d(TAG, "Tile started listening") LocalBroadcastManager.getInstance(this) .registerReceiver(statusReceiver, IntentFilter(OpenListService.ACTION_STATUS_CHANGED)) updateTileState() } override fun onStopListening() { super.onStopListening() Log.d(TAG, "Tile stopped listening") try { LocalBroadcastManager.getInstance(this).unregisterReceiver(statusReceiver) } catch (e: Exception) { Log.w(TAG, "Failed to unregister receiver", e) } } override fun onClick() { super.onClick() // 防重复点击 val currentTime = System.currentTimeMillis() if (currentTime - lastClickTime < CLICK_DEBOUNCE_TIME) { Log.d(TAG, "Click ignored due to debounce") return } lastClickTime = currentTime val isRunning = OpenListService.isRunning Log.d(TAG, "Tile clicked, service running: $isRunning") // 设置瓦片为过渡状态,显示操作进行中 setTileTransitionState(!isRunning) if (isRunning) { stopOpenListService() } else { startOpenListService() } // 移除立即状态更新,依赖广播接收器异步更新 // updateTileState() - 现在由广播接收器处理 } private fun startOpenListService() { try { AppConfig.isManuallyStoppedByUser = false val intent = Intent(this, OpenListService::class.java) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(intent) } else { startService(intent) } Log.d(TAG, "Service start command sent from tile") } catch (e: Exception) { Log.e(TAG, "Failed to start service from tile", e) } } private fun stopOpenListService() { try { AppConfig.isManuallyStoppedByUser = true val serviceInstance = OpenListService.serviceInstance if (serviceInstance != null && OpenListService.isRunning) { serviceInstance.stopOpenListService() } // 移除else分支的冗余stopService调用 Log.d(TAG, "Service stop command sent from tile") } catch (e: Exception) { Log.e(TAG, "Failed to stop service from tile", e) // 出错时恢复瓦片状态 updateTileState() } } /** * 设置瓦片过渡状态,显示操作正在进行中 */ private fun setTileTransitionState(targetActiveState: Boolean) { val tile = qsTile ?: return // 设置过渡状态 tile.state = if (targetActiveState) Tile.STATE_UNAVAILABLE else Tile.STATE_UNAVAILABLE tile.label = if (targetActiveState) "启动中..." else "停止中..." tile.contentDescription = if (targetActiveState) "OpenList Starting" else "OpenList Stopping" try { val icon = Icon.createWithResource(this, R.mipmap.ic_launcher) tile.icon = icon } catch (e: Exception) { Log.w(TAG, "Failed to set tile icon during transition", e) } tile.updateTile() Log.d(TAG, "Tile set to transition state: ${if (targetActiveState) "starting" else "stopping"}") } private fun updateTileState() { val tile = qsTile ?: return val isRunning = OpenListService.isRunning Log.d(TAG, "Updating tile state, service running: $isRunning") if (isRunning) { tile.state = Tile.STATE_ACTIVE tile.label = "OpenList" tile.contentDescription = "OpenList Running" } else { tile.state = Tile.STATE_INACTIVE tile.label = "OpenList" tile.contentDescription = "OpenList Stopped" } try { val icon = Icon.createWithResource(this, R.mipmap.ic_launcher) tile.icon = icon } catch (e: Exception) { Log.w(TAG, "Failed to set tile icon", e) } tile.updateTile() } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/SwitchServerActivity.kt ================================================ package com.openlist.mobile import android.app.Activity import android.content.Intent import android.os.Bundle import android.util.Log import com.openlist.mobile.config.AppConfig import com.openlist.mobile.utils.ToastUtils.toast class SwitchServerActivity : Activity() { companion object { private const val TAG = "SwitchServerActivity" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (OpenListService.isRunning) { Log.d(TAG, "Service is running, stopping it") // 设置手动停止标志 AppConfig.isManuallyStoppedByUser = true startService(Intent(this, OpenListService::class.java).apply { action = OpenListService.ACTION_SHUTDOWN }) } else { // 检查是否被手动停止 if (AppConfig.isManuallyStoppedByUser) { Log.d(TAG, "Service was manually stopped, clearing flag and starting") // 清除手动停止标志 AppConfig.isManuallyStoppedByUser = false } Log.d(TAG, "Starting service") startService(Intent(this, OpenListService::class.java)) } finish() } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/bridge/AndroidBridge.kt ================================================ package com.openlist.mobile.bridge import android.content.Context import android.content.Intent import android.os.Build import android.util.Log import com.openlist.mobile.OpenListService import com.openlist.mobile.BuildConfig import com.openlist.mobile.R import com.openlist.mobile.SwitchServerActivity import com.openlist.mobile.config.AppConfig import com.openlist.mobile.model.openlist.OpenList import com.openlist.mobile.utils.MyTools import com.openlist.mobile.utils.ToastUtils.longToast import com.openlist.mobile.utils.ToastUtils.toast import com.openlist.pigeon.GeneratedApi class AndroidBridge(private val context: Context) : GeneratedApi.Android { companion object { private const val TAG = "AndroidBridge" } override fun addShortcut() { MyTools.addShortcut( context, context.getString(R.string.app_switch), "openlist_mobile_switch", R.drawable.openlist_switch, Intent(context, SwitchServerActivity::class.java) ) } override fun startService() { // 清除手动停止标志,表示用户手动启动了服务 AppConfig.isManuallyStoppedByUser = false Log.d(TAG, "Starting service via AndroidBridge, manual stop flag cleared") context.startService(Intent(context, OpenListService::class.java)) } override fun setAdminPwd(pwd: String) { OpenList.setAdminPassword(pwd) } override fun getOpenListHttpPort(): Long { return OpenList.getHttpPort().toLong() } override fun isRunning() = OpenListService.isRunning override fun getOpenListVersion() = BuildConfig.OPENLIST_VERSION } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/bridge/AppConfigBridge.kt ================================================ package com.openlist.mobile.bridge import com.openlist.mobile.config.AppConfig import com.openlist.pigeon.GeneratedApi object AppConfigBridge : GeneratedApi.AppConfig { override fun isWakeLockEnabled() = AppConfig.isWakeLockEnabled override fun isStartAtBootEnabled() = AppConfig.isStartAtBootEnabled override fun isAutoCheckUpdateEnabled() = AppConfig.isAutoCheckUpdateEnabled override fun isAutoOpenWebPageEnabled() = AppConfig.isAutoOpenWebPageEnabled override fun getDataDir() = AppConfig.dataDir override fun setDataDir(dir: String) { AppConfig.dataDir = dir } override fun isSilentJumpAppEnabled(): Boolean = AppConfig.isSilentJumpAppEnabled override fun setSilentJumpAppEnabled(enabled: Boolean) { AppConfig.isSilentJumpAppEnabled = enabled } override fun setAutoOpenWebPageEnabled(enabled: Boolean) { AppConfig.isAutoOpenWebPageEnabled = enabled } override fun setAutoCheckUpdateEnabled(enabled: Boolean) { AppConfig.isAutoCheckUpdateEnabled = enabled } override fun setStartAtBootEnabled(enabled: Boolean) { AppConfig.isStartAtBootEnabled = enabled } override fun setWakeLockEnabled(enabled: Boolean) { AppConfig.isWakeLockEnabled = enabled } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/bridge/CommonBridge.kt ================================================ package com.openlist.mobile.bridge import android.content.Context import android.content.Intent import android.os.Build import com.openlist.mobile.BuildConfig import com.openlist.mobile.utils.ToastUtils.longToast import com.openlist.mobile.utils.ToastUtils.toast import com.openlist.pigeon.GeneratedApi class CommonBridge(private val context: Context) : GeneratedApi.NativeCommon { override fun startActivityFromUri(intentUri: String): Boolean { val intent = Intent.parseUri(intentUri, Intent.URI_INTENT_SCHEME) return if (intent.resolveActivity(context.packageManager) != null){ context.startActivity(intent) true }else{ false } } override fun getDeviceSdkInt(): Long { return Build.VERSION.SDK_INT.toLong() } override fun getDeviceCPUABI(): String { return Build.SUPPORTED_ABIS[0] } override fun getVersionName() = BuildConfig.VERSION_NAME override fun getVersionCode() = BuildConfig.VERSION_CODE.toLong() override fun toast(msg: String) { context.toast(msg) } override fun longToast(msg: String) { context.longToast(msg) } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/bridge/ServiceBridge.kt ================================================ package com.openlist.mobile.bridge import android.content.Context import android.content.Intent import android.os.Build import android.util.Log import com.openlist.mobile.OpenListService import com.openlist.mobile.config.AppConfig import com.openlist.mobile.utils.BatteryOptimizationUtils import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import openlistlib.Openlistlib /** * 服务桥接类 - 连接Flutter和Android服务 */ class ServiceBridge(private val context: Context, private val channel: MethodChannel) : MethodCallHandler { companion object { private const val TAG = "ServiceBridge" private const val CHANNEL_NAME = "com.openlist.mobile/service" } init { channel.setMethodCallHandler(this) Log.d(TAG, "ServiceBridge initialized") } override fun onMethodCall(call: MethodCall, result: Result) { try { when (call.method) { "startService" -> { val success = startOpenListService() result.success(success) } "stopService" -> { val success = stopOpenListService() result.success(success) } "isServiceRunning" -> { val isRunning = isOpenListServiceRunning() result.success(isRunning) } "isBatteryOptimizationIgnored" -> { val isIgnored = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { BatteryOptimizationUtils.isIgnoringBatteryOptimizations(context) } else { true } result.success(isIgnored) } "requestIgnoreBatteryOptimization" -> { val success = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { BatteryOptimizationUtils.requestIgnoreBatteryOptimizations(context) } else { true } result.success(success) } "openBatteryOptimizationSettings" -> { val success = BatteryOptimizationUtils.openBatteryOptimizationSettings(context) result.success(success) } "openAutoStartSettings" -> { val success = BatteryOptimizationUtils.openAutoStartSettings(context) result.success(success) } "getServiceAddress" -> { val address = getServiceAddress() result.success(address) } else -> { Log.w(TAG, "Unknown method: ${call.method}") result.notImplemented() } } } catch (e: Exception) { Log.e(TAG, "Error handling method call: ${call.method}", e) result.error("ERROR", e.message, e.toString()) } } /** * 启动OpenList服务 */ private fun startOpenListService(): Boolean { return try { // 清除手动停止标志,表示用户手动启动了服务 AppConfig.isManuallyStoppedByUser = false val intent = Intent(context, OpenListService::class.java) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(intent) } else { context.startService(intent) } Log.d(TAG, "OpenList service start command sent, manual stop flag cleared") true } catch (e: Exception) { Log.e(TAG, "Failed to start OpenList service", e) false } } /** * 停止OpenList服务 */ private fun stopOpenListService(): Boolean { return try { // 设置手动停止标志,阻止保活机制重启服务 AppConfig.isManuallyStoppedByUser = true // 首先尝试通过服务实例直接停止OpenList val serviceInstance = OpenListService.serviceInstance if (serviceInstance != null && OpenListService.isRunning) { Log.d(TAG, "Calling service stopOpenListService method directly") // 直接调用服务的停止方法 serviceInstance.stopOpenListService() } else { Log.w(TAG, "Service instance not available or not running, using stopService") // 如果服务实例不可用,直接停止服务 val intent = Intent(context, OpenListService::class.java) context.stopService(intent) } Log.d(TAG, "OpenList service stop command sent, manual stop flag set") true } catch (e: Exception) { Log.e(TAG, "Failed to stop OpenList service", e) false } } /** * 检查OpenList服务是否运行 */ private fun isOpenListServiceRunning(): Boolean { return try { OpenListService.isRunning } catch (e: Exception) { Log.e(TAG, "Failed to check service status", e) false } } /** * 获取服务地址 */ private fun getServiceAddress(): String { return try { if (OpenListService.isRunning) { Openlistlib.getOutboundIPString() } else { "" } } catch (e: Exception) { Log.e(TAG, "Failed to get service address", e) "" } } /** * 通知Flutter服务状态变化 */ fun notifyServiceStatusChanged(isRunning: Boolean) { try { val arguments = mapOf("isRunning" to isRunning) channel.invokeMethod("onServiceStatusChanged", arguments) Log.d(TAG, "Service status change notified: $isRunning") } catch (e: Exception) { Log.e(TAG, "Failed to notify service status change", e) } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/config/AppConfig.kt ================================================ package com.openlist.mobile.config import com.cioccarellia.ksprefs.KsPrefs import com.cioccarellia.ksprefs.dynamic import com.openlist.mobile.app object AppConfig { val prefs by lazy { KsPrefs(app, "app") } var isSilentJumpAppEnabled by prefs.dynamic("isSilentJumpAppEnabled", fallback = false) var isWakeLockEnabled: Boolean by prefs.dynamic("isWakeLockEnabled", fallback = false) var isStartAtBootEnabled: Boolean by prefs.dynamic("isStartAtBootEnabled", fallback = false) var isAutoCheckUpdateEnabled: Boolean by prefs.dynamic( "isAutoCheckUpdateEnabled", fallback = false ) var isAutoOpenWebPageEnabled: Boolean by prefs.dynamic( "isAutoOpenWebPageEnabled", fallback = false ) // 用户手动停止服务的标志,当为true时,保活机制不会重启服务 var isManuallyStoppedByUser: Boolean by prefs.dynamic("isManuallyStoppedByUser", fallback = false) val defaultDataDir by lazy { app.getExternalFilesDir("data")?.absolutePath!! } private var mDataDir: String by prefs.dynamic("dataDir", fallback = defaultDataDir) var dataDir: String get() { if (mDataDir.isBlank()) mDataDir = defaultDataDir return mDataDir } set(value) { if (value.isBlank()) { mDataDir = defaultDataDir return } mDataDir = value } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/constant/AppConst.kt ================================================ package com.openlist.mobile.constant import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.openlist.mobile.app import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json object AppConst { @OptIn(ExperimentalSerializationApi::class) val json = Json { ignoreUnknownKeys = true allowStructuredMapKeys = true prettyPrint = true isLenient = true explicitNulls = false } val localBroadcast by lazy { LocalBroadcastManager.getInstance(app) } // val fileProviderAuthor = BuildConfig.APPLICATION_ID + ".fileprovider" } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/constant/LogLevel.kt ================================================ package com.openlist.mobile.constant import androidx.annotation.IntDef @IntDef( LogLevel.PANIC, LogLevel.FATAL, LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO, LogLevel.DEBUG, LogLevel.TRACE ) annotation class LogLevel { companion object { const val PANIC = 0 const val FATAL = 1 const val ERROR = 2 const val WARN = 3 const val INFO = 4 const val DEBUG = 5 const val TRACE = 6 fun Int.toLevelString(): String { return when (this) { PANIC -> "PANIC" FATAL -> "FATAL" ERROR -> "ERROR" WARN -> "WARN" INFO -> "INFO" DEBUG -> "DEBUG" TRACE -> "TRACE" else -> "UNKNOWN" } } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/data/AppDatabase.kt ================================================ /* package com.openlist.mobile.data import androidx.room.AutoMigration import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import com.openlist.openlistandroid.data.dao.ServerLogDao import com.openlist.mobile.data.entities.ServerLog import com.openlist.mobile.App.Companion.app val appDb by lazy { AppDatabase.create() } @Database( version = 2, entities = [ServerLog::class], autoMigrations = [ AutoMigration(from = 1, to = 2) ] ) abstract class AppDatabase : RoomDatabase() { abstract val serverLogDao: ServerLogDao companion object { fun create() = Room.databaseBuilder( app, AppDatabase::class.java, "openlistandroid.db" ) .allowMainThreadQueries() .build() } }*/ ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/data/entities/ServerLog.kt ================================================ package com.openlist.mobile.data.entities import com.openlist.mobile.constant.LogLevel data class ServerLog( @LogLevel val level: Int, val message: String, val time: String, ) { companion object { @Suppress("RegExpRedundantEscape") fun String.evalLog(): ServerLog? { val logPattern = """(\w+)\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (.+)""".toRegex() val result = logPattern.find(this) if (result != null) { val (level, time, msg) = result.destructured val l = when (level[0].toString()) { "D" -> LogLevel.DEBUG "I" -> LogLevel.INFO "W" -> LogLevel.WARN "E" -> LogLevel.ERROR else -> LogLevel.INFO } return ServerLog(level = l, message = msg, time = time) } return null } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/model/ShortCuts.kt ================================================ package com.openlist.mobile.model import android.content.Context import android.content.Intent import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat import com.openlist.mobile.R import com.openlist.mobile.SwitchServerActivity object ShortCuts { private inline fun buildIntent(context: Context): Intent { val intent = Intent(context, T::class.java) intent.action = Intent.ACTION_VIEW return intent } private fun buildOpenListSwitchShortCutInfo(context: Context): ShortcutInfoCompat { val msSwitchIntent = buildIntent(context) return ShortcutInfoCompat.Builder(context, "openlist_switch") .setShortLabel(context.getString(R.string.app_switch)) .setLongLabel(context.getString(R.string.app_switch)) .setIcon(IconCompat.createWithResource(context, R.drawable.openlist_switch)) .setIntent(msSwitchIntent) .build() } fun buildShortCuts(context: Context) { ShortcutManagerCompat.setDynamicShortcuts( context, listOf( buildOpenListSwitchShortCutInfo(context), ) ) } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/model/UpdateResult.kt ================================================ package com.openlist.openlistandroid.model data class UpdateResult( val version: String = "", val time: String = "", val content: String = "", val downloadUrl: String = "", val size: Long = 0, ) { fun hasUpdate() = version.isNotBlank() && downloadUrl.isNotBlank() } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/model/openlist/Logger.kt ================================================ package com.openlist.mobile.model.openlist object Logger { private var listeners = mutableListOf() fun addListener(listener: Listener) { listeners.add(listener) } fun removeListener(listener: Listener) { listeners.remove(listener) } interface Listener { fun onLog(level: Int, time: String, msg: String) } fun log(level: Int, time: String, msg: String) { listeners.forEach { it.onLog(level, time, msg) } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/model/openlist/OpenList.kt ================================================ package com.openlist.mobile.model.openlist import openlistlib.Openlistlib import openlistlib.Event import openlistlib.LogCallback import android.annotation.SuppressLint import android.util.Log import com.openlist.mobile.R import com.openlist.mobile.app import com.openlist.mobile.config.AppConfig import com.openlist.mobile.constant.LogLevel import com.openlist.mobile.utils.ToastUtils.longToast import java.io.File import java.text.SimpleDateFormat import java.util.Locale object OpenList : Event, LogCallback { const val TAG = "OpenList" val context = app val dataDir: String get() = AppConfig.dataDir val configPath: String get() = "$dataDir${File.separator}config.json" fun init() { runCatching { Openlistlib.setConfigData(dataDir) Openlistlib.setConfigLogStd(true) Openlistlib.init(this, this) }.onFailure { Log.e(TAG, "init:", it) } } interface Listener { fun onShutdown(type: String) } private val mListeners = mutableListOf() fun addListener(listener: Listener) { mListeners.add(listener) } fun removeListener(listener: Listener) { mListeners.remove(listener) } override fun onShutdown(p0: String) { Log.d(TAG, "onShutdown: $p0") mListeners.forEach { it.onShutdown(p0) } } override fun onStartError(type: String, msg: String) { Log.e(TAG, "onStartError: $type, $msg") Logger.log(LogLevel.FATAL, type, msg) } private val mDateFormatter by lazy { SimpleDateFormat("MM-dd HH:mm:ss", Locale.getDefault())} override fun onLog(level: Short, time: Long, log: String) { Log.d(TAG, "onLog: $level, $time, $log") Logger.log(level.toInt(), mDateFormatter.format(time), log) } override fun onProcessExit(code: Long) { } fun isRunning(): Boolean { return Openlistlib.isRunning("") } fun setAdminPassword(pwd: String) { if (!isRunning()) init() Log.d(TAG, "setAdminPassword: $dataDir") Openlistlib.setConfigData(dataDir) Openlistlib.setAdminPassword(pwd) } fun shutdown() { Log.d(TAG, "shutdown") runCatching { Openlistlib.shutdown(5000) }.onFailure { context.longToast(R.string.shutdown_failed) } } /** * Force database synchronization (WAL checkpoint) * This ensures SQLite WAL files are merged into the main database file */ fun forceDatabaseSync() { Log.d(TAG, "forceDatabaseSync") runCatching { Openlistlib.forceDBSync() Log.d(TAG, "Database sync completed successfully") }.onFailure { e -> Log.e(TAG, "Failed to sync database", e) } } @SuppressLint("SdCardPath") @Synchronized fun startup() { Log.d(TAG, "startup: $dataDir") try { // 确保数据目录存在 val dataDirFile = File(dataDir) if (!dataDirFile.exists()) { dataDirFile.mkdirs() Log.d(TAG, "Created data directory: $dataDir") } // 重新初始化以确保配置正确 init() // 多重检查是否已经在运行,防止重复启动 if (isRunning()) { Log.w(TAG, "OpenList is already running, skipping startup") return } // 再次检查以确保安全 Thread.sleep(100) // 短暂等待以避免竞态条件 if (isRunning()) { Log.w(TAG, "OpenList started by another thread, skipping startup") return } Log.d(TAG, "Starting OpenList...") Openlistlib.start() // 验证启动是否成功 Thread.sleep(1000) // 等待1秒让服务完全启动 if (isRunning()) { Log.d(TAG, "OpenList started successfully and confirmed running") } else { Log.w(TAG, "OpenList startup command sent but status check failed") } } catch (e: Exception) { Log.e(TAG, "Failed to start OpenList", e) throw e } catch (t: Throwable) { Log.e(TAG, "Fatal error starting OpenList", t) throw RuntimeException("Fatal error starting OpenList", t) } } fun getHttpPort(): Int { return OpenListConfigManager.config().scheme.httpPort } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/model/openlist/OpenListConfig.kt ================================================ package com.openlist.mobile.model.openlist import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class OpenListConfig( @SerialName("bleve_dir") val bleveDir: String = "", // /storage/emulated/0/Android/data/com.openlist.openlistandroid.debug/files/data/bleve @SerialName("cdn") val cdn: String = "", // @SerialName("database") // val database: Database = Database(), @SerialName("delayed_start") val delayedStart: Int = 0, // 0 @SerialName("force") val force: Boolean = false, // false @SerialName("jwt_secret") val jwtSecret: String = "", // // @SerialName("log") // val log: Log = Log(), @SerialName("max_connections") val maxConnections: Int = 0, // 0 @SerialName("scheme") val scheme: Scheme = Scheme(), @SerialName("site_url") val siteUrl: String = "", @SerialName("temp_dir") val tempDir: String = "", // /storage/emulated/0/Android/data/com.openlist.openlistandroid.debug/files/data/temp @SerialName("tls_insecure_skip_verify") val tlsInsecureSkipVerify: Boolean = true, // true @SerialName("token_expires_in") val tokenExpiresIn: Int = 48 // 48 ) { @Serializable data class Database( @SerialName("db_file") val dbFile: String = "", // /storage/emulated/0/Android/data/com.openlist.openlistandroid.debug/files/data/data.db @SerialName("host") val host: String = "", @SerialName("name") val name: String = "", @SerialName("password") val password: String = "", @SerialName("port") val port: Int = 0, // 0 @SerialName("ssl_mode") val sslMode: String = "", @SerialName("table_prefix") val tablePrefix: String = "x_", // x_ @SerialName("type") val type: String = "sqlite3", // sqlite3 @SerialName("user") val user: String = "" ) @Serializable data class Log( @SerialName("compress") val compress: Boolean = false, // false @SerialName("enable") val enable: Boolean = true, // true @SerialName("max_age") val maxAge: Int = 28, // 28 @SerialName("max_backups") val maxBackups: Int = 5, // 5 @SerialName("max_size") val maxSize: Int = 10, // 10 @SerialName("name") val name: String = "" // /storage/emulated/0/Android/data/com.openlist.openlistandroid.debug/files/data/log/log.log ) @Serializable data class Scheme( @SerialName("address") val address: String = "0.0.0.0", // 0.0.0.0 @SerialName("cert_file") val certFile: String = "", @SerialName("force_https") val forceHttps: Boolean = false, // false @SerialName("http_port") val httpPort: Int = 5244, // 5244 @SerialName("https_port") val httpsPort: Int = -1, // -1 @SerialName("key_file") val keyFile: String = "", @SerialName("unix_file") val unixFile: String = "", @SerialName("unix_file_perm") val unixFilePerm: String = "" ) } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/model/openlist/OpenListConfigManager.kt ================================================ package com.openlist.mobile.model.openlist import android.os.FileObserver import android.util.Log import com.openlist.mobile.app import com.openlist.mobile.constant.AppConst import com.openlist.mobile.utils.ToastUtils.longToast import kotlinx.coroutines.CancellationException import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.encodeToStream import java.io.File @Suppress("DEPRECATION") object OpenListConfigManager { const val TAG = "OpenListConfigManager" val context get() = app suspend fun flowConfig(): Flow = channelFlow { val obs = object : FileObserver(OpenList.configPath) { override fun onEvent(event: Int, p1: String?) { if (listOf(CLOSE_NOWRITE, CLOSE_WRITE).contains(event)) runBlocking { Log.d(TAG, "config.json changed: $event") send((config())) } } } coroutineScope { val waitJob = launch { obs.startWatching() try { awaitCancellation() } catch (_: CancellationException) { } obs.stopWatching() } waitJob.join() } } @OptIn(ExperimentalSerializationApi::class) fun config(): OpenListConfig { try { File(OpenList.configPath).inputStream().use { return AppConst.json.decodeFromStream(it) } } catch (e: Exception) { OpenList.context.longToast("读取 config.json 失败:\n$e") return OpenListConfig() } } @OptIn(ExperimentalSerializationApi::class) fun update(cfg: OpenListConfig) { try { File(OpenList.configPath).outputStream().use { AppConst.json.encodeToStream(cfg, it) } } catch (e: Exception) { OpenList.context.longToast("更新 config.json 失败:\n$e") } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/utils/AndroidUtils.kt ================================================ package com.openlist.mobile.utils import android.content.BroadcastReceiver import android.content.Context import android.content.Context.RECEIVER_EXPORTED import android.content.IntentFilter import android.os.Build object AndroidUtils { fun Context.registerReceiverCompat( receiver: BroadcastReceiver, vararg actions: String ) { val intentFilter = IntentFilter() actions.forEach { intentFilter.addAction(it) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { registerReceiver(receiver, intentFilter, RECEIVER_EXPORTED) } else { registerReceiver(receiver, intentFilter) } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/utils/BatteryOptimizationUtils.kt ================================================ package com.openlist.mobile.utils import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build import android.os.PowerManager import android.provider.Settings import android.util.Log import androidx.annotation.RequiresApi /** * 电池优化管理工具类 * 帮助应用获得电池优化白名单,提高后台服务存活率 */ object BatteryOptimizationUtils { private const val TAG = "BatteryOptimization" /** * 检查是否在电池优化白名单中 */ @RequiresApi(Build.VERSION_CODES.M) fun isIgnoringBatteryOptimizations(context: Context): Boolean { return try { val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager powerManager.isIgnoringBatteryOptimizations(context.packageName) } catch (e: Exception) { Log.e(TAG, "Failed to check battery optimization status", e) false } } /** * 请求忽略电池优化 */ @SuppressLint("BatteryLife") @RequiresApi(Build.VERSION_CODES.M) fun requestIgnoreBatteryOptimizations(context: Context): Boolean { return try { if (isIgnoringBatteryOptimizations(context)) { Log.d(TAG, "Already ignoring battery optimizations") return true } val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { data = Uri.parse("package:${context.packageName}") } if (intent.resolveActivity(context.packageManager) != null) { context.startActivity(intent) Log.d(TAG, "Battery optimization request sent") true } else { Log.w(TAG, "No activity found to handle battery optimization request") false } } catch (e: Exception) { Log.e(TAG, "Failed to request battery optimization exemption", e) false } } /** * 打开电池优化设置页面 */ fun openBatteryOptimizationSettings(context: Context): Boolean { return try { val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) } else { Intent(Settings.ACTION_APPLICATION_SETTINGS) } if (intent.resolveActivity(context.packageManager) != null) { context.startActivity(intent) Log.d(TAG, "Battery optimization settings opened") true } else { Log.w(TAG, "No activity found to handle battery optimization settings") false } } catch (e: Exception) { Log.e(TAG, "Failed to open battery optimization settings", e) false } } /** * 打开应用详情页面 */ fun openAppDetailsSettings(context: Context): Boolean { return try { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { data = Uri.parse("package:${context.packageName}") } if (intent.resolveActivity(context.packageManager) != null) { context.startActivity(intent) Log.d(TAG, "App details settings opened") true } else { Log.w(TAG, "No activity found to handle app details settings") false } } catch (e: Exception) { Log.e(TAG, "Failed to open app details settings", e) false } } /** * 尝试打开自启动管理页面(针对不同厂商) */ fun openAutoStartSettings(context: Context): Boolean { return try { val autoStartIntents = listOf( // 华为 Intent().setClassName( "com.huawei.systemmanager", "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity" ), Intent().setClassName( "com.huawei.systemmanager", "com.huawei.systemmanager.optimize.process.ProtectActivity" ), // 小米 Intent().setClassName( "com.miui.securitycenter", "com.miui.permcenter.autostart.AutoStartManagementActivity" ), Intent().setClassName( "com.xiaomi.mipicks", "com.xiaomi.mipicks.ui.AppPicksTabActivity" ), // OPPO Intent().setClassName( "com.coloros.safecenter", "com.coloros.safecenter.permission.startup.FakeActivity" ), Intent().setClassName( "com.oppo.safe", "com.oppo.safe.permission.startup.StartupAppListActivity" ), // Vivo Intent().setClassName( "com.iqoo.secure", "com.iqoo.secure.ui.phoneoptimize.AddWhiteListActivity" ), Intent().setClassName( "com.vivo.permissionmanager", "com.vivo.permissionmanager.activity.BgStartUpManagerActivity" ), // 魅族 Intent().setClassName( "com.meizu.safe", "com.meizu.safe.security.SHOW_APPSEC" ).apply { addCategory(Intent.CATEGORY_DEFAULT) putExtra("packageName", context.packageName) }, // 三星 Intent().setClassName( "com.samsung.android.lool", "com.samsung.android.sm.ui.battery.BatteryActivity" ), // 一加 Intent().setClassName( "com.oneplus.security", "com.oneplus.security.chainlaunch.view.ChainLaunchAppListActivity" ) ) for (intent in autoStartIntents) { try { if (intent.resolveActivity(context.packageManager) != null) { context.startActivity(intent) Log.d(TAG, "Auto start settings opened: ${intent.component}") return true } } catch (e: Exception) { Log.d(TAG, "Failed to open auto start settings: ${intent.component}", e) } } Log.w(TAG, "No auto start settings activity found") false } catch (e: Exception) { Log.e(TAG, "Failed to open auto start settings", e) false } } /** * 获取电池优化状态描述 */ fun getBatteryOptimizationStatus(context: Context): String { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (isIgnoringBatteryOptimizations(context)) { "已加入电池优化白名单" } else { "未加入电池优化白名单" } } else { "系统版本过低,无需设置" } } /** * 检查是否需要设置电池优化 */ fun needsBatteryOptimizationSetup(context: Context): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { !isIgnoringBatteryOptimizations(context) } else { false } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/utils/ClipBoardUtils.kt ================================================ package com.openlist.mobile.utils import android.content.ClipData import android.content.ClipboardManager import android.content.ClipboardManager.OnPrimaryClipChangedListener import android.content.Context import com.openlist.mobile.app /** *
 * author: Blankj
 * blog  : http://blankj.com
 * time  : 2016/09/25
 * desc  : utils about clipboard
* */ object ClipboardUtils { /** * Copy the text to clipboard. * * The label equals name of package. * * @param text The text. */ fun copyText(text: CharSequence?) { val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(ClipData.newPlainText(app.getPackageName(), text)) } /** * Copy the text to clipboard. * * @param label The label. * @param text The text. */ fun copyText(label: CharSequence?, text: CharSequence?) { val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(ClipData.newPlainText(label, text)) } /** * Clear the clipboard. */ fun clear() { val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(ClipData.newPlainText(null, "")) } /** * Return the label for clipboard. * * @return the label for clipboard */ fun getLabel(): CharSequence { val cm = app .getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val des = cm.primaryClipDescription ?: return "" return des.label ?: return "" } /** * Return the text for clipboard. * * @return the text for clipboard */ val text: CharSequence get() { val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clip = cm.primaryClip if (clip != null && clip.itemCount > 0) { val text = clip.getItemAt(0).coerceToText(app) if (text != null) { return text } } return "" } /** * Add the clipboard changed listener. */ fun addChangedListener(listener: OnPrimaryClipChangedListener?) { val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.addPrimaryClipChangedListener(listener) } /** * Remove the clipboard changed listener. */ fun removeChangedListener(listener: OnPrimaryClipChangedListener?) { val cm = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.removePrimaryClipChangedListener(listener) } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/utils/FileUtils.kt ================================================ package com.openlist.mobile.utils import java.io.File import java.io.InputStream import java.net.URLConnection object FileUtils { val File.mimeType: String? get() { val fileNameMap = URLConnection.getFileNameMap() return fileNameMap.getContentTypeFor(name) } /** * 按行读取txt */ fun InputStream.readAllText(): String { val bufferedReader = this.bufferedReader() val buffer = StringBuffer("") var str: String? while (bufferedReader.readLine().also { str = it } != null) { buffer.append(str) buffer.append("\n") } return buffer.toString() } fun copyFolder(src: File, target: File, overwrite: Boolean = true) { val folder = File(target.absolutePath + File.separator + src.name) folder.mkdirs() src.listFiles()?.forEach { if (it.isFile) { val newFile = File(folder.absolutePath + File.separator + it.name) it.copyTo(newFile, overwrite) } else if (it.isDirectory) { copyFolder(it, folder) } } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/utils/MyTools.kt ================================================ package com.openlist.mobile.utils import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.ShortcutInfo import android.content.pm.ShortcutManager import android.graphics.drawable.Icon import android.net.Uri import android.os.Build import android.provider.Settings import com.openlist.mobile.utils.ToastUtils.longToast import splitties.systemservices.powerManager object MyTools { fun Context.isIgnoringBatteryOptimizations(): Boolean { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && powerManager.isIgnoringBatteryOptimizations(packageName) } @SuppressLint("BatteryLife") fun Context.killBattery() { runCatching { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !isIgnoringBatteryOptimizations()) { startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { data = Uri.parse("package:$packageName") }) } } } /* 添加快捷方式 */ @SuppressLint("UnspecifiedImmutableFlag") @Suppress("DEPRECATION") fun addShortcut( ctx: Context, name: String, id: String, iconResId: Int, launcherIntent: Intent ) { ctx.longToast("如失败 请手动授予权限") if (Build.VERSION.SDK_INT < 26) { /* Android8.0 */ val addShortcutIntent = Intent("com.android.launcher.action.INSTALL_SHORTCUT") // 不允许重复创建 addShortcutIntent.putExtra("duplicate", false) // 经测试不是根据快捷方式的名字判断重复的 addShortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name) addShortcutIntent.putExtra( Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext( ctx, iconResId ) ) launcherIntent.action = Intent.ACTION_MAIN launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER) addShortcutIntent .putExtra(Intent.EXTRA_SHORTCUT_INTENT, launcherIntent) // 发送广播 ctx.sendBroadcast(addShortcutIntent) } else { val shortcutManager: ShortcutManager = ctx.getSystemService(ShortcutManager::class.java) if (shortcutManager.isRequestPinShortcutSupported) { launcherIntent.action = Intent.ACTION_VIEW val pinShortcutInfo = ShortcutInfo.Builder(ctx, id) .setIcon( Icon.createWithResource(ctx, iconResId) ) .setIntent(launcherIntent) .setShortLabel(name) .build() val pinnedShortcutCallbackIntent = shortcutManager .createShortcutResultIntent(pinShortcutInfo) //Get notified when a shortcut is pinned successfully// val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent.FLAG_IMMUTABLE } else { 0 } val successCallback = PendingIntent.getBroadcast( ctx, 0, pinnedShortcutCallbackIntent, pendingIntentFlags ) shortcutManager.requestPinShortcut( pinShortcutInfo, successCallback.intentSender ) } } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/utils/StringUtils.kt ================================================ package com.openlist.mobile.utils object StringUtils { private fun paramsParseInternal(params: String): HashMap { val parameters: HashMap = hashMapOf() if (params.isBlank()) return parameters for (param in params.split("&")) { val entry = param.split("=".toRegex()).dropLastWhile { it.isEmpty() } if (entry.size > 1) { parameters[entry[0]] = entry[1] } else { parameters[entry[0]] = "" } } return parameters } fun String.paramsParse() = paramsParseInternal(this) fun String.toNumberInt(): Int { return this.replace(Regex("[^0-9]"), "").toIntOrNull() ?: 0 } private val mAnsiRegex = Regex("""\x1b(\[.*?[@-~]|].*?(\x07|\x1b\\))""") fun String.removeAnsiCodes(): String { return mAnsiRegex.replace(this, "") } fun String.parseToMap(): Map { return this.split(";").associate { val ss = it.trim().split("=") if (ss.size != 2) return@associate "" to "" val key = ss[0] val value = ss[1] key.trim() to value.trim() } } } ================================================ FILE: android/app/src/main/kotlin/com/openlist/mobile/utils/ToastUtils.kt ================================================ package com.openlist.mobile.utils import android.content.Context import android.widget.Toast import androidx.annotation.StringRes import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch object ToastUtils { @OptIn(DelicateCoroutinesApi::class) fun runMain(block: () -> Unit) { GlobalScope.launch(Dispatchers.Main) { block() } } fun Context.toast(str: String) { runMain { Toast.makeText(this, str, Toast.LENGTH_SHORT).show() } } fun Context.toast(@StringRes strId: Int, vararg args: Any) { runMain { Toast.makeText( this, getString(strId, *args), Toast.LENGTH_SHORT ).show() } } fun Context.longToast(str: String) { runMain { Toast.makeText(this, str, Toast.LENGTH_LONG).show() } } fun Context.longToast(@StringRes strId: Int) { runMain { Toast.makeText(this, strId, Toast.LENGTH_LONG).show() } } fun Context.longToast(@StringRes strId: Int, vararg args: Any) { runMain { Toast.makeText( this, getString(strId, *args), Toast.LENGTH_LONG ).show() } } } ================================================ FILE: android/app/src/main/res/drawable/ic_download.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/ic_female.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/openlist_logo.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/openlist_switch.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/server.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/server2.xml ================================================ ================================================ FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: android/app/src/main/res/values/ic_launcher_background.xml ================================================ #FFFFFF ================================================ FILE: android/app/src/main/res/values/strings.xml ================================================ OpenList Mobile 开源许可 返回 未选择文件 ❌ 错误 确定 描述 日志 OpenList服务器 添加桌面快捷方式 关闭 复制地址 OpenList运行中 取消 admin 密码已设为:\n %1$s admin 密码 关闭失败:%1$s 已复制地址 ⚠️启动服务器才可设置admin密码 OpenList配置 设置 密码 启动中 关闭中 开关 更多选项 关于 监听地址 编辑 config.json 请至少启用一个服务器! OpenList提供者 account 路径已复制 检查更新 启动 所有文件访问权限 挂载本地存储时必须打开,否则无权限读写文件。 读取存储权限 写入存储权限 请求电池优化白名单 如果程序在后台运行时被系统杀死,可以尝试设置。 关闭 自动检查更新 打开程序主界面时从Github检查更新 唤醒锁 打开可防止锁屏后CPU休眠,但在部分系统可能会导致杀后台 重要设置 打开data文件夹 点按上方路径选择“MT管理器”打开data文件夹 网页 跳转失败: %1$s 清空网页数据 清空网页缓存 清空网页数据库、Cookie、DomStorage。 仅清空资源缓存,不影响用户数据。 已清除 确定 自动打开网页界面 打开主界面时,自动跳转到网页界面。 下载文件 系统下载器 打开链接 已复制链接 启动中 已关闭: %1$s 浏览器 选择下载器 上次使用 开机自启动服务 在开机时自动开启OpenList服务。 关闭失败 ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values/themes.xml ================================================ ================================================ FILE: android/app/src/main/res/xml/backup_rules.xml ================================================ ================================================ FILE: android/app/src/main/res/xml/data_extraction_rules.xml ================================================ ================================================ FILE: android/app/src/main/res/xml/file_path_data.xml ================================================ ================================================ FILE: android/app/src/main/res/xml/file_paths.xml ================================================ ================================================ FILE: android/app/src/main/res/xml/network_security_config.xml ================================================ ================================================ FILE: android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: android/build/reports/problems/problems-report.html ================================================ Gradle Configuration Cache
Loading...
================================================ FILE: android/build.gradle ================================================ buildscript { ext{ kotlin_version = '1.8.0' agp_version = '8.1.4' room_version = '2.6.1' ksp_version = '1.9.21-1.0.16' } repositories { google() mavenCentral() } dependencies { classpath "com.android.tools.build:gradle:$agp_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" } } allprojects { repositories { google() mavenCentral() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } tasks.register("clean", Delete) { delete rootProject.buildDir } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ #Tue Jul 15 12:22:13 CST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true ================================================ FILE: android/settings.gradle ================================================ pluginManagement { def flutterSdkPath = { def properties = new Properties() file("local.properties").withInputStream { properties.load(it) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" return flutterSdkPath } settings.ext.flutterSdkPath = flutterSdkPath() includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") repositories { google() mavenCentral() gradlePluginPortal() } plugins { id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false } } plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version '8.1.4' apply false } include ":app" include ":utils" ================================================ FILE: android/utils/.gitignore ================================================ /build /.cxx ================================================ FILE: android/utils/build.gradle ================================================ plugins { id("com.android.library") id("kotlin-android") } android { namespace "com.openlist.utils" compileSdk 35 defaultConfig { minSdk 21 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" version "3.22.1" } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = "17" } } dependencies { testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") } ================================================ FILE: android/utils/consumer-rules.pro ================================================ ================================================ FILE: android/utils/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: android/utils/src/androidTest/java/com/github/jing332/utils/ExampleInstrumentedTest.kt ================================================ package com.openlist.utils import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith import org.junit.Assert.* /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("com.openlist.utils.test", appContext.packageName) } } ================================================ FILE: android/utils/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/utils/src/main/cpp/CMakeLists.txt ================================================ # For more information about using CMake with Android Studio, read the # documentation: https://d.android.com/studio/projects/add-native-code.html. # For more examples on how to use CMake, see https://github.com/android/ndk-samples. # Sets the minimum CMake version required for this project. cmake_minimum_required(VERSION 3.22.1) # Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, # Since this is the top level CMakeLists.txt, the project name is also accessible # with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level # build script scope). project("utils") # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds them for you. # Gradle automatically packages shared libraries with your APK. # # In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define # the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} # is preferred for the same purpose. # # In order to load a library into your app from Java/Kotlin, you must call # System.loadLibrary() and pass the name of the library defined here; # for GameActivity/NativeActivity derived applications, the same library name must be # used in the AndroidManifest.xml file. add_library(${CMAKE_PROJECT_NAME} SHARED # List C/C++ source files with relative paths to this CMakeLists.txt. utils.cpp) # Specifies libraries CMake should link to your target library. You # can link libraries from various origins, such as libraries defined in this # build script, prebuilt third-party libraries, or Android system libraries. target_link_libraries(${CMAKE_PROJECT_NAME} # List libraries link to the target library android log) ================================================ FILE: android/utils/src/main/cpp/utils.cpp ================================================ #include #include #include #include #include #include #include #include #include #include #define RUN_SUCCESS 0 #define RUN_FAIL 1 int get_local_ip_using_ifconf(char *str_ip) { int sock_fd, intrface; struct ifreq buf[INET_ADDRSTRLEN]; struct ifconf ifc; char *local_ip = NULL; int status = RUN_FAIL; if ((sock_fd = socket(AF_INET, SOCK_DGRAM, 0)) >= 0) { ifc.ifc_len = sizeof(buf); ifc.ifc_buf = (caddr_t)buf; if (!ioctl(sock_fd, SIOCGIFCONF, (char *)&ifc)) { intrface = ifc.ifc_len/sizeof(struct ifreq); while (intrface-- > 0) { if (!(ioctl(sock_fd, SIOCGIFADDR, (char *)&buf[intrface]))) { local_ip = NULL; local_ip = inet_ntoa(((struct sockaddr_in*)(&buf[intrface].ifr_addr))->sin_addr); if(local_ip) { strcpy(str_ip, local_ip); status = RUN_SUCCESS; if(strcmp("127.0.0.1", str_ip)) { break; } } } } } close(sock_fd); } return status; } extern "C" JNIEXPORT jstring JNICALL Java_com_github_openlistteam_utils_NativeLib_getLocalIp( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; char str_ip[INET_ADDRSTRLEN]; int status = get_local_ip_using_ifconf(str_ip); return env->NewStringUTF(str_ip); } ================================================ FILE: android/utils/src/main/java/com/github/jing332/utils/NativeLib.kt ================================================ package com.openlist.utils object NativeLib { external fun getLocalIp(): String init { System.loadLibrary("utils") } } ================================================ FILE: android/utils/src/test/java/com/github/jing332/utils/ExampleUnitTest.kt ================================================ package com.openlist.utils import org.junit.Test import org.junit.Assert.* /** * Example local unit test, which will execute on the development machine (host). * * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { @Test fun addition_isCorrect() { assertEquals(4, 2 + 2) } } ================================================ FILE: ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 12.0 ================================================ FILE: ios/Flutter/Debug.xcconfig ================================================ #include "Generated.xcconfig" ================================================ FILE: ios/Flutter/Release.xcconfig ================================================ #include "Generated.xcconfig" ================================================ FILE: ios/Podfile ================================================ # Uncomment this line to define a global platform for your project platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end # Embed xcframeworks into Runner target installer.aggregate_targets.each do |aggregate_target| aggregate_target.user_project.targets.each do |target| next unless target.name == 'Runner' # Add xcframeworks to build settings target.build_configurations.each do |config| # Add framework search paths config.build_settings['FRAMEWORK_SEARCH_PATHS'] ||= ['$(inherited)'] config.build_settings['FRAMEWORK_SEARCH_PATHS'] << '$(PROJECT_DIR)/Frameworks' # Add system libraries needed by Go mobile config.build_settings['OTHER_LDFLAGS'] ||= ['$(inherited)'] config.build_settings['OTHER_LDFLAGS'] << '-lresolv' end # Add xcframeworks as embedded frameworks frameworks_dir = File.join(File.dirname(__FILE__), 'Frameworks') if Dir.exist?(frameworks_dir) frameworks_build_phase = target.frameworks_build_phase Dir.glob("#{frameworks_dir}/*.xcframework").each do |path| framework_ref = aggregate_target.user_project.new(Xcodeproj::Project::Object::PBXFileReference) framework_ref.name = File.basename(path) framework_ref.path = "Frameworks/#{File.basename(path)}" framework_ref.source_tree = '' framework_ref.last_known_file_type = 'wrapper.xcframework' # Add to Frameworks group frameworks_group = aggregate_target.user_project.frameworks_group frameworks_group.children << framework_ref unless frameworks_group.children.include?(framework_ref) # Add to build phase build_file = frameworks_build_phase.add_file_reference(framework_ref) build_file.settings = { 'ATTRIBUTES' => ['CodeSignOnCopy', 'RemoveHeadersOnCopy'] } end aggregate_target.user_project.save end end end end ================================================ FILE: ios/README_iOS_CONFIG.md ================================================ # iOS特定配置指南 ## 1. 应用图标配置 ### 自动生成图标(推荐) 使用提供的Python脚本自动从Logo文件夹生成所有所需尺寸的iOS图标: ```bash # 在项目根目录运行 python ios/scripts/generate_ios_icons.py ``` 该脚本会自动: - 从 `../Logo/logo/` 目录读取源图标 - 生成所有iOS所需尺寸的PNG图标 - 使用高质量的LANCZOS重采样算法 - 自动优化输出的PNG文件 ### 手动替换图标 如需手动替换,在 `ios/Runner/Assets.xcassets/AppIcon.appiconset/` 目录中替换以下图标文件: ### iPhone图标尺寸: - Icon-App-20x20@2x.png (40x40) - Icon-App-20x20@3x.png (60x60) - Icon-App-29x29@1x.png (29x29) - Icon-App-29x29@2x.png (58x58) - Icon-App-29x29@3x.png (87x87) - Icon-App-40x40@2x.png (80x80) - Icon-App-40x40@3x.png (120x120) - Icon-App-60x60@2x.png (120x120) - Icon-App-60x60@3x.png (180x180) ### iPad图标尺寸: - Icon-App-20x20@1x.png (20x20) - Icon-App-40x40@1x.png (40x40) - Icon-App-76x76@1x.png (76x76) - Icon-App-76x76@2x.png (152x152) - Icon-App-83.5x83.5@2x.png (167x167) ### App Store图标: - Icon-App-1024x1024@1x.png (1024x1024) ## 2. 启动画面配置 在 `ios/Runner/Base.lproj/LaunchScreen.storyboard` 中自定义启动画面 ## 3. 代码签名配置(发布时需要) 在Xcode中配置: 1. 打开 `ios/Runner.xcworkspace` 2. 选择Runner项目 → Signing & Capabilities 3. 配置Team和Bundle Identifier 4. 选择适当的Provisioning Profile ## 4. App Store Connect配置 1. 创建App Store Connect记录 2. 配置应用元数据 3. 上传应用截图 4. 设置应用描述和关键词 ## 5. 推送通知配置(如需要) 1. 在Apple Developer Portal启用Push Notifications 2. 在Info.plist中添加推送权限 3. 在AppDelegate.swift中配置推送处理 ## 6. 深度链接配置(如需要) 在Info.plist中添加URL Schemes: ```xml CFBundleURLTypes CFBundleURLName com.openlist.mobile CFBundleURLSchemes openlist ``` ## 7. 网络安全配置 ### ⚠️ 重要:本地HTTP访问配置 OpenList需要访问本地HTTP服务(如 `http://127.0.0.1`),这在iOS中需要特殊配置。 已配置的网络安全设置: ```xml NSAppTransportSecurity NSExceptionDomains localhost NSExceptionAllowsInsecureHTTPLoads NSExceptionMinimumTLSVersion TLSv1.0 127.0.0.1 NSExceptionAllowsInsecureHTTPLoads NSExceptionMinimumTLSVersion TLSv1.0 ``` ### 为什么需要这个配置? 1. **iOS App Transport Security (ATS)** 默认阻止所有HTTP连接 2. **本地服务访问** - OpenList需要连接到 `http://127.0.0.1` 的本地服务 3. **开发和调试** - 允许连接到本地开发服务器 4. **安全性平衡** - 仅为本地环回地址提供例外,保持 ATS 默认保护 ### 支持的本地地址: - `localhost` - 标准本地主机名 - `127.0.0.1` - IPv4回环地址 ### 生产环境建议: 如果生产版本不需要访问本地HTTP服务,可以进一步移除 `NSExceptionDomains` 的本地例外配置。 ## 8. 后台模式配置(如需要) 在Info.plist中添加后台模式: ```xml UIBackgroundModes background-fetch background-processing ``` ## 9. 隐私权限说明 已配置的权限说明: - 相册访问权限 - 相机访问权限 - 麦克风访问权限 - 文档文件夹访问权限 - 下载文件夹访问权限 ## 10. 构建配置 - 最低iOS版本:12.0 - 支持设备:arm64架构 - 状态栏样式:默认样式 ## 构建命令 ```bash # 调试构建 flutter build ios --debug # 发布构建(无代码签名) flutter build ios --release --no-codesign # 发布构建(带代码签名) flutter build ios --release ``` ## 发布到App Store 1. 在Xcode中Archive项目 2. 通过Xcode Organizer上传到App Store Connect 3. 在App Store Connect中提交审核 ================================================ FILE: ios/Runner/AppDelegate.swift ================================================ import Flutter import UIKit @main @objc class AppDelegate: FlutterAppDelegate { var eventAPI: Event? private var backgroundTask: UIBackgroundTaskIdentifier = .invalid private var appStoreUpdateBridge: AppStoreUpdateBridge? override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) // Setup Pigeon APIs guard let controller = window?.rootViewController as? FlutterViewController else { print("[AppDelegate] Failed to get FlutterViewController") return super.application(application, didFinishLaunchingWithOptions: launchOptions) } let messenger = controller.binaryMessenger // Register Pigeon API implementations AppConfigSetup.setUp(binaryMessenger: messenger, api: AppConfigBridge()) AndroidSetup.setUp(binaryMessenger: messenger, api: OpenListBridge()) NativeCommonSetup.setUp(binaryMessenger: messenger, api: CommonBridge(viewController: controller)) let appStoreChannel = FlutterMethodChannel( name: "openlist/app_store_update", binaryMessenger: messenger ) let appStoreBridge = AppStoreUpdateBridge(viewController: controller) appStoreUpdateBridge = appStoreBridge appStoreChannel.setMethodCallHandler { call, result in switch call.method { case "checkAndShowUpdate": appStoreBridge.checkAndShowUpdate(result: result) default: result(FlutterMethodNotImplemented) } } // Setup Event API for Flutter callbacks eventAPI = Event(binaryMessenger: messenger) // Initialize OpenList core (if XCFramework is available) #if canImport(Openlistlib) let eventHandler = OpenListEventHandler() let logCallback = OpenListLogCallback() eventHandler.eventAPI = eventAPI logCallback.eventAPI = eventAPI do { try OpenListManager.shared.initialize(event: eventHandler, logger: logCallback) print("[AppDelegate] OpenList core initialized") } catch { print("[AppDelegate] OpenList core initialization failed: \(error)") // Continue without core - will work in Flutter-only mode } #else print("[AppDelegate] OpenList core not available - running in Flutter-only mode") #endif print("[AppDelegate] Pigeon APIs registered successfully") return super.application(application, didFinishLaunchingWithOptions: launchOptions) } // MARK: - Application Lifecycle override func applicationWillTerminate(_ application: UIApplication) { // Cleanup OpenList core OpenListManager.shared.stopServer() // End background task if still active endBackgroundTask() super.applicationWillTerminate(application) } override func applicationDidEnterBackground(_ application: UIApplication) { // Begin background task to prevent WebView process suspension backgroundTask = application.beginBackgroundTask { [weak self] in // Background task is about to expire, clean up self?.endBackgroundTask() } } override func applicationWillEnterForeground(_ application: UIApplication) { // End background task when returning to foreground endBackgroundTask() } // MARK: - Background Task Management private func endBackgroundTask() { if backgroundTask != .invalid { UIApplication.shared.endBackgroundTask(backgroundTask) backgroundTask = .invalid } } } ================================================ FILE: ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "LaunchImage.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. ================================================ FILE: ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: ios/Runner/Bridges/AppConfigBridge.swift ================================================ import Flutter import Foundation /// Bridge implementation for App Configuration APIs class AppConfigBridge: NSObject, AppConfig { private let defaults = UserDefaults.standard // Keys for UserDefaults private enum Keys { static let wakeLock = "app_config_wake_lock" static let startAtBoot = "app_config_start_at_boot" static let autoCheckUpdate = "app_config_auto_check_update" static let autoOpenWebPage = "app_config_auto_open_web_page" static let dataDir = "app_config_data_dir" static let silentJumpApp = "app_config_silent_jump_app" } func isWakeLockEnabled() throws -> Bool { return defaults.bool(forKey: Keys.wakeLock) } func setWakeLockEnabled(enabled: Bool) throws { defaults.set(enabled, forKey: Keys.wakeLock) print("[AppConfigBridge] Wake lock enabled: \(enabled)") } func isStartAtBootEnabled() throws -> Bool { return defaults.bool(forKey: Keys.startAtBoot) } func setStartAtBootEnabled(enabled: Bool) throws { defaults.set(enabled, forKey: Keys.startAtBoot) print("[AppConfigBridge] Start at boot enabled: \(enabled)") } func isAutoCheckUpdateEnabled() throws -> Bool { return defaults.bool(forKey: Keys.autoCheckUpdate) } func setAutoCheckUpdateEnabled(enabled: Bool) throws { defaults.set(enabled, forKey: Keys.autoCheckUpdate) print("[AppConfigBridge] Auto check update enabled: \(enabled)") } func isAutoOpenWebPageEnabled() throws -> Bool { return defaults.bool(forKey: Keys.autoOpenWebPage) } func setAutoOpenWebPageEnabled(enabled: Bool) throws { defaults.set(enabled, forKey: Keys.autoOpenWebPage) print("[AppConfigBridge] Auto open web page enabled: \(enabled)") } func getDataDir() throws -> String { if let customDir = defaults.string(forKey: Keys.dataDir), !customDir.isEmpty { print("[AppConfigBridge] Using custom data directory: \(customDir)") return customDir } // Default to app's document directory with openlist_data subdirectory // This follows iOS app data storage guidelines let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) let documentsDirectory = paths[0] let openlistDataDir = documentsDirectory.appendingPathComponent("openlist_data") // Create directory if not exists if !FileManager.default.fileExists(atPath: openlistDataDir.path) { do { try FileManager.default.createDirectory(at: openlistDataDir, withIntermediateDirectories: true, attributes: nil) print("[AppConfigBridge] Created data directory: \(openlistDataDir.path)") } catch { print("[AppConfigBridge] Failed to create data directory: \(error)") throw error } } print("[AppConfigBridge] Data directory: \(openlistDataDir.path)") return openlistDataDir.path } func setDataDir(dir: String) throws { // On iOS, we should not allow users to change data directory arbitrarily // But we keep the method for compatibility if dir.isEmpty { defaults.removeObject(forKey: Keys.dataDir) print("[AppConfigBridge] Data directory reset to default") } else { // iOS: Only allow setting within app's container let appDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].path if dir.hasPrefix(appDir) { defaults.set(dir, forKey: Keys.dataDir) print("[AppConfigBridge] Data directory set to: \(dir)") } else { print("[AppConfigBridge] Rejected invalid data directory (outside app container): \(dir)") throw NSError(domain: "AppConfigBridge", code: -2, userInfo: [NSLocalizedDescriptionKey: "Data directory must be within app container"]) } } } func isSilentJumpAppEnabled() throws -> Bool { return defaults.bool(forKey: Keys.silentJumpApp) } func setSilentJumpAppEnabled(enabled: Bool) throws { defaults.set(enabled, forKey: Keys.silentJumpApp) print("[AppConfigBridge] Silent jump app enabled: \(enabled)") } } ================================================ FILE: ios/Runner/Bridges/AppStoreUpdateBridge.swift ================================================ import Foundation import StoreKit import UIKit import Flutter final class AppStoreUpdateBridge: NSObject, SKStoreProductViewControllerDelegate { private weak var viewController: UIViewController? init(viewController: UIViewController?) { self.viewController = viewController super.init() } func checkAndShowUpdate(result: @escaping FlutterResult) { guard let bundleId = Bundle.main.bundleIdentifier else { result(false) return } let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0" let urlString = "https://itunes.apple.com/lookup?bundleId=\(bundleId)" guard let url = URL(string: urlString) else { result(false) return } URLSession.shared.dataTask(with: url) { [weak self] data, _, error in if let error = error { print("[AppStoreUpdateBridge] Apple API error: \(error)") result(false) return } guard let data = data, let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let results = json["results"] as? [[String: Any]], let first = results.first, let latestVersion = first["version"] as? String, let trackId = first["trackId"] as? Int else { result(false) return } let hasUpdate = self?.isVersionNewer(latest: latestVersion, current: currentVersion) ?? false if !hasUpdate { result(false) return } self?.presentStoreProduct(trackId: trackId) { presented in result(presented) } }.resume() } private func presentStoreProduct(trackId: Int, completion: @escaping (Bool) -> Void) { DispatchQueue.main.async { [weak self] in guard let presenter = self?.viewController ?? UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController else { completion(false) return } let storeVC = SKStoreProductViewController() storeVC.delegate = self let parameters = [SKStoreProductParameterITunesItemIdentifier: NSNumber(value: trackId)] storeVC.loadProduct(withParameters: parameters) { loaded, error in if let error = error { print("[AppStoreUpdateBridge] Failed to load product: \(error)") completion(false) return } if loaded { presenter.present(storeVC, animated: true) { completion(true) } } else { completion(false) } } } } private func isVersionNewer(latest: String, current: String) -> Bool { let latestParts = latest.split(separator: ".").map { Int($0) ?? 0 } let currentParts = current.split(separator: ".").map { Int($0) ?? 0 } let count = max(latestParts.count, currentParts.count) for index in 0.. currentValue } } return false } func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { viewController.dismiss(animated: true) } } ================================================ FILE: ios/Runner/Bridges/CommonBridge.swift ================================================ import Flutter import Foundation import UIKit /// Bridge implementation for common native APIs class CommonBridge: NSObject, NativeCommon { private let viewController: UIViewController? init(viewController: UIViewController? = nil) { self.viewController = viewController super.init() } func startActivityFromUri(intentUri: String) throws -> Bool { print("[CommonBridge] startActivityFromUri: \(intentUri)") guard let url = URL(string: intentUri) else { print("[CommonBridge] Invalid URL: \(intentUri)") return false } // Check if the URL can be opened guard UIApplication.shared.canOpenURL(url) else { print("[CommonBridge] Cannot open URL: \(intentUri)") return false } // Open the URL UIApplication.shared.open(url, options: [:]) { success in print("[CommonBridge] Open URL result: \(success)") } return true } func getDeviceSdkInt() throws -> Int64 { // iOS doesn't have SDK int like Android, return iOS major version let systemVersion = UIDevice.current.systemVersion let majorVersion = systemVersion.components(separatedBy: ".").first ?? "0" let version = Int64(majorVersion) ?? 0 print("[CommonBridge] Device iOS version: \(version)") return version } func getDeviceCPUABI() throws -> String { // Get CPU architecture var systemInfo = utsname() uname(&systemInfo) let machineMirror = Mirror(reflecting: systemInfo.machine) let identifier = machineMirror.children.reduce("") { identifier, element in guard let value = element.value as? Int8, value != 0 else { return identifier } return identifier + String(UnicodeScalar(UInt8(value))) } print("[CommonBridge] Device CPU ABI: \(identifier)") return identifier } func getVersionName() throws -> String { let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0" print("[CommonBridge] Version name: \(version)") return version } func getVersionCode() throws -> Int64 { let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1" let code = Int64(build) ?? 1 print("[CommonBridge] Version code: \(code)") return code } func toast(msg: String) throws { print("[CommonBridge] Toast: \(msg)") showToast(message: msg, duration: 2.0) } func longToast(msg: String) throws { print("[CommonBridge] Long toast: \(msg)") showToast(message: msg, duration: 4.0) } // MARK: - Toast Helper private func showToast(message: String, duration: TimeInterval) { DispatchQueue.main.async { [weak self] in guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else { print("[CommonBridge] No key window found for toast") return } let toastLabel = UILabel() toastLabel.backgroundColor = UIColor.black.withAlphaComponent(0.7) toastLabel.textColor = UIColor.white toastLabel.textAlignment = .center toastLabel.font = UIFont.systemFont(ofSize: 14) toastLabel.text = message toastLabel.alpha = 0.0 toastLabel.layer.cornerRadius = 10 toastLabel.clipsToBounds = true toastLabel.numberOfLines = 0 let maxSize = CGSize(width: window.frame.width - 80, height: window.frame.height) let expectedSize = toastLabel.sizeThatFits(maxSize) toastLabel.frame = CGRect( x: (window.frame.width - expectedSize.width - 20) / 2, y: window.frame.height - 150, width: expectedSize.width + 20, height: expectedSize.height + 20 ) window.addSubview(toastLabel) UIView.animate(withDuration: 0.3, animations: { toastLabel.alpha = 1.0 }) { _ in UIView.animate(withDuration: 0.3, delay: duration, options: [], animations: { toastLabel.alpha = 0.0 }) { _ in toastLabel.removeFromSuperview() } } } } } ================================================ FILE: ios/Runner/Bridges/OpenListBridge.swift ================================================ import Flutter import Foundation /// Bridge implementation for Android-specific APIs (iOS equivalent) class OpenListBridge: NSObject, Android { private let registrar: FlutterPluginRegistrar? init(registrar: FlutterPluginRegistrar? = nil) { self.registrar = registrar super.init() } func addShortcut() throws { print("[OpenListBridge] addShortcut called - iOS does not support shortcuts like Android") // iOS doesn't have the same shortcut system as Android // This is a no-op on iOS } func startService() throws { print("[OpenListBridge] startService called") OpenListManager.shared.startServer() } func setAdminPwd(pwd: String) throws { print("[OpenListBridge] setAdminPwd called") try OpenListManager.shared.setAdminPassword(pwd) } func getOpenListHttpPort() throws -> Int64 { let port = OpenListManager.shared.getHttpPort() print("[OpenListBridge] getOpenListHttpPort: \(port)") return Int64(port) } func isRunning() throws -> Bool { let running = OpenListManager.shared.isRunning() print("[OpenListBridge] isRunning: \(running)") return running } func getOpenListVersion() throws -> String { // Get version from build configuration or Info.plist let version = Bundle.main.infoDictionary?["OpenListVersion"] as? String ?? "dev" print("[OpenListBridge] getOpenListVersion: \(version)") return version } } ================================================ FILE: ios/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName OpenList CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName openlist_mobile CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents NSPhotoLibraryUsageDescription This app needs access to photo library to select files NSCameraUsageDescription This app needs access to camera to take photos NSMicrophoneUsageDescription This app needs access to microphone for recording NSDocumentsFolderUsageDescription This app needs access to documents folder to manage files NSDownloadsFolderUsageDescription This app needs access to downloads folder to manage files MinimumOSVersion 12.0 UIRequiredDeviceCapabilities arm64 UIStatusBarStyle UIStatusBarStyleDefault UIViewControllerBasedStatusBarAppearance NSAppTransportSecurity NSExceptionDomains localhost NSExceptionAllowsInsecureHTTPLoads NSExceptionMinimumTLSVersion TLSv1.0 127.0.0.1 NSExceptionAllowsInsecureHTTPLoads NSExceptionMinimumTLSVersion TLSv1.0 UIBackgroundModes fetch OpenListVersion dev-ios-beta WKAppBoundDomains localhost 127.0.0.1 UIFileSharingEnabled LSSupportsOpeningDocumentsInPlace ================================================ FILE: ios/Runner/OpenListManager.swift ================================================ import Foundation import Openlistlib /// Manages OpenList core server lifecycle class OpenListManager: NSObject { static let shared = OpenListManager() private var isInitialized = false private var isServerRunning = false private var dataDir: String? // Keep strong references to prevent deallocation var eventHandler: OpenListEventHandler? var logCallback: OpenListLogCallback? private override init() { super.init() } private func ensureInitializedForConfig() throws { if isInitialized { return } let eventHandler = OpenListEventHandler() let logCallback = OpenListLogCallback() let appDelegate = UIApplication.shared.delegate as? AppDelegate eventHandler.eventAPI = appDelegate?.eventAPI logCallback.eventAPI = appDelegate?.eventAPI self.eventHandler = eventHandler self.logCallback = logCallback try initialize(event: eventHandler, logger: logCallback) } // MARK: - Initialization func initialize(event: OpenListEventHandler, logger: OpenListLogCallback) throws { guard !isInitialized else { print("[OpenListManager] Already initialized") return } // Get data directory from AppConfigBridge let appConfig = AppConfigBridge() let dataDirPath: String do { dataDirPath = try appConfig.getDataDir() self.dataDir = dataDirPath print("[OpenListManager] Data directory: \(dataDirPath)") } catch { print("[OpenListManager] Failed to get data directory: \(error)") throw error } // Set data directory for OpenList core (no error return) OpenlistlibSetConfigData(dataDirPath) // Enable stdout logging (no error return) OpenlistlibSetConfigLogStd(true) var error: NSError? OpenlistlibInit(event, logger, &error) if let err = error { print("[OpenListManager] Initialization failed: \(err)") throw err } isInitialized = true print("[OpenListManager] Initialized successfully with data directory: \(dataDirPath)") } // MARK: - Server Control func startServer() { print("[OpenListManager] Start server request received") // Check if initialized, if not, try to initialize first if !isInitialized { print("[OpenListManager] Not initialized, attempting initialization...") let eventHandler = OpenListEventHandler() let logCallback = OpenListLogCallback() // Set event API reference before initialization let appDelegate = UIApplication.shared.delegate as? AppDelegate eventHandler.eventAPI = appDelegate?.eventAPI logCallback.eventAPI = appDelegate?.eventAPI // Store references globally for persistence OpenListManager.shared.eventHandler = eventHandler OpenListManager.shared.logCallback = logCallback do { try initialize(event: eventHandler, logger: logCallback) print("[OpenListManager] Initialization completed, proceeding to start server") } catch { print("[OpenListManager] Initialization failed: \(error), cannot start server") return } } guard !isServerRunning else { print("[OpenListManager] Server already running") return } print("[OpenListManager] Starting OpenList server with data directory: \(dataDir ?? "unknown")...") DispatchQueue.global(qos: .userInitiated).async { [weak self] in OpenlistlibStart() // Small delay to ensure server is ready Thread.sleep(forTimeInterval: 0.5) DispatchQueue.main.async { self?.isServerRunning = true print("[OpenListManager] Server started successfully") // Notify Flutter side if let eventAPI = (UIApplication.shared.delegate as? AppDelegate)?.eventAPI { eventAPI.onServiceStatusChanged(isRunning: true) { result in switch result { case .failure(let error): print("[OpenListManager] Failed to notify Flutter of status change: \(error)") case .success: print("[OpenListManager] Status change notification sent to Flutter") } } } } } } func stopServer(timeout: Int64 = 5000) { guard isServerRunning else { print("[OpenListManager] Server not running") return } print("[OpenListManager] Stopping OpenList server...") var error: NSError? OpenlistlibShutdown(timeout, &error) if let err = error { print("[OpenListManager] Failed to stop server: \(err)") return } isServerRunning = false print("[OpenListManager] Server stopped") } func isRunning() -> Bool { return isServerRunning && OpenlistlibIsRunning("http") } func getHttpPort() -> Int { // Default port for OpenList return 5244 } func setAdminPassword(_ pwd: String) throws { try ensureInitializedForConfig() if let dataDir = dataDir { OpenlistlibSetConfigData(dataDir) } OpenlistlibSetAdminPassword(pwd) print("[OpenListManager] Admin password updated") } func forceDBSync() { var error: NSError? OpenlistlibForceDBSync(&error) if let err = error { print("[OpenListManager] Database sync failed: \(err)") return } print("[OpenListManager] Database sync completed") } } // MARK: - Event Handler class OpenListEventHandler: NSObject, OpenlistlibEventProtocol { weak var eventAPI: Event? func onStartError(_ t: String?, err: String?) { print("[OpenListEvent] Start error - Type: \(t ?? "unknown"), Error: \(err ?? "unknown")") // Notify Flutter side via Event API if needed } func onShutdown(_ t: String?) { print("[OpenListEvent] Shutdown - Type: \(t ?? "unknown")") // Notify Flutter side via Event API if needed } func onProcessExit(_ code: Int) { print("[OpenListEvent] Process exit - Code: \(code)") // Handle process exit if needed } } // MARK: - Log Callback class OpenListLogCallback: NSObject, OpenlistlibLogCallbackProtocol { weak var eventAPI: Event? func onLog(_ level: Int16, time: Int64, message: String?) { let logMessage = message ?? "" print("[OpenListLog] Level: \(level), Message: \(logMessage)") // Forward logs to Flutter side if let api = eventAPI { api.onServerLog(level: Int64(level), time: "\(time)", log: logMessage) { result in switch result { case .failure(let error): print("[OpenListLog] Failed to send log to Flutter: \(error)") case .success: break // Success, no action needed } } } else { print("[OpenListLog] Warning: eventAPI is nil, cannot forward log to Flutter") } } } ================================================ FILE: ios/Runner/PigeonApi.swift ================================================ // Autogenerated from Pigeon (v26.0.2), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation #if os(iOS) import Flutter #elseif os(macOS) import FlutterMacOS #else #error("Unsupported platform.") #endif /// Error class for passing custom error details to Dart side. final class PigeonError: Error { let code: String let message: String? let details: Sendable? init(code: String, message: String?, details: Sendable?) { self.code = code self.message = message self.details = details } var localizedDescription: String { return "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" } } private func wrapResult(_ result: Any?) -> [Any?] { return [result] } private func wrapError(_ error: Any) -> [Any?] { if let pigeonError = error as? PigeonError { return [ pigeonError.code, pigeonError.message, pigeonError.details, ] } if let flutterError = error as? FlutterError { return [ flutterError.code, flutterError.message, flutterError.details, ] } return [ "\(error)", "\(type(of: error))", "Stacktrace: \(Thread.callStackSymbols)", ] } private func createConnectionError(withChannelName channelName: String) -> PigeonError { return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "") } private func isNullish(_ value: Any?) -> Bool { return value is NSNull || value == nil } private func nilOrValue(_ value: Any?) -> T? { if value is NSNull { return nil } return value as! T? } private class PigeonApiPigeonCodecReader: FlutterStandardReader { } private class PigeonApiPigeonCodecWriter: FlutterStandardWriter { } private class PigeonApiPigeonCodecReaderWriter: FlutterStandardReaderWriter { override func reader(with data: Data) -> FlutterStandardReader { return PigeonApiPigeonCodecReader(data: data) } override func writer(with data: NSMutableData) -> FlutterStandardWriter { return PigeonApiPigeonCodecWriter(data: data) } } class PigeonApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { static let shared = PigeonApiPigeonCodec(readerWriter: PigeonApiPigeonCodecReaderWriter()) } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol AppConfig { func isWakeLockEnabled() throws -> Bool func setWakeLockEnabled(enabled: Bool) throws func isStartAtBootEnabled() throws -> Bool func setStartAtBootEnabled(enabled: Bool) throws func isAutoCheckUpdateEnabled() throws -> Bool func setAutoCheckUpdateEnabled(enabled: Bool) throws func isAutoOpenWebPageEnabled() throws -> Bool func setAutoOpenWebPageEnabled(enabled: Bool) throws func getDataDir() throws -> String func setDataDir(dir: String) throws func isSilentJumpAppEnabled() throws -> Bool func setSilentJumpAppEnabled(enabled: Bool) throws } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. class AppConfigSetup { static var codec: FlutterStandardMessageCodec { PigeonApiPigeonCodec.shared } /// Sets up an instance of `AppConfig` to handle messages through the `binaryMessenger`. static func setUp(binaryMessenger: FlutterBinaryMessenger, api: AppConfig?, messageChannelSuffix: String = "") { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" let isWakeLockEnabledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.AppConfig.isWakeLockEnabled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { isWakeLockEnabledChannel.setMessageHandler { _, reply in do { let result = try api.isWakeLockEnabled() reply(wrapResult(result)) } catch { reply(wrapError(error)) } } } else { isWakeLockEnabledChannel.setMessageHandler(nil) } let setWakeLockEnabledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.AppConfig.setWakeLockEnabled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { setWakeLockEnabledChannel.setMessageHandler { message, reply in let args = message as! [Any?] let enabledArg = args[0] as! Bool do { try api.setWakeLockEnabled(enabled: enabledArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { setWakeLockEnabledChannel.setMessageHandler(nil) } let isStartAtBootEnabledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.AppConfig.isStartAtBootEnabled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { isStartAtBootEnabledChannel.setMessageHandler { _, reply in do { let result = try api.isStartAtBootEnabled() reply(wrapResult(result)) } catch { reply(wrapError(error)) } } } else { isStartAtBootEnabledChannel.setMessageHandler(nil) } let setStartAtBootEnabledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.AppConfig.setStartAtBootEnabled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { setStartAtBootEnabledChannel.setMessageHandler { message, reply in let args = message as! [Any?] let enabledArg = args[0] as! Bool do { try api.setStartAtBootEnabled(enabled: enabledArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { setStartAtBootEnabledChannel.setMessageHandler(nil) } let isAutoCheckUpdateEnabledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.AppConfig.isAutoCheckUpdateEnabled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { isAutoCheckUpdateEnabledChannel.setMessageHandler { _, reply in do { let result = try api.isAutoCheckUpdateEnabled() reply(wrapResult(result)) } catch { reply(wrapError(error)) } } } else { isAutoCheckUpdateEnabledChannel.setMessageHandler(nil) } let setAutoCheckUpdateEnabledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.AppConfig.setAutoCheckUpdateEnabled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { setAutoCheckUpdateEnabledChannel.setMessageHandler { message, reply in let args = message as! [Any?] let enabledArg = args[0] as! Bool do { try api.setAutoCheckUpdateEnabled(enabled: enabledArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { setAutoCheckUpdateEnabledChannel.setMessageHandler(nil) } let isAutoOpenWebPageEnabledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.AppConfig.isAutoOpenWebPageEnabled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { isAutoOpenWebPageEnabledChannel.setMessageHandler { _, reply in do { let result = try api.isAutoOpenWebPageEnabled() reply(wrapResult(result)) } catch { reply(wrapError(error)) } } } else { isAutoOpenWebPageEnabledChannel.setMessageHandler(nil) } let setAutoOpenWebPageEnabledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.AppConfig.setAutoOpenWebPageEnabled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { setAutoOpenWebPageEnabledChannel.setMessageHandler { message, reply in let args = message as! [Any?] let enabledArg = args[0] as! Bool do { try api.setAutoOpenWebPageEnabled(enabled: enabledArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { setAutoOpenWebPageEnabledChannel.setMessageHandler(nil) } let getDataDirChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.AppConfig.getDataDir\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getDataDirChannel.setMessageHandler { _, reply in do { let result = try api.getDataDir() reply(wrapResult(result)) } catch { reply(wrapError(error)) } } } else { getDataDirChannel.setMessageHandler(nil) } let setDataDirChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.AppConfig.setDataDir\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { setDataDirChannel.setMessageHandler { message, reply in let args = message as! [Any?] let dirArg = args[0] as! String do { try api.setDataDir(dir: dirArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { setDataDirChannel.setMessageHandler(nil) } let isSilentJumpAppEnabledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.AppConfig.isSilentJumpAppEnabled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { isSilentJumpAppEnabledChannel.setMessageHandler { _, reply in do { let result = try api.isSilentJumpAppEnabled() reply(wrapResult(result)) } catch { reply(wrapError(error)) } } } else { isSilentJumpAppEnabledChannel.setMessageHandler(nil) } let setSilentJumpAppEnabledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.AppConfig.setSilentJumpAppEnabled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { setSilentJumpAppEnabledChannel.setMessageHandler { message, reply in let args = message as! [Any?] let enabledArg = args[0] as! Bool do { try api.setSilentJumpAppEnabled(enabled: enabledArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { setSilentJumpAppEnabledChannel.setMessageHandler(nil) } } } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol NativeCommon { func startActivityFromUri(intentUri: String) throws -> Bool func getDeviceSdkInt() throws -> Int64 func getDeviceCPUABI() throws -> String func getVersionName() throws -> String func getVersionCode() throws -> Int64 func toast(msg: String) throws func longToast(msg: String) throws } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. class NativeCommonSetup { static var codec: FlutterStandardMessageCodec { PigeonApiPigeonCodec.shared } /// Sets up an instance of `NativeCommon` to handle messages through the `binaryMessenger`. static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NativeCommon?, messageChannelSuffix: String = "") { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" let startActivityFromUriChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.NativeCommon.startActivityFromUri\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { startActivityFromUriChannel.setMessageHandler { message, reply in let args = message as! [Any?] let intentUriArg = args[0] as! String do { let result = try api.startActivityFromUri(intentUri: intentUriArg) reply(wrapResult(result)) } catch { reply(wrapError(error)) } } } else { startActivityFromUriChannel.setMessageHandler(nil) } let getDeviceSdkIntChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.NativeCommon.getDeviceSdkInt\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getDeviceSdkIntChannel.setMessageHandler { _, reply in do { let result = try api.getDeviceSdkInt() reply(wrapResult(result)) } catch { reply(wrapError(error)) } } } else { getDeviceSdkIntChannel.setMessageHandler(nil) } let getDeviceCPUABIChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.NativeCommon.getDeviceCPUABI\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getDeviceCPUABIChannel.setMessageHandler { _, reply in do { let result = try api.getDeviceCPUABI() reply(wrapResult(result)) } catch { reply(wrapError(error)) } } } else { getDeviceCPUABIChannel.setMessageHandler(nil) } let getVersionNameChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.NativeCommon.getVersionName\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getVersionNameChannel.setMessageHandler { _, reply in do { let result = try api.getVersionName() reply(wrapResult(result)) } catch { reply(wrapError(error)) } } } else { getVersionNameChannel.setMessageHandler(nil) } let getVersionCodeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.NativeCommon.getVersionCode\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getVersionCodeChannel.setMessageHandler { _, reply in do { let result = try api.getVersionCode() reply(wrapResult(result)) } catch { reply(wrapError(error)) } } } else { getVersionCodeChannel.setMessageHandler(nil) } let toastChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.NativeCommon.toast\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { toastChannel.setMessageHandler { message, reply in let args = message as! [Any?] let msgArg = args[0] as! String do { try api.toast(msg: msgArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { toastChannel.setMessageHandler(nil) } let longToastChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.NativeCommon.longToast\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { longToastChannel.setMessageHandler { message, reply in let args = message as! [Any?] let msgArg = args[0] as! String do { try api.longToast(msg: msgArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { longToastChannel.setMessageHandler(nil) } } } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol Android { func addShortcut() throws func startService() throws func setAdminPwd(pwd: String) throws func getOpenListHttpPort() throws -> Int64 func isRunning() throws -> Bool func getOpenListVersion() throws -> String } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. class AndroidSetup { static var codec: FlutterStandardMessageCodec { PigeonApiPigeonCodec.shared } /// Sets up an instance of `Android` to handle messages through the `binaryMessenger`. static func setUp(binaryMessenger: FlutterBinaryMessenger, api: Android?, messageChannelSuffix: String = "") { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" let addShortcutChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.Android.addShortcut\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { addShortcutChannel.setMessageHandler { _, reply in do { try api.addShortcut() reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { addShortcutChannel.setMessageHandler(nil) } let startServiceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.Android.startService\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { startServiceChannel.setMessageHandler { _, reply in do { try api.startService() reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { startServiceChannel.setMessageHandler(nil) } let setAdminPwdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.Android.setAdminPwd\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { setAdminPwdChannel.setMessageHandler { message, reply in let args = message as! [Any?] let pwdArg = args[0] as! String do { try api.setAdminPwd(pwd: pwdArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { setAdminPwdChannel.setMessageHandler(nil) } let getOpenListHttpPortChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.Android.getOpenListHttpPort\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getOpenListHttpPortChannel.setMessageHandler { _, reply in do { let result = try api.getOpenListHttpPort() reply(wrapResult(result)) } catch { reply(wrapError(error)) } } } else { getOpenListHttpPortChannel.setMessageHandler(nil) } let isRunningChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.Android.isRunning\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { isRunningChannel.setMessageHandler { _, reply in do { let result = try api.isRunning() reply(wrapResult(result)) } catch { reply(wrapError(error)) } } } else { isRunningChannel.setMessageHandler(nil) } let getOpenListVersionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.openlist_mobile.Android.getOpenListVersion\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getOpenListVersionChannel.setMessageHandler { _, reply in do { let result = try api.getOpenListVersion() reply(wrapResult(result)) } catch { reply(wrapError(error)) } } } else { getOpenListVersionChannel.setMessageHandler(nil) } } } /// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. protocol EventProtocol { func onServiceStatusChanged(isRunning isRunningArg: Bool, completion: @escaping (Result) -> Void) func onServerLog(level levelArg: Int64, time timeArg: String, log logArg: String, completion: @escaping (Result) -> Void) } class Event: EventProtocol { private let binaryMessenger: FlutterBinaryMessenger private let messageChannelSuffix: String init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") { self.binaryMessenger = binaryMessenger self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" } var codec: PigeonApiPigeonCodec { return PigeonApiPigeonCodec.shared } func onServiceStatusChanged(isRunning isRunningArg: Bool, completion: @escaping (Result) -> Void) { let channelName: String = "dev.flutter.pigeon.openlist_mobile.Event.onServiceStatusChanged\(messageChannelSuffix)" let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) channel.sendMessage([isRunningArg] as [Any?]) { response in guard let listResponse = response as? [Any?] else { completion(.failure(createConnectionError(withChannelName: channelName))) return } if listResponse.count > 1 { let code: String = listResponse[0] as! String let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(PigeonError(code: code, message: message, details: details))) } else { completion(.success(())) } } } func onServerLog(level levelArg: Int64, time timeArg: String, log logArg: String, completion: @escaping (Result) -> Void) { let channelName: String = "dev.flutter.pigeon.openlist_mobile.Event.onServerLog\(messageChannelSuffix)" let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) channel.sendMessage([levelArg, timeArg, logArg] as [Any?]) { response in guard let listResponse = response as? [Any?] else { completion(.failure(createConnectionError(withChannelName: channelName))) return } if listResponse.count > 1 { let code: String = listResponse[0] as! String let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(PigeonError(code: code, message: message, details: details))) } else { completion(.success(())) } } } } ================================================ FILE: ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; AA1234561234567890ABCDE1 /* PigeonApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDE0 /* PigeonApi.swift */; }; AA1234561234567890ABCDE3 /* OpenListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDE2 /* OpenListManager.swift */; }; AA1234561234567890ABCDE5 /* OpenListBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDE4 /* OpenListBridge.swift */; }; AA1234561234567890ABCDE7 /* AppConfigBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDE6 /* AppConfigBridge.swift */; }; AA1234561234567890ABCDE9 /* CommonBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDE8 /* CommonBridge.swift */; }; AA1234561234567890ABCDEF /* AppStoreUpdateBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDEE /* AppStoreUpdateBridge.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; AA1234561234567890ABCDE0 /* PigeonApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonApi.swift; sourceTree = ""; }; AA1234561234567890ABCDE2 /* OpenListManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenListManager.swift; sourceTree = ""; }; AA1234561234567890ABCDE4 /* OpenListBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenListBridge.swift; sourceTree = ""; }; AA1234561234567890ABCDE6 /* AppConfigBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigBridge.swift; sourceTree = ""; }; AA1234561234567890ABCDE8 /* CommonBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonBridge.swift; sourceTree = ""; }; AA1234561234567890ABCDEE /* AppStoreUpdateBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreUpdateBridge.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C807B294A618700263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( AA1234561234567890ABCDEA /* Bridges */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, AA1234561234567890ABCDE0 /* PigeonApi.swift */, AA1234561234567890ABCDE2 /* OpenListManager.swift */, ); path = Runner; sourceTree = ""; }; AA1234561234567890ABCDEA /* Bridges */ = { isa = PBXGroup; children = ( AA1234561234567890ABCDEE /* AppStoreUpdateBridge.swift */, AA1234561234567890ABCDE4 /* OpenListBridge.swift */, AA1234561234567890ABCDE6 /* AppConfigBridge.swift */, AA1234561234567890ABCDE8 /* CommonBridge.swift */, ); path = Bridges; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C8080294A63A400263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, ); buildRules = ( ); dependencies = ( 331C8086294A63A400263BE5 /* PBXTargetDependency */, ); name = RunnerTests; productName = RunnerTests; productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 97C146ED1CF9000F007C117D; }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 331C807F294A63A400263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 331C807D294A63A400263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, AA1234561234567890ABCDE1 /* PigeonApi.swift in Sources */, AA1234561234567890ABCDE3 /* OpenListManager.swift in Sources */, AA1234561234567890ABCDEF /* AppStoreUpdateBridge.swift in Sources */, AA1234561234567890ABCDE5 /* OpenListBridge.swift in Sources */, AA1234561234567890ABCDE7 /* AppConfigBridge.swift in Sources */, AA1234561234567890ABCDE9 /* CommonBridge.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = org.oplist.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.openlist.mobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Debug; }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.openlist.mobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Release; }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.openlist.mobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = org.oplist.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = org.oplist.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C8088294A63A400263BE5 /* Debug */, 331C8089294A63A400263BE5 /* Release */, 331C808A294A63A400263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/RunnerTests/RunnerTests.swift ================================================ import Flutter import UIKit import XCTest class RunnerTests: XCTestCase { func testExample() { // If you add code to the Runner application, consider adding tests here. // See https://developer.apple.com/documentation/xctest for more information about using XCTest. } } ================================================ FILE: ios/scripts/generate_ios_icons.py ================================================ #!/usr/bin/env python3 """ iOS App Icon Generator Generates all required iOS app icon sizes from source PNG files """ import os import sys from PIL import Image # Icon size mappings: (filename, target_size_px) ICON_MAPPINGS = [ ("Icon-App-20x20@1x.png", 20), ("Icon-App-20x20@2x.png", 40), ("Icon-App-20x20@3x.png", 60), ("Icon-App-29x29@1x.png", 29), ("Icon-App-29x29@2x.png", 58), ("Icon-App-29x29@3x.png", 87), ("Icon-App-40x40@1x.png", 40), ("Icon-App-40x40@2x.png", 80), ("Icon-App-40x40@3x.png", 120), ("Icon-App-60x60@2x.png", 120), ("Icon-App-60x60@3x.png", 180), ("Icon-App-76x76@1x.png", 76), ("Icon-App-76x76@2x.png", 152), ("Icon-App-83.5x83.5@2x.png", 167), ("Icon-App-1024x1024@1x.png", 1024), ] def find_best_source_image(logo_dir, target_size): """Find the best source image (closest size >= target size)""" available_sizes = [16, 24, 32, 48, 64, 72, 96, 120, 128, 144, 160, 192, 224, 240, 248, 256, 300, 320, 384, 512, 1024] # Find the smallest size that's >= target size for size in available_sizes: if size >= target_size: source_path = os.path.join(logo_dir, f"{size}x{size}.png") if os.path.exists(source_path): return source_path # Fallback to largest available return os.path.join(logo_dir, "1024x1024.png") def generate_icon(source_path, output_path, target_size): """Resize and save icon""" try: img = Image.open(source_path) img = img.resize((target_size, target_size), Image.Resampling.LANCZOS) img.save(output_path, "PNG", optimize=True) print(f"✓ Generated: {os.path.basename(output_path)} ({target_size}x{target_size})") return True except Exception as e: print(f"✗ Failed to generate {os.path.basename(output_path)}: {e}") return False def main(): # Paths script_dir = os.path.dirname(os.path.abspath(__file__)) ios_dir = os.path.dirname(script_dir) flutter_project_root = os.path.dirname(ios_dir) workspace_root = os.path.dirname(flutter_project_root) logo_dir = os.path.join(workspace_root, "Logo", "logo") output_dir = os.path.join(ios_dir, "Runner", "Assets.xcassets", "AppIcon.appiconset") # Verify directories if not os.path.exists(logo_dir): print(f"Error: Logo directory not found: {logo_dir}") sys.exit(1) if not os.path.exists(output_dir): print(f"Error: Output directory not found: {output_dir}") sys.exit(1) print(f"Source: {logo_dir}") print(f"Output: {output_dir}") print("-" * 60) # Generate all icons success_count = 0 for filename, target_size in ICON_MAPPINGS: source_path = find_best_source_image(logo_dir, target_size) output_path = os.path.join(output_dir, filename) if generate_icon(source_path, output_path, target_size): success_count += 1 print("-" * 60) print(f"✓ Successfully generated {success_count}/{len(ICON_MAPPINGS)} icons") if success_count == len(ICON_MAPPINGS): print("✓ All iOS app icons generated successfully!") return 0 else: print("✗ Some icons failed to generate") return 1 if __name__ == "__main__": sys.exit(main()) ================================================ FILE: lib/contant/log_level.dart ================================================ import 'dart:ui'; class LogLevel { static const int panic = 0; static const int fatal = 1; static const int error = 2; static const int warn = 3; static const int info = 4; static const int debug = 5; static const int trace = 6; static Color toColor(int level) { //Color.fromARGB(a, r, g, b) return switch(level) { LogLevel.panic => const Color.fromARGB(255, 255, 0, 0), LogLevel.fatal => const Color.fromARGB(255, 255, 0, 0), LogLevel.error => const Color.fromARGB(255, 255, 0, 0), LogLevel.warn => const Color.fromARGB(255, 255, 165, 0), LogLevel.info => const Color.fromARGB(255, 0, 0, 255), LogLevel.debug => const Color.fromARGB(255, 0, 255, 0), LogLevel.trace => const Color.fromARGB(255, 0, 255, 0), _ => const Color.fromARGB(255, 0, 0, 0) }; } static String toStr(int level) { return switch(level) { LogLevel.panic => "Panic", LogLevel.fatal => "Fatal", LogLevel.error => "Error", LogLevel.warn => "Warn", LogLevel.info => "Info", LogLevel.debug => "Debug", LogLevel.trace => "Trace", _ => "" }; } } ================================================ FILE: lib/contant/native_bridge.dart ================================================ import 'package:openlist_mobile/generated_api.dart'; class NativeBridge { static NativeCommon common = NativeCommon(); static Android android = Android(); static AppConfig appConfig = AppConfig(); } ================================================ FILE: lib/generated/intl/messages_all.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that looks up messages for specific locales by // delegating to the appropriate library. // Ignore issues from commonly used lints in this file. // ignore_for_file:implementation_imports, file_names, unnecessary_new // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering // ignore_for_file:argument_type_not_assignable, invalid_assignment // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases // ignore_for_file:comment_references import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; import 'package:intl/src/intl_helpers.dart'; import 'messages_en.dart' as messages_en; import 'messages_zh.dart' as messages_zh; typedef Future LibraryLoader(); Map _deferredLibraries = { 'en': () => new SynchronousFuture(null), 'zh': () => new SynchronousFuture(null), }; MessageLookupByLibrary? _findExact(String localeName) { switch (localeName) { case 'en': return messages_en.messages; case 'zh': return messages_zh.messages; default: return null; } } /// User programs should call this before using [localeName] for messages. Future initializeMessages(String localeName) { var availableLocale = Intl.verifiedLocale( localeName, (locale) => _deferredLibraries[locale] != null, onFailure: (_) => null, ); if (availableLocale == null) { return new SynchronousFuture(false); } var lib = _deferredLibraries[availableLocale]; lib == null ? new SynchronousFuture(false) : lib(); initializeInternalMessageLookup(() => new CompositeMessageLookup()); messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); return new SynchronousFuture(true); } bool _messagesExistFor(String locale) { try { return _findExact(locale) != null; } catch (e) { return false; } } MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { var actualLocale = Intl.verifiedLocale( locale, _messagesExistFor, onFailure: (_) => null, ); if (actualLocale == null) return null; return _findExact(actualLocale); } ================================================ FILE: lib/generated/intl/messages_en.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that provides messages for a en locale. All the // messages from the main program should be duplicated here with the same // function name. // Ignore issues from commonly used lints in this file. // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; final messages = new MessageLookup(); typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'en'; static String m0(error) => "Failed to cancel all notifications: ${error}"; static String m1(error) => "Failed to cancel download notification: ${error}"; static String m2(error) => "Failed to check install permission: ${error}"; static String m3(error) => "Failed to clear download directory: ${error}"; static String m4(filename) => "Are you sure you want to cancel downloading \"${filename}\"?"; static String m5(filename) => "Are you sure you want to delete file \"${filename}\"? This action cannot be undone."; static String m6(filename) => "Are you sure you want to delete the download record of \"${filename}\"?"; static String m7(error) => "Failed to create OpenList directory: ${error}"; static String m8(path) => "Create OpenList download directory: ${path}"; static String m9(count) => "Currently ${count} files are downloading"; static String m10(error) => "Failed to delete file: ${error}"; static String m11(url) => "Download cancelled: ${url}"; static String m12(filename) => "Download complete: ${filename}"; static String m13(filename) => "Download complete: ${filename}"; static String m14(filename) => "${filename} download completed"; static String m15(filename) => "Download failed: ${filename}"; static String m16(filename) => "Download failed: ${filename}"; static String m17(count) => "Download (${count})"; static String m18(progress) => "Download progress: ${progress}%"; static String m19(current, total) => "Downloading file ${current}/${total}"; static String m20(filename) => "File deleted: ${filename}"; static String m21(index) => "File ${index} download failed"; static String m22(size) => "Size: ${size}"; static String m23(time) => "Time: ${time}"; static String m24(error) => "Failed to get download directory: ${error}"; static String m25(error) => "Failed to get download file list: ${error}"; static String m26(line, error) => "Invalid JSON format at line ${line}: ${error}"; static String m27(error) => "Load failed: ${error}"; static String m28(count) => "${count} files completed, click to jump to download manager"; static String m29(payload) => "Notification clicked: ${payload}"; static String m30(error) => "Failed to initialize notification manager: ${error}"; static String m31(error) => "Failed to open download directory: ${error}"; static String m32(error) => "Open file exception: ${error}"; static String m33(error) => "Failed to open file: ${error}"; static String m34(error) => "Failed to open file manager: ${error}"; static String m35(type, message) => "Open file result: ${type} - ${message}"; static String m36(path) => "OpenList download directory: ${path}"; static String m37(error) => "Failed to parse filename: ${error}"; static String m38(error) => "Restore backup failed: ${error}"; static String m39(error) => "Save failed: ${error}"; static String m40(error) => "Failed to show download complete notification: ${error}"; static String m41(error) => "Failed to show download progress notification: ${error}"; static String m42(error) => "Failed to show single file complete notification: ${error}"; static String m43(filename) => "Start download: ${filename}"; static String m44(filename) => "Start download: ${filename}"; static String m45(path) => "Trying to open file: ${path}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("About"), "apkDownloadCompleteMessage": MessageLookupByLibrary.simpleMessage( "APK file download completed, do you want to install?", ), "appName": MessageLookupByLibrary.simpleMessage("OpenList"), "autoCheckForUpdates": MessageLookupByLibrary.simpleMessage( "Auto check for updates", ), "autoCheckForUpdatesDesc": MessageLookupByLibrary.simpleMessage( "Check for updates when app starts", ), "autoStartIssue": MessageLookupByLibrary.simpleMessage( "Auto-Start Information", ), "autoStartIssueDesc": MessageLookupByLibrary.simpleMessage( "When enabling auto-start, it\'s recommended to disable battery optimization for the app. Currently, after enabling auto-start, the service will automatically start in the background after system reboot, but may not show a notification in the notification bar. Rest assured, the service is running normally. You can check the service status through the quick settings tile in the notification shade, or return to the main interface to check the service toggle to confirm if the service has started.", ), "autoStartWebPage": MessageLookupByLibrary.simpleMessage( "Set web page as startup page", ), "autoStartWebPageDesc": MessageLookupByLibrary.simpleMessage( "Default page when opening main interface", ), "backupRestored": MessageLookupByLibrary.simpleMessage( "Backup restored successfully", ), "batchDownloadComplete": MessageLookupByLibrary.simpleMessage( "Batch download complete", ), "bootAutoStartService": MessageLookupByLibrary.simpleMessage( "Boot auto-start service", ), "bootAutoStartServiceDesc": MessageLookupByLibrary.simpleMessage( "Automatically start OpenList service after boot. (Please make sure to grant auto-start permission)", ), "browserDownload": MessageLookupByLibrary.simpleMessage("Browser Download"), "browserDownloadMethod": MessageLookupByLibrary.simpleMessage( "Browser Download", ), "browserDownloadMethodDesc": MessageLookupByLibrary.simpleMessage( "Use system browser", ), "cancel": MessageLookupByLibrary.simpleMessage("Cancel"), "cancelAllNotificationsFailed": m0, "cancelDownload": MessageLookupByLibrary.simpleMessage("Cancel Download"), "cancelDownloadNotificationFailed": m1, "cancelled": MessageLookupByLibrary.simpleMessage("Cancelled"), "cannotGetBaseDownloadDirectory": MessageLookupByLibrary.simpleMessage( "Cannot get base download directory", ), "cannotGetDownloadDirectory": MessageLookupByLibrary.simpleMessage( "Cannot get download directory", ), "cannotGetDownloadDirectoryError": MessageLookupByLibrary.simpleMessage( "Cannot get download directory", ), "cannotInstallApkFile": MessageLookupByLibrary.simpleMessage( "Cannot install APK file, you may need to enable \"Install unknown apps\" in settings", ), "cannotInstallApkNeedPermission": MessageLookupByLibrary.simpleMessage( "Cannot install APK file, you may need to enable \"Install unknown apps\" in settings", ), "checkDownloadManagerForFiles": MessageLookupByLibrary.simpleMessage( "Please check download manager via bottom navigation bar to view download files", ), "checkForUpdates": MessageLookupByLibrary.simpleMessage( "Check for updates", ), "checkImageInDownloadFolder": MessageLookupByLibrary.simpleMessage( "Please check image in download folder", ), "checkInstallPermissionFailed": m2, "clear": MessageLookupByLibrary.simpleMessage("Clear"), "clearAll": MessageLookupByLibrary.simpleMessage("Clear All"), "clearDownloadDirectoryFailed": m3, "clearFailed": MessageLookupByLibrary.simpleMessage("Clear failed"), "clearRecords": MessageLookupByLibrary.simpleMessage("Clear Records"), "cleared": MessageLookupByLibrary.simpleMessage( "Download directory cleared", ), "clickToJumpToDownloadManager": MessageLookupByLibrary.simpleMessage( "Click to jump to download manager", ), "completed": MessageLookupByLibrary.simpleMessage("Completed"), "completedTime": MessageLookupByLibrary.simpleMessage("Completed time"), "configSavedRestartRequired": MessageLookupByLibrary.simpleMessage( "Config saved. Please restart OpenList service to take effect.", ), "confirm": MessageLookupByLibrary.simpleMessage("OK"), "confirmCancelDownload": m4, "confirmClear": MessageLookupByLibrary.simpleMessage("Confirm Clear"), "confirmClearAllFiles": MessageLookupByLibrary.simpleMessage( "Are you sure you want to clear all download files? This action cannot be undone.", ), "confirmDelete": MessageLookupByLibrary.simpleMessage("Confirm Delete"), "confirmDeleteFile": m5, "confirmDeleteRecord": m6, "confirmDownload": MessageLookupByLibrary.simpleMessage("Confirm Download"), "confirmDownloadMessage": MessageLookupByLibrary.simpleMessage( "Do you want to download this file?", ), "confirmSaveConfigMessage": MessageLookupByLibrary.simpleMessage( "Modifying configuration may cause service unavailable. Are you sure to save?", ), "confirmSaveConfigTitle": MessageLookupByLibrary.simpleMessage( "Confirm Save", ), "continueDownload": MessageLookupByLibrary.simpleMessage( "Continue Download", ), "copiedToClipboard": MessageLookupByLibrary.simpleMessage( "Copied to clipboard", ), "createOpenListDirectoryFailed": m7, "createOpenListDownloadDirectory": m8, "currentDownloadingFiles": m9, "currentIsLatestVersion": MessageLookupByLibrary.simpleMessage( "Current is latest version", ), "currentlyDownloading": MessageLookupByLibrary.simpleMessage("Downloading"), "dataDirectory": MessageLookupByLibrary.simpleMessage("data Directory"), "databaseNotSavedIssue": MessageLookupByLibrary.simpleMessage( "Database Not Saved Issue", ), "databaseNotSavedIssueDesc": MessageLookupByLibrary.simpleMessage( "If you don\'t manually close OpenList, the database may not be saved to the corresponding db file. If you encounter this issue, please manually close the app to resolve it. (The switch is located in the main program menu on the OpenList interface, as well as in the notification bar)", ), "delete": MessageLookupByLibrary.simpleMessage("Delete"), "deleteFailed": MessageLookupByLibrary.simpleMessage("Delete failed"), "deleteFile": MessageLookupByLibrary.simpleMessage("Delete file"), "deleteFileFailedLog": m10, "deleteRecord": MessageLookupByLibrary.simpleMessage("Delete record"), "description": MessageLookupByLibrary.simpleMessage("Description:"), "desktopShortcut": MessageLookupByLibrary.simpleMessage("Desktop shortcut"), "directDownload": MessageLookupByLibrary.simpleMessage("Direct Download"), "directDownloadApk": MessageLookupByLibrary.simpleMessage( "Direct Download APK", ), "directDownloadMethod": MessageLookupByLibrary.simpleMessage( "Direct Download", ), "directDownloadMethodDesc": MessageLookupByLibrary.simpleMessage( "Use in-app downloader", ), "download": MessageLookupByLibrary.simpleMessage("download"), "downloadApk": MessageLookupByLibrary.simpleMessage("Download APK"), "downloadCancelled": m11, "downloadCancelledStatus": MessageLookupByLibrary.simpleMessage( "Download cancelled", ), "downloadCancelledText": MessageLookupByLibrary.simpleMessage( "Download cancelled", ), "downloadComplete": m12, "downloadCompleteChannel": MessageLookupByLibrary.simpleMessage( "Download Complete", ), "downloadCompleteChannelDesc": MessageLookupByLibrary.simpleMessage( "File download complete notification", ), "downloadCompleteFile": m13, "downloadCompleteNotificationTitle": m14, "downloadCompleteTitle": MessageLookupByLibrary.simpleMessage( "Download Complete", ), "downloadDirectory": MessageLookupByLibrary.simpleMessage( "Download Directory", ), "downloadDirectoryCleared": MessageLookupByLibrary.simpleMessage( "Download directory cleared", ), "downloadDirectoryOpened": MessageLookupByLibrary.simpleMessage( "Download directory opened", ), "downloadDirectoryPathUnknown": MessageLookupByLibrary.simpleMessage( "Download directory path unknown", ), "downloadFailed": MessageLookupByLibrary.simpleMessage("Download failed"), "downloadFailedFile": m15, "downloadFailedWithError": m16, "downloadFunctionTest": MessageLookupByLibrary.simpleMessage( "Download Function Test", ), "downloadInstructions": MessageLookupByLibrary.simpleMessage( "• Files will be downloaded to system download directory\\n• Download progress will show notifications\\n• You can choose to open file after download completes\\n• If filename exists, a number will be added automatically\\n• Please check download manager via bottom navigation bar to view files", ), "downloadManager": MessageLookupByLibrary.simpleMessage("Download"), "downloadManagerWithCount": m17, "downloadProgress": m18, "downloadProgressChannel": MessageLookupByLibrary.simpleMessage( "Download Progress", ), "downloadProgressDesc": MessageLookupByLibrary.simpleMessage( "Show file download progress", ), "downloadRecordsCleared": MessageLookupByLibrary.simpleMessage( "Download records cleared", ), "downloadThisFile": MessageLookupByLibrary.simpleMessage( "Download this file?", ), "downloading": MessageLookupByLibrary.simpleMessage("Downloading"), "downloadingFileProgress": m19, "downloadingImage": MessageLookupByLibrary.simpleMessage( "Downloading image...", ), "edit": MessageLookupByLibrary.simpleMessage("Edit"), "editOpenListConfig": MessageLookupByLibrary.simpleMessage( "Edit OpenList Config", ), "english": MessageLookupByLibrary.simpleMessage("English"), "failed": MessageLookupByLibrary.simpleMessage("Failed"), "fileDeleted": MessageLookupByLibrary.simpleMessage("File deleted"), "fileDeletedLog": m20, "fileDownloadFailed": m21, "fileInfo": MessageLookupByLibrary.simpleMessage("File info"), "fileLocation": MessageLookupByLibrary.simpleMessage("File Location"), "fileLocationTip": MessageLookupByLibrary.simpleMessage( "You can use a file manager to find this file, or try installing the appropriate app to open it.", ), "fileManagerOpened": MessageLookupByLibrary.simpleMessage( "File manager opened", ), "fileName": MessageLookupByLibrary.simpleMessage("File name"), "fileNotFound": MessageLookupByLibrary.simpleMessage( "File not found or has been deleted", ), "fileNotFoundWillCreateOnSave": MessageLookupByLibrary.simpleMessage( "File not found. Will create on save.", ), "filePath": MessageLookupByLibrary.simpleMessage("Path"), "filePermissionDenied": MessageLookupByLibrary.simpleMessage( "File permission denied. Please check app permissions.", ), "fileSavedTo": MessageLookupByLibrary.simpleMessage("File saved to:"), "fileSize": m22, "fileTime": m23, "findApkInDownloadFolder": MessageLookupByLibrary.simpleMessage( "Please find APK file in download folder to install", ), "followSystem": MessageLookupByLibrary.simpleMessage("Follow System"), "general": MessageLookupByLibrary.simpleMessage("General"), "getDownloadDirectoryFailed": m24, "getDownloadFileListFailed": m25, "getDownloadPathFailed": MessageLookupByLibrary.simpleMessage( "Failed to get", ), "goTo": MessageLookupByLibrary.simpleMessage("GO"), "goToSettings": MessageLookupByLibrary.simpleMessage("Go to Settings"), "grantManagerStoragePermission": MessageLookupByLibrary.simpleMessage( "Grant [Manage external storage] permission", ), "grantNotificationPermission": MessageLookupByLibrary.simpleMessage( "Grant [Notification] permission", ), "grantNotificationPermissionDesc": MessageLookupByLibrary.simpleMessage( "Used for foreground service keep alive", ), "grantStoragePermission": MessageLookupByLibrary.simpleMessage( "Grant [external storage] permission", ), "grantStoragePermissionDesc": MessageLookupByLibrary.simpleMessage( "Mounting local storage is a must, otherwise no permission to read and write files", ), "imageDownloadSuccess": MessageLookupByLibrary.simpleMessage( "Image download success", ), "importantSettings": MessageLookupByLibrary.simpleMessage( "Important settings", ), "inProgress": MessageLookupByLibrary.simpleMessage("In Progress"), "initializingNotificationManager": MessageLookupByLibrary.simpleMessage( "Initializing notification manager", ), "installNow": MessageLookupByLibrary.simpleMessage("Install Now"), "invalidJsonFormat": m26, "jumpToOtherApp": MessageLookupByLibrary.simpleMessage( "Jump to other app?", ), "language": MessageLookupByLibrary.simpleMessage("Language"), "languageSettings": MessageLookupByLibrary.simpleMessage( "Language Settings", ), "languageSettingsDesc": MessageLookupByLibrary.simpleMessage( "Select app display language", ), "laterInstall": MessageLookupByLibrary.simpleMessage("Install Later"), "loadDownloadFilesFailed": MessageLookupByLibrary.simpleMessage( "Failed to load download files", ), "loadFailed": m27, "modifiedTime": MessageLookupByLibrary.simpleMessage("Modified time"), "modifyAdminPassword": MessageLookupByLibrary.simpleMessage( "Modify Admin Password", ), "moreOptions": MessageLookupByLibrary.simpleMessage("More options"), "multipleFilesCompleted": m28, "needInstallPermission": MessageLookupByLibrary.simpleMessage( "Install Permission Required", ), "needInstallPermissionDesc": MessageLookupByLibrary.simpleMessage( "To install APK files, install permission is required. Please enable it manually in settings.", ), "needInstallPermissionToInstallApk": MessageLookupByLibrary.simpleMessage( "Install permission is required to install APK files", ), "newVersionFound": MessageLookupByLibrary.simpleMessage( "New Version Found", ), "noActiveDownloads": MessageLookupByLibrary.simpleMessage( "No active downloads", ), "noAppToOpenFile": MessageLookupByLibrary.simpleMessage( "No app found to open this file", ), "noBackupFound": MessageLookupByLibrary.simpleMessage( "No backup file found", ), "noCompletedDownloads": MessageLookupByLibrary.simpleMessage( "No completed downloads", ), "noPermissionToInstallApk": MessageLookupByLibrary.simpleMessage( "No permission to install APK file, please enable install permission in settings", ), "noPermissionToInstallApkFile": MessageLookupByLibrary.simpleMessage( "No permission to install APK file, please enable install permission in settings", ), "noPermissionToOpenFile": MessageLookupByLibrary.simpleMessage( "No permission to open this file", ), "notificationClicked": m29, "notificationManagerInitFailed": m30, "notificationManagerInitialized": MessageLookupByLibrary.simpleMessage( "Notification manager initialized successfully", ), "ok": MessageLookupByLibrary.simpleMessage("OK"), "open": MessageLookupByLibrary.simpleMessage("Open"), "openDirectory": MessageLookupByLibrary.simpleMessage("Open Directory"), "openDownloadDirectoryFailed": m31, "openDownloadManager": MessageLookupByLibrary.simpleMessage( "Open Download Manager", ), "openDownloadTestPage": MessageLookupByLibrary.simpleMessage( "Do you want to open download test page?", ), "openFile": MessageLookupByLibrary.simpleMessage("Open file"), "openFileException": m32, "openFileFailed": m33, "openFileManager": MessageLookupByLibrary.simpleMessage( "Open File Manager", ), "openFileManagerFailed": m34, "openFileResult": m35, "openListDownloadDirectory": m36, "openSourceLicenses": MessageLookupByLibrary.simpleMessage( "Open Source Licenses", ), "openlist": MessageLookupByLibrary.simpleMessage("OpenList"), "openlistMobile": MessageLookupByLibrary.simpleMessage("OpenList Mobile"), "parseFilenameFailed": m37, "pending": MessageLookupByLibrary.simpleMessage("Pending"), "preparingDownload": MessageLookupByLibrary.simpleMessage( "Preparing download...", ), "preparingDownloadStatus": MessageLookupByLibrary.simpleMessage( "Preparing download...", ), "preview": MessageLookupByLibrary.simpleMessage("Preview"), "refresh": MessageLookupByLibrary.simpleMessage("Refresh"), "releasePage": MessageLookupByLibrary.simpleMessage("Release Page"), "restartingService": MessageLookupByLibrary.simpleMessage( "Restarting OpenList service...", ), "restoreBackup": MessageLookupByLibrary.simpleMessage("Restore Backup"), "restoreBackupFailed": m38, "save": MessageLookupByLibrary.simpleMessage("Save"), "saveAndRestart": MessageLookupByLibrary.simpleMessage("Save and Restart"), "saveFailed": m39, "saveOnly": MessageLookupByLibrary.simpleMessage("Save Only"), "saved": MessageLookupByLibrary.simpleMessage("Saved"), "selectAppToOpen": MessageLookupByLibrary.simpleMessage( "Select app to open", ), "selectDownloadMethod": MessageLookupByLibrary.simpleMessage( "Select download method", ), "serviceRestartFailed": MessageLookupByLibrary.simpleMessage( "Failed to restart service. Please restart manually.", ), "serviceRestartOnlyAndroid": MessageLookupByLibrary.simpleMessage( "Service restart is only supported on Android", ), "serviceRestartSuccess": MessageLookupByLibrary.simpleMessage( "Service restarted successfully", ), "setAdminPassword": MessageLookupByLibrary.simpleMessage( "Set Admin password", ), "setDefaultDirectory": MessageLookupByLibrary.simpleMessage( "Set as default directory?", ), "settings": MessageLookupByLibrary.simpleMessage("Settings"), "shareFeatureNotImplemented": MessageLookupByLibrary.simpleMessage( "Share feature not implemented yet", ), "shareFile": MessageLookupByLibrary.simpleMessage("Share file"), "shareLink": MessageLookupByLibrary.simpleMessage("Share Link"), "shareLinkDesc": MessageLookupByLibrary.simpleMessage( "Share download link", ), "showDownloadCompleteNotificationFailed": m40, "showDownloadProgressNotificationFailed": m41, "showInFileManager": MessageLookupByLibrary.simpleMessage( "Show in file manager", ), "showSingleFileCompleteNotificationFailed": m42, "silentJumpApp": MessageLookupByLibrary.simpleMessage("Silent jump app"), "silentJumpAppDesc": MessageLookupByLibrary.simpleMessage( "Jump to other app without prompt", ), "simplifiedChinese": MessageLookupByLibrary.simpleMessage("简体中文"), "size": MessageLookupByLibrary.simpleMessage("Size"), "startDownload": m43, "startDownloadFile": m44, "startTime": MessageLookupByLibrary.simpleMessage("Start time"), "testDirectDownloadFunction": MessageLookupByLibrary.simpleMessage( "Test direct download function", ), "testDownloadJsonFile": MessageLookupByLibrary.simpleMessage( "Test download JSON file", ), "testDownloadLargeFile": MessageLookupByLibrary.simpleMessage( "Test download large file (1MB)", ), "testDownloadPngImage": MessageLookupByLibrary.simpleMessage( "Test download PNG image", ), "troubleshooting": MessageLookupByLibrary.simpleMessage("Troubleshooting"), "troubleshootingDesc": MessageLookupByLibrary.simpleMessage( "Common issues and solutions", ), "tryToOpenFile": m45, "uiSettings": MessageLookupByLibrary.simpleMessage("UI"), "userCancelledDownload": MessageLookupByLibrary.simpleMessage( "User cancelled download", ), "userCancelledDownloadError": MessageLookupByLibrary.simpleMessage( "User cancelled download", ), "view": MessageLookupByLibrary.simpleMessage("View"), "viewDownloadDirectory": MessageLookupByLibrary.simpleMessage( "View download directory", ), "viewDownloadFiles": MessageLookupByLibrary.simpleMessage( "View download files", ), "viewDownloads": MessageLookupByLibrary.simpleMessage("View Downloads"), "viewLocation": MessageLookupByLibrary.simpleMessage("View Location"), "viewThirdPartyLicenses": MessageLookupByLibrary.simpleMessage( "View third-party licenses", ), "wakeLock": MessageLookupByLibrary.simpleMessage("Wake lock"), "wakeLockDesc": MessageLookupByLibrary.simpleMessage( "Prevent CPU from sleeping when screen is off. (May cause app killed in background on some devices)", ), "webPage": MessageLookupByLibrary.simpleMessage("Web Page"), }; } ================================================ FILE: lib/generated/intl/messages_zh.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that provides messages for a zh locale. All the // messages from the main program should be duplicated here with the same // function name. // Ignore issues from commonly used lints in this file. // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; final messages = new MessageLookup(); typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'zh'; static String m0(error) => "取消所有通知失败: ${error}"; static String m1(error) => "取消下载通知失败: ${error}"; static String m2(error) => "检查安装权限失败: ${error}"; static String m3(error) => "清理下载目录失败: ${error}"; static String m4(filename) => "确定要取消下载 \"${filename}\" 吗?"; static String m5(filename) => "确定要删除文件 \"${filename}\" 吗?此操作不可撤销。"; static String m6(filename) => "确定要删除 \"${filename}\" 的下载记录吗?"; static String m7(error) => "创建OpenList目录失败: ${error}"; static String m8(path) => "创建OpenList下载目录: ${path}"; static String m9(count) => "当前有 ${count} 个文件在下载"; static String m10(error) => "删除文件失败: ${error}"; static String m11(url) => "下载已取消: ${url}"; static String m12(filename) => "下载完成: ${filename}"; static String m13(filename) => "下载完成: ${filename}"; static String m14(filename) => "${filename} 下载完毕"; static String m15(filename) => "下载失败: ${filename}"; static String m16(filename) => "下载失败: ${filename}"; static String m17(count) => "下载管理(${count})"; static String m18(progress) => "下载进度: ${progress}%"; static String m19(current, total) => "正在下载第 ${current}/${total} 个文件"; static String m20(filename) => "已删除文件: ${filename}"; static String m21(index) => "第 ${index} 个文件下载失败"; static String m22(size) => "大小: ${size}"; static String m23(time) => "时间: ${time}"; static String m24(error) => "获取下载目录失败: ${error}"; static String m25(error) => "获取下载文件列表失败: ${error}"; static String m26(line, error) => "JSON格式错误,第${line}行:${error}"; static String m27(error) => "加载失败:${error}"; static String m28(count) => "${count} 个文件已完成,点击跳转到下载管理"; static String m29(payload) => "通知被点击: ${payload}"; static String m30(error) => "通知管理器初始化失败: ${error}"; static String m31(error) => "打开下载目录失败: ${error}"; static String m32(error) => "打开文件异常: ${error}"; static String m33(error) => "打开文件失败: ${error}"; static String m34(error) => "打开文件管理器失败: ${error}"; static String m35(type, message) => "打开文件结果: ${type} - ${message}"; static String m36(path) => "OpenList下载目录: ${path}"; static String m37(error) => "解析文件名失败: ${error}"; static String m38(error) => "恢复备份失败:${error}"; static String m39(error) => "保存失败:${error}"; static String m40(error) => "显示下载完成通知失败: ${error}"; static String m41(error) => "显示下载进度通知失败: ${error}"; static String m42(error) => "显示单个文件下载完成通知失败: ${error}"; static String m43(filename) => "开始下载: ${filename}"; static String m44(filename) => "开始下载: ${filename}"; static String m45(path) => "尝试打开文件: ${path}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("关于"), "apkDownloadCompleteMessage": MessageLookupByLibrary.simpleMessage( "APK文件已下载完成,是否要安装?", ), "appName": MessageLookupByLibrary.simpleMessage("OpenList"), "autoCheckForUpdates": MessageLookupByLibrary.simpleMessage("自动检查更新"), "autoCheckForUpdatesDesc": MessageLookupByLibrary.simpleMessage( "启动时自动检查更新", ), "autoStartIssue": MessageLookupByLibrary.simpleMessage("自启动相关说明"), "autoStartIssueDesc": MessageLookupByLibrary.simpleMessage( "设置自启动时建议把app的电池优化一并关闭,当前在开启自启动后,系统重启时服务会自动在后台启动,但可能不会在通知栏弹出通知。请放心,服务已正常运行,您可以在通知栏快捷开关查看服务状态,或回到主界面查看服务开关确认服务是否已启动。", ), "autoStartWebPage": MessageLookupByLibrary.simpleMessage("将网页设置为打开首页"), "autoStartWebPageDesc": MessageLookupByLibrary.simpleMessage("打开主界面时的首页"), "backupRestored": MessageLookupByLibrary.simpleMessage("备份已恢复"), "batchDownloadComplete": MessageLookupByLibrary.simpleMessage("批量下载完成"), "bootAutoStartService": MessageLookupByLibrary.simpleMessage("开机自启动服务"), "bootAutoStartServiceDesc": MessageLookupByLibrary.simpleMessage( "在开机后自动启动OpenList服务。(请确保授予自启动权限)", ), "browserDownload": MessageLookupByLibrary.simpleMessage("浏览器下载"), "browserDownloadMethod": MessageLookupByLibrary.simpleMessage("浏览器下载"), "browserDownloadMethodDesc": MessageLookupByLibrary.simpleMessage( "使用系统浏览器", ), "cancel": MessageLookupByLibrary.simpleMessage("取消"), "cancelAllNotificationsFailed": m0, "cancelDownload": MessageLookupByLibrary.simpleMessage("取消下载"), "cancelDownloadNotificationFailed": m1, "cancelled": MessageLookupByLibrary.simpleMessage("已取消"), "cannotGetBaseDownloadDirectory": MessageLookupByLibrary.simpleMessage( "无法获取基础下载目录", ), "cannotGetDownloadDirectory": MessageLookupByLibrary.simpleMessage( "无法获取下载目录", ), "cannotGetDownloadDirectoryError": MessageLookupByLibrary.simpleMessage( "无法获取下载目录", ), "cannotInstallApkFile": MessageLookupByLibrary.simpleMessage( "无法安装 APK 文件,可能需要在设置中开启\"允许安装未知来源应用\"", ), "cannotInstallApkNeedPermission": MessageLookupByLibrary.simpleMessage( "无法安装 APK 文件,可能需要在设置中开启\"允许安装未知来源应用\"", ), "checkDownloadManagerForFiles": MessageLookupByLibrary.simpleMessage( "请通过底部导航栏的\"下载管理\"查看下载文件", ), "checkForUpdates": MessageLookupByLibrary.simpleMessage("检查更新"), "checkImageInDownloadFolder": MessageLookupByLibrary.simpleMessage( "请在下载目录查看图片", ), "checkInstallPermissionFailed": m2, "clear": MessageLookupByLibrary.simpleMessage("清空"), "clearAll": MessageLookupByLibrary.simpleMessage("清空所有"), "clearDownloadDirectoryFailed": m3, "clearFailed": MessageLookupByLibrary.simpleMessage("清空失败"), "clearRecords": MessageLookupByLibrary.simpleMessage("清空记录"), "cleared": MessageLookupByLibrary.simpleMessage("已清空下载目录"), "clickToJumpToDownloadManager": MessageLookupByLibrary.simpleMessage( "点击跳转到下载管理", ), "completed": MessageLookupByLibrary.simpleMessage("已完成"), "completedTime": MessageLookupByLibrary.simpleMessage("完成时间"), "configSavedRestartRequired": MessageLookupByLibrary.simpleMessage( "配置已保存,请重启OpenList服务以生效", ), "confirm": MessageLookupByLibrary.simpleMessage("确认"), "confirmCancelDownload": m4, "confirmClear": MessageLookupByLibrary.simpleMessage("确认清空"), "confirmClearAllFiles": MessageLookupByLibrary.simpleMessage( "确定要清空所有下载文件吗?此操作不可撤销。", ), "confirmDelete": MessageLookupByLibrary.simpleMessage("确认删除"), "confirmDeleteFile": m5, "confirmDeleteRecord": m6, "confirmDownload": MessageLookupByLibrary.simpleMessage("确认下载"), "confirmDownloadMessage": MessageLookupByLibrary.simpleMessage( "是否要下载这个文件?", ), "confirmSaveConfigMessage": MessageLookupByLibrary.simpleMessage( "修改配置可能导致服务不可用,确定保存吗?", ), "confirmSaveConfigTitle": MessageLookupByLibrary.simpleMessage("确认保存"), "continueDownload": MessageLookupByLibrary.simpleMessage("继续下载"), "copiedToClipboard": MessageLookupByLibrary.simpleMessage("已复制到剪贴板"), "createOpenListDirectoryFailed": m7, "createOpenListDownloadDirectory": m8, "currentDownloadingFiles": m9, "currentIsLatestVersion": MessageLookupByLibrary.simpleMessage("已经是最新版本"), "currentlyDownloading": MessageLookupByLibrary.simpleMessage("正在下载"), "dataDirectory": MessageLookupByLibrary.simpleMessage("data 文件夹路径"), "databaseNotSavedIssue": MessageLookupByLibrary.simpleMessage("数据库未保存问题"), "databaseNotSavedIssueDesc": MessageLookupByLibrary.simpleMessage( "如不手动关闭OpenList,则数据库可能不会被保存到对应的db文件中,如遇到此问题,请手动关闭以解决此问题。(开关位于主程序菜单OpenList界面,以及通知栏的通知上)", ), "delete": MessageLookupByLibrary.simpleMessage("删除"), "deleteFailed": MessageLookupByLibrary.simpleMessage("删除失败"), "deleteFile": MessageLookupByLibrary.simpleMessage("删除文件"), "deleteFileFailedLog": m10, "deleteRecord": MessageLookupByLibrary.simpleMessage("删除记录"), "description": MessageLookupByLibrary.simpleMessage("说明:"), "desktopShortcut": MessageLookupByLibrary.simpleMessage("桌面快捷方式"), "directDownload": MessageLookupByLibrary.simpleMessage("直接下载"), "directDownloadApk": MessageLookupByLibrary.simpleMessage("直接下载APK"), "directDownloadMethod": MessageLookupByLibrary.simpleMessage("直接下载"), "directDownloadMethodDesc": MessageLookupByLibrary.simpleMessage( "使用应用内下载器", ), "download": MessageLookupByLibrary.simpleMessage("下载"), "downloadApk": MessageLookupByLibrary.simpleMessage("下载APK"), "downloadCancelled": m11, "downloadCancelledStatus": MessageLookupByLibrary.simpleMessage("下载已取消"), "downloadCancelledText": MessageLookupByLibrary.simpleMessage("下载已取消"), "downloadComplete": m12, "downloadCompleteChannel": MessageLookupByLibrary.simpleMessage("下载完成"), "downloadCompleteChannelDesc": MessageLookupByLibrary.simpleMessage( "文件下载完成通知", ), "downloadCompleteFile": m13, "downloadCompleteNotificationTitle": m14, "downloadCompleteTitle": MessageLookupByLibrary.simpleMessage("下载完成"), "downloadDirectory": MessageLookupByLibrary.simpleMessage("下载目录"), "downloadDirectoryCleared": MessageLookupByLibrary.simpleMessage("已清理下载目录"), "downloadDirectoryOpened": MessageLookupByLibrary.simpleMessage("已打开下载目录"), "downloadDirectoryPathUnknown": MessageLookupByLibrary.simpleMessage( "下载目录路径未知", ), "downloadFailed": MessageLookupByLibrary.simpleMessage("下载失败"), "downloadFailedFile": m15, "downloadFailedWithError": m16, "downloadFunctionTest": MessageLookupByLibrary.simpleMessage("下载功能测试"), "downloadInstructions": MessageLookupByLibrary.simpleMessage( "• 文件将下载到系统下载目录\\n• 下载过程会显示进度通知\\n• 下载完成后可以选择打开文件\\n• 如果文件名重复会自动添加序号\\n• 请通过底部导航栏的\\\"下载管理\\\"查看下载文件", ), "downloadManager": MessageLookupByLibrary.simpleMessage("下载管理"), "downloadManagerWithCount": m17, "downloadProgress": m18, "downloadProgressChannel": MessageLookupByLibrary.simpleMessage("下载进度"), "downloadProgressDesc": MessageLookupByLibrary.simpleMessage("显示文件下载进度"), "downloadRecordsCleared": MessageLookupByLibrary.simpleMessage("已清空下载记录"), "downloadThisFile": MessageLookupByLibrary.simpleMessage("下载此文件吗?"), "downloading": MessageLookupByLibrary.simpleMessage("下载中"), "downloadingFileProgress": m19, "downloadingImage": MessageLookupByLibrary.simpleMessage("正在下载图片..."), "edit": MessageLookupByLibrary.simpleMessage("编辑"), "editOpenListConfig": MessageLookupByLibrary.simpleMessage( "修改OpenList配置文件", ), "english": MessageLookupByLibrary.simpleMessage("English"), "failed": MessageLookupByLibrary.simpleMessage("失败"), "fileDeleted": MessageLookupByLibrary.simpleMessage("文件已删除"), "fileDeletedLog": m20, "fileDownloadFailed": m21, "fileInfo": MessageLookupByLibrary.simpleMessage("文件信息"), "fileLocation": MessageLookupByLibrary.simpleMessage("文件位置"), "fileLocationTip": MessageLookupByLibrary.simpleMessage( "您可以使用文件管理器找到此文件,或者尝试安装相应的应用来打开它。", ), "fileManagerOpened": MessageLookupByLibrary.simpleMessage("已打开文件管理器"), "fileName": MessageLookupByLibrary.simpleMessage("文件名"), "fileNotFound": MessageLookupByLibrary.simpleMessage("文件不存在或已被删除"), "fileNotFoundWillCreateOnSave": MessageLookupByLibrary.simpleMessage( "文件不存在,保存时将创建", ), "filePath": MessageLookupByLibrary.simpleMessage("路径"), "filePermissionDenied": MessageLookupByLibrary.simpleMessage( "文件权限被拒绝,请检查应用权限", ), "fileSavedTo": MessageLookupByLibrary.simpleMessage("文件已保存到:"), "fileSize": m22, "fileTime": m23, "findApkInDownloadFolder": MessageLookupByLibrary.simpleMessage( "请在下载目录找到APK文件进行安装", ), "followSystem": MessageLookupByLibrary.simpleMessage("跟随系统"), "general": MessageLookupByLibrary.simpleMessage("通用"), "getDownloadDirectoryFailed": m24, "getDownloadFileListFailed": m25, "getDownloadPathFailed": MessageLookupByLibrary.simpleMessage("获取失败"), "goTo": MessageLookupByLibrary.simpleMessage("前往"), "goToSettings": MessageLookupByLibrary.simpleMessage("去设置"), "grantManagerStoragePermission": MessageLookupByLibrary.simpleMessage( "申请【所有文件访问权限】", ), "grantNotificationPermission": MessageLookupByLibrary.simpleMessage( "申请【通知权限】", ), "grantNotificationPermissionDesc": MessageLookupByLibrary.simpleMessage( "用于前台服务保活", ), "grantStoragePermission": MessageLookupByLibrary.simpleMessage( "申请【读写外置存储权限】", ), "grantStoragePermissionDesc": MessageLookupByLibrary.simpleMessage( "挂载本地存储时必须授予,否则无权限读写文件", ), "imageDownloadSuccess": MessageLookupByLibrary.simpleMessage("图片下载成功"), "importantSettings": MessageLookupByLibrary.simpleMessage("重要"), "inProgress": MessageLookupByLibrary.simpleMessage("进行中"), "initializingNotificationManager": MessageLookupByLibrary.simpleMessage( "初始化通知管理器", ), "installNow": MessageLookupByLibrary.simpleMessage("立即安装"), "invalidJsonFormat": m26, "jumpToOtherApp": MessageLookupByLibrary.simpleMessage("跳转到其他APP ?"), "language": MessageLookupByLibrary.simpleMessage("语言"), "languageSettings": MessageLookupByLibrary.simpleMessage("语言设置"), "languageSettingsDesc": MessageLookupByLibrary.simpleMessage("选择应用显示语言"), "laterInstall": MessageLookupByLibrary.simpleMessage("稍后安装"), "loadDownloadFilesFailed": MessageLookupByLibrary.simpleMessage("加载下载文件失败"), "loadFailed": m27, "modifiedTime": MessageLookupByLibrary.simpleMessage("修改时间"), "modifyAdminPassword": MessageLookupByLibrary.simpleMessage("修改Admin密码"), "moreOptions": MessageLookupByLibrary.simpleMessage("更多选项"), "multipleFilesCompleted": m28, "needInstallPermission": MessageLookupByLibrary.simpleMessage("需要安装权限"), "needInstallPermissionDesc": MessageLookupByLibrary.simpleMessage( "为了安装 APK 文件,需要授予安装权限。请在设置中手动开启。", ), "needInstallPermissionToInstallApk": MessageLookupByLibrary.simpleMessage( "需要安装权限才能安装 APK 文件", ), "newVersionFound": MessageLookupByLibrary.simpleMessage("发现新版本"), "noActiveDownloads": MessageLookupByLibrary.simpleMessage("暂无进行中的下载"), "noAppToOpenFile": MessageLookupByLibrary.simpleMessage("没有找到可以打开此文件的应用"), "noBackupFound": MessageLookupByLibrary.simpleMessage("未找到备份文件"), "noCompletedDownloads": MessageLookupByLibrary.simpleMessage("暂无已完成的下载"), "noPermissionToInstallApk": MessageLookupByLibrary.simpleMessage( "没有权限安装 APK 文件,请在设置中开启安装权限", ), "noPermissionToInstallApkFile": MessageLookupByLibrary.simpleMessage( "没有权限安装 APK 文件,请在设置中开启安装权限", ), "noPermissionToOpenFile": MessageLookupByLibrary.simpleMessage("没有权限打开此文件"), "notificationClicked": m29, "notificationManagerInitFailed": m30, "notificationManagerInitialized": MessageLookupByLibrary.simpleMessage( "通知管理器初始化成功", ), "ok": MessageLookupByLibrary.simpleMessage("确定"), "open": MessageLookupByLibrary.simpleMessage("打开"), "openDirectory": MessageLookupByLibrary.simpleMessage("打开目录"), "openDownloadDirectoryFailed": m31, "openDownloadManager": MessageLookupByLibrary.simpleMessage("打开下载管理"), "openDownloadTestPage": MessageLookupByLibrary.simpleMessage( "是否要打开下载测试页面?", ), "openFile": MessageLookupByLibrary.simpleMessage("打开文件"), "openFileException": m32, "openFileFailed": m33, "openFileManager": MessageLookupByLibrary.simpleMessage("打开文件管理器"), "openFileManagerFailed": m34, "openFileResult": m35, "openListDownloadDirectory": m36, "openSourceLicenses": MessageLookupByLibrary.simpleMessage("开源许可证"), "openlist": MessageLookupByLibrary.simpleMessage("OpenList"), "openlistMobile": MessageLookupByLibrary.simpleMessage("OpenList Mobile"), "parseFilenameFailed": m37, "pending": MessageLookupByLibrary.simpleMessage("等待中"), "preparingDownload": MessageLookupByLibrary.simpleMessage("准备下载..."), "preparingDownloadStatus": MessageLookupByLibrary.simpleMessage("准备下载..."), "preview": MessageLookupByLibrary.simpleMessage("预览"), "refresh": MessageLookupByLibrary.simpleMessage("刷新"), "releasePage": MessageLookupByLibrary.simpleMessage("发布页面"), "restartingService": MessageLookupByLibrary.simpleMessage( "正在重启OpenList服务...", ), "restoreBackup": MessageLookupByLibrary.simpleMessage("恢复备份"), "restoreBackupFailed": m38, "save": MessageLookupByLibrary.simpleMessage("保存"), "saveAndRestart": MessageLookupByLibrary.simpleMessage("保存并重启"), "saveFailed": m39, "saveOnly": MessageLookupByLibrary.simpleMessage("仅保存"), "saved": MessageLookupByLibrary.simpleMessage("已保存"), "selectAppToOpen": MessageLookupByLibrary.simpleMessage("选择应用打开"), "selectDownloadMethod": MessageLookupByLibrary.simpleMessage("选择下载方式"), "serviceRestartFailed": MessageLookupByLibrary.simpleMessage( "服务重启失败,请手动重启", ), "serviceRestartOnlyAndroid": MessageLookupByLibrary.simpleMessage( "服务重启仅支持Android系统", ), "serviceRestartSuccess": MessageLookupByLibrary.simpleMessage("服务重启成功"), "setAdminPassword": MessageLookupByLibrary.simpleMessage("设置Admin密码"), "setDefaultDirectory": MessageLookupByLibrary.simpleMessage("是否设为初始目录?"), "settings": MessageLookupByLibrary.simpleMessage("设置"), "shareFeatureNotImplemented": MessageLookupByLibrary.simpleMessage( "分享功能待实现", ), "shareFile": MessageLookupByLibrary.simpleMessage("分享文件"), "shareLink": MessageLookupByLibrary.simpleMessage("分享链接"), "shareLinkDesc": MessageLookupByLibrary.simpleMessage("分享下载链接"), "showDownloadCompleteNotificationFailed": m40, "showDownloadProgressNotificationFailed": m41, "showInFileManager": MessageLookupByLibrary.simpleMessage("在文件管理器中显示"), "showSingleFileCompleteNotificationFailed": m42, "silentJumpApp": MessageLookupByLibrary.simpleMessage("静默跳转APP"), "silentJumpAppDesc": MessageLookupByLibrary.simpleMessage("跳转APP时,不弹出提示框"), "simplifiedChinese": MessageLookupByLibrary.simpleMessage("简体中文"), "size": MessageLookupByLibrary.simpleMessage("大小"), "startDownload": m43, "startDownloadFile": m44, "startTime": MessageLookupByLibrary.simpleMessage("开始时间"), "testDirectDownloadFunction": MessageLookupByLibrary.simpleMessage( "测试直接下载功能", ), "testDownloadJsonFile": MessageLookupByLibrary.simpleMessage("测试下载JSON文件"), "testDownloadLargeFile": MessageLookupByLibrary.simpleMessage( "测试下载大文件(1MB)", ), "testDownloadPngImage": MessageLookupByLibrary.simpleMessage("测试下载PNG图片"), "troubleshooting": MessageLookupByLibrary.simpleMessage("疑难解答"), "troubleshootingDesc": MessageLookupByLibrary.simpleMessage("常见问题与解决方案"), "tryToOpenFile": m45, "uiSettings": MessageLookupByLibrary.simpleMessage("界面"), "userCancelledDownload": MessageLookupByLibrary.simpleMessage("用户取消下载"), "userCancelledDownloadError": MessageLookupByLibrary.simpleMessage( "用户取消下载", ), "view": MessageLookupByLibrary.simpleMessage("查看"), "viewDownloadDirectory": MessageLookupByLibrary.simpleMessage("查看下载目录"), "viewDownloadFiles": MessageLookupByLibrary.simpleMessage("查看下载文件"), "viewDownloads": MessageLookupByLibrary.simpleMessage("查看下载"), "viewLocation": MessageLookupByLibrary.simpleMessage("查看位置"), "viewThirdPartyLicenses": MessageLookupByLibrary.simpleMessage("查看第三方许可证"), "wakeLock": MessageLookupByLibrary.simpleMessage("唤醒锁"), "wakeLockDesc": MessageLookupByLibrary.simpleMessage( "开启防止锁屏后CPU休眠,保持进程在后台运行。(部分系统可能导致杀后台)", ), "webPage": MessageLookupByLibrary.simpleMessage("网页"), }; } ================================================ FILE: lib/generated/l10n.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'intl/messages_all.dart'; // ************************************************************************** // Generator: Flutter Intl IDE plugin // Made by Localizely // ************************************************************************** // ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars // ignore_for_file: join_return_with_assignment, prefer_final_in_for_each // ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes class S { S(); static S? _current; static S get current { assert( _current != null, 'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.', ); return _current!; } static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future load(Locale locale) { final name = (locale.countryCode?.isEmpty ?? false) ? locale.languageCode : locale.toString(); final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; final instance = S(); S._current = instance; return instance; }); } static S of(BuildContext context) { final instance = S.maybeOf(context); assert( instance != null, 'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?', ); return instance!; } static S? maybeOf(BuildContext context) { return Localizations.of(context, S); } /// `OpenList` String get appName { return Intl.message('OpenList', name: 'appName', desc: '', args: []); } /// `桌面快捷方式` String get desktopShortcut { return Intl.message('桌面快捷方式', name: 'desktopShortcut', desc: '', args: []); } /// `设置Admin密码` String get setAdminPassword { return Intl.message( '设置Admin密码', name: 'setAdminPassword', desc: '', args: [], ); } /// `更多选项` String get moreOptions { return Intl.message('更多选项', name: 'moreOptions', desc: '', args: []); } /// `检查更新` String get checkForUpdates { return Intl.message('检查更新', name: 'checkForUpdates', desc: '', args: []); } /// `已经是最新版本` String get currentIsLatestVersion { return Intl.message( '已经是最新版本', name: 'currentIsLatestVersion', desc: '', args: [], ); } /// `确认` String get confirm { return Intl.message('确认', name: 'confirm', desc: '', args: []); } /// `取消` String get cancel { return Intl.message('取消', name: 'cancel', desc: '', args: []); } /// `发布页面` String get releasePage { return Intl.message('发布页面', name: 'releasePage', desc: '', args: []); } /// `下载APK` String get downloadApk { return Intl.message('下载APK', name: 'downloadApk', desc: '', args: []); } /// `关于` String get about { return Intl.message('关于', name: 'about', desc: '', args: []); } /// `通用` String get general { return Intl.message('通用', name: 'general', desc: '', args: []); } /// `自动检查更新` String get autoCheckForUpdates { return Intl.message( '自动检查更新', name: 'autoCheckForUpdates', desc: '', args: [], ); } /// `启动时自动检查更新` String get autoCheckForUpdatesDesc { return Intl.message( '启动时自动检查更新', name: 'autoCheckForUpdatesDesc', desc: '', args: [], ); } /// `唤醒锁` String get wakeLock { return Intl.message('唤醒锁', name: 'wakeLock', desc: '', args: []); } /// `开启防止锁屏后CPU休眠,保持进程在后台运行。(部分系统可能导致杀后台)` String get wakeLockDesc { return Intl.message( '开启防止锁屏后CPU休眠,保持进程在后台运行。(部分系统可能导致杀后台)', name: 'wakeLockDesc', desc: '', args: [], ); } /// `开机自启动服务` String get bootAutoStartService { return Intl.message( '开机自启动服务', name: 'bootAutoStartService', desc: '', args: [], ); } /// `在开机后自动启动OpenList服务。(请确保授予自启动权限)` String get bootAutoStartServiceDesc { return Intl.message( '在开机后自动启动OpenList服务。(请确保授予自启动权限)', name: 'bootAutoStartServiceDesc', desc: '', args: [], ); } /// `网页` String get webPage { return Intl.message('网页', name: 'webPage', desc: '', args: []); } /// `设置` String get settings { return Intl.message('设置', name: 'settings', desc: '', args: []); } /// `选择应用打开` String get selectAppToOpen { return Intl.message('选择应用打开', name: 'selectAppToOpen', desc: '', args: []); } /// `前往` String get goTo { return Intl.message('前往', name: 'goTo', desc: '', args: []); } /// `下载此文件吗?` String get downloadThisFile { return Intl.message( '下载此文件吗?', name: 'downloadThisFile', desc: '', args: [], ); } /// `下载` String get download { return Intl.message('下载', name: 'download', desc: '', args: []); } /// `已复制到剪贴板` String get copiedToClipboard { return Intl.message( '已复制到剪贴板', name: 'copiedToClipboard', desc: '', args: [], ); } /// `重要` String get importantSettings { return Intl.message('重要', name: 'importantSettings', desc: '', args: []); } /// `界面` String get uiSettings { return Intl.message('界面', name: 'uiSettings', desc: '', args: []); } /// `申请【所有文件访问权限】` String get grantManagerStoragePermission { return Intl.message( '申请【所有文件访问权限】', name: 'grantManagerStoragePermission', desc: '', args: [], ); } /// `挂载本地存储时必须授予,否则无权限读写文件` String get grantStoragePermissionDesc { return Intl.message( '挂载本地存储时必须授予,否则无权限读写文件', name: 'grantStoragePermissionDesc', desc: '', args: [], ); } /// `申请【读写外置存储权限】` String get grantStoragePermission { return Intl.message( '申请【读写外置存储权限】', name: 'grantStoragePermission', desc: '', args: [], ); } /// `申请【通知权限】` String get grantNotificationPermission { return Intl.message( '申请【通知权限】', name: 'grantNotificationPermission', desc: '', args: [], ); } /// `用于前台服务保活` String get grantNotificationPermissionDesc { return Intl.message( '用于前台服务保活', name: 'grantNotificationPermissionDesc', desc: '', args: [], ); } /// `将网页设置为打开首页` String get autoStartWebPage { return Intl.message( '将网页设置为打开首页', name: 'autoStartWebPage', desc: '', args: [], ); } /// `跳转到其他APP ?` String get jumpToOtherApp { return Intl.message( '跳转到其他APP ?', name: 'jumpToOtherApp', desc: '', args: [], ); } /// `打开主界面时的首页` String get autoStartWebPageDesc { return Intl.message( '打开主界面时的首页', name: 'autoStartWebPageDesc', desc: '', args: [], ); } /// `data 文件夹路径` String get dataDirectory { return Intl.message( 'data 文件夹路径', name: 'dataDirectory', desc: '', args: [], ); } /// `是否设为初始目录?` String get setDefaultDirectory { return Intl.message( '是否设为初始目录?', name: 'setDefaultDirectory', desc: '', args: [], ); } /// `静默跳转APP` String get silentJumpApp { return Intl.message('静默跳转APP', name: 'silentJumpApp', desc: '', args: []); } /// `跳转APP时,不弹出提示框` String get silentJumpAppDesc { return Intl.message( '跳转APP时,不弹出提示框', name: 'silentJumpAppDesc', desc: '', args: [], ); } /// `发现新版本` String get newVersionFound { return Intl.message('发现新版本', name: 'newVersionFound', desc: '', args: []); } /// `直接下载APK` String get directDownloadApk { return Intl.message( '直接下载APK', name: 'directDownloadApk', desc: '', args: [], ); } /// `初始化通知管理器` String get initializingNotificationManager { return Intl.message( '初始化通知管理器', name: 'initializingNotificationManager', desc: '', args: [], ); } /// `下载管理` String get downloadManager { return Intl.message('下载管理', name: 'downloadManager', desc: '', args: []); } /// `下载管理({count})` String downloadManagerWithCount(int count) { return Intl.message( '下载管理($count)', name: 'downloadManagerWithCount', desc: '', args: [count], ); } /// `修改Admin密码` String get modifyAdminPassword { return Intl.message( '修改Admin密码', name: 'modifyAdminPassword', desc: '', args: [], ); } /// `直接下载` String get directDownload { return Intl.message('直接下载', name: 'directDownload', desc: '', args: []); } /// `浏览器下载` String get browserDownload { return Intl.message('浏览器下载', name: 'browserDownload', desc: '', args: []); } /// `加载下载文件失败` String get loadDownloadFilesFailed { return Intl.message( '加载下载文件失败', name: 'loadDownloadFilesFailed', desc: '', args: [], ); } /// `暂无进行中的下载` String get noActiveDownloads { return Intl.message( '暂无进行中的下载', name: 'noActiveDownloads', desc: '', args: [], ); } /// `下载失败` String get downloadFailed { return Intl.message('下载失败', name: 'downloadFailed', desc: '', args: []); } /// `开始时间` String get startTime { return Intl.message('开始时间', name: 'startTime', desc: '', args: []); } /// `暂无已完成的下载` String get noCompletedDownloads { return Intl.message( '暂无已完成的下载', name: 'noCompletedDownloads', desc: '', args: [], ); } /// `完成时间` String get completedTime { return Intl.message('完成时间', name: 'completedTime', desc: '', args: []); } /// `大小` String get size { return Intl.message('大小', name: 'size', desc: '', args: []); } /// `打开文件` String get openFile { return Intl.message('打开文件', name: 'openFile', desc: '', args: []); } /// `删除记录` String get deleteRecord { return Intl.message('删除记录', name: 'deleteRecord', desc: '', args: []); } /// `删除文件` String get deleteFile { return Intl.message('删除文件', name: 'deleteFile', desc: '', args: []); } /// `确认清空` String get confirmClear { return Intl.message('确认清空', name: 'confirmClear', desc: '', args: []); } /// `确定要清空所有下载文件吗?此操作不可撤销。` String get confirmClearAllFiles { return Intl.message( '确定要清空所有下载文件吗?此操作不可撤销。', name: 'confirmClearAllFiles', desc: '', args: [], ); } /// `已清空下载目录` String get cleared { return Intl.message('已清空下载目录', name: 'cleared', desc: '', args: []); } /// `清空失败` String get clearFailed { return Intl.message('清空失败', name: 'clearFailed', desc: '', args: []); } /// `取消下载` String get cancelDownload { return Intl.message('取消下载', name: 'cancelDownload', desc: '', args: []); } /// `确定要取消下载 "{filename}" 吗?` String confirmCancelDownload(String filename) { return Intl.message( '确定要取消下载 "$filename" 吗?', name: 'confirmCancelDownload', desc: '', args: [filename], ); } /// `继续下载` String get continueDownload { return Intl.message('继续下载', name: 'continueDownload', desc: '', args: []); } /// `确定要删除 "{filename}" 的下载记录吗?` String confirmDeleteRecord(String filename) { return Intl.message( '确定要删除 "$filename" 的下载记录吗?', name: 'confirmDeleteRecord', desc: '', args: [filename], ); } /// `删除` String get delete { return Intl.message('删除', name: 'delete', desc: '', args: []); } /// `分享文件` String get shareFile { return Intl.message('分享文件', name: 'shareFile', desc: '', args: []); } /// `分享功能待实现` String get shareFeatureNotImplemented { return Intl.message( '分享功能待实现', name: 'shareFeatureNotImplemented', desc: '', args: [], ); } /// `文件信息` String get fileInfo { return Intl.message('文件信息', name: 'fileInfo', desc: '', args: []); } /// `文件名` String get fileName { return Intl.message('文件名', name: 'fileName', desc: '', args: []); } /// `修改时间` String get modifiedTime { return Intl.message('修改时间', name: 'modifiedTime', desc: '', args: []); } /// `路径` String get filePath { return Intl.message('路径', name: 'filePath', desc: '', args: []); } /// `确认删除` String get confirmDelete { return Intl.message('确认删除', name: 'confirmDelete', desc: '', args: []); } /// `确定要删除文件 "{filename}" 吗?此操作不可撤销。` String confirmDeleteFile(String filename) { return Intl.message( '确定要删除文件 "$filename" 吗?此操作不可撤销。', name: 'confirmDeleteFile', desc: '', args: [filename], ); } /// `文件已删除` String get fileDeleted { return Intl.message('文件已删除', name: 'fileDeleted', desc: '', args: []); } /// `删除失败` String get deleteFailed { return Intl.message('删除失败', name: 'deleteFailed', desc: '', args: []); } /// `清空` String get clear { return Intl.message('清空', name: 'clear', desc: '', args: []); } /// `没有找到可以打开此文件的应用` String get noAppToOpenFile { return Intl.message( '没有找到可以打开此文件的应用', name: 'noAppToOpenFile', desc: '', args: [], ); } /// `查看位置` String get viewLocation { return Intl.message('查看位置', name: 'viewLocation', desc: '', args: []); } /// `文件不存在或已被删除` String get fileNotFound { return Intl.message('文件不存在或已被删除', name: 'fileNotFound', desc: '', args: []); } /// `没有权限打开此文件` String get noPermissionToOpenFile { return Intl.message( '没有权限打开此文件', name: 'noPermissionToOpenFile', desc: '', args: [], ); } /// `打开文件失败: {error}` String openFileFailed(String error) { return Intl.message( '打开文件失败: $error', name: 'openFileFailed', desc: '', args: [error], ); } /// `文件位置` String get fileLocation { return Intl.message('文件位置', name: 'fileLocation', desc: '', args: []); } /// `文件已保存到:` String get fileSavedTo { return Intl.message('文件已保存到:', name: 'fileSavedTo', desc: '', args: []); } /// `您可以使用文件管理器找到此文件,或者尝试安装相应的应用来打开它。` String get fileLocationTip { return Intl.message( '您可以使用文件管理器找到此文件,或者尝试安装相应的应用来打开它。', name: 'fileLocationTip', desc: '', args: [], ); } /// `下载目录` String get downloadDirectory { return Intl.message('下载目录', name: 'downloadDirectory', desc: '', args: []); } /// `打开目录` String get openDirectory { return Intl.message('打开目录', name: 'openDirectory', desc: '', args: []); } /// `清空记录` String get clearRecords { return Intl.message('清空记录', name: 'clearRecords', desc: '', args: []); } /// `清空所有` String get clearAll { return Intl.message('清空所有', name: 'clearAll', desc: '', args: []); } /// `已清空下载记录` String get downloadRecordsCleared { return Intl.message( '已清空下载记录', name: 'downloadRecordsCleared', desc: '', args: [], ); } /// `进行中` String get inProgress { return Intl.message('进行中', name: 'inProgress', desc: '', args: []); } /// `已完成` String get completed { return Intl.message('已完成', name: 'completed', desc: '', args: []); } /// `刷新` String get refresh { return Intl.message('刷新', name: 'refresh', desc: '', args: []); } /// `等待中` String get pending { return Intl.message('等待中', name: 'pending', desc: '', args: []); } /// `下载中` String get downloading { return Intl.message('下载中', name: 'downloading', desc: '', args: []); } /// `失败` String get failed { return Intl.message('失败', name: 'failed', desc: '', args: []); } /// `已取消` String get cancelled { return Intl.message('已取消', name: 'cancelled', desc: '', args: []); } /// `无法获取下载目录` String get cannotGetDownloadDirectory { return Intl.message( '无法获取下载目录', name: 'cannotGetDownloadDirectory', desc: '', args: [], ); } /// `开始下载: {filename}` String startDownload(String filename) { return Intl.message( '开始下载: $filename', name: 'startDownload', desc: '', args: [filename], ); } /// `下载进度: {progress}%` String downloadProgress(String progress) { return Intl.message( '下载进度: $progress%', name: 'downloadProgress', desc: '', args: [progress], ); } /// `下载完成: {filename}` String downloadComplete(String filename) { return Intl.message( '下载完成: $filename', name: 'downloadComplete', desc: '', args: [filename], ); } /// `打开` String get open { return Intl.message('打开', name: 'open', desc: '', args: []); } /// `下载已取消: {url}` String downloadCancelled(String url) { return Intl.message( '下载已取消: $url', name: 'downloadCancelled', desc: '', args: [url], ); } /// `下载失败: {filename}` String downloadFailedWithError(String filename) { return Intl.message( '下载失败: $filename', name: 'downloadFailedWithError', desc: '', args: [filename], ); } /// `用户取消下载` String get userCancelledDownload { return Intl.message( '用户取消下载', name: 'userCancelledDownload', desc: '', args: [], ); } /// `无法获取基础下载目录` String get cannotGetBaseDownloadDirectory { return Intl.message( '无法获取基础下载目录', name: 'cannotGetBaseDownloadDirectory', desc: '', args: [], ); } /// `创建OpenList下载目录: {path}` String createOpenListDownloadDirectory(String path) { return Intl.message( '创建OpenList下载目录: $path', name: 'createOpenListDownloadDirectory', desc: '', args: [path], ); } /// `创建OpenList目录失败: {error}` String createOpenListDirectoryFailed(String error) { return Intl.message( '创建OpenList目录失败: $error', name: 'createOpenListDirectoryFailed', desc: '', args: [error], ); } /// `OpenList下载目录: {path}` String openListDownloadDirectory(String path) { return Intl.message( 'OpenList下载目录: $path', name: 'openListDownloadDirectory', desc: '', args: [path], ); } /// `获取下载目录失败: {error}` String getDownloadDirectoryFailed(String error) { return Intl.message( '获取下载目录失败: $error', name: 'getDownloadDirectoryFailed', desc: '', args: [error], ); } /// `解析文件名失败: {error}` String parseFilenameFailed(String error) { return Intl.message( '解析文件名失败: $error', name: 'parseFilenameFailed', desc: '', args: [error], ); } /// `需要安装权限` String get needInstallPermission { return Intl.message( '需要安装权限', name: 'needInstallPermission', desc: '', args: [], ); } /// `为了安装 APK 文件,需要授予安装权限。请在设置中手动开启。` String get needInstallPermissionDesc { return Intl.message( '为了安装 APK 文件,需要授予安装权限。请在设置中手动开启。', name: 'needInstallPermissionDesc', desc: '', args: [], ); } /// `去设置` String get goToSettings { return Intl.message('去设置', name: 'goToSettings', desc: '', args: []); } /// `需要安装权限才能安装 APK 文件` String get needInstallPermissionToInstallApk { return Intl.message( '需要安装权限才能安装 APK 文件', name: 'needInstallPermissionToInstallApk', desc: '', args: [], ); } /// `检查安装权限失败: {error}` String checkInstallPermissionFailed(String error) { return Intl.message( '检查安装权限失败: $error', name: 'checkInstallPermissionFailed', desc: '', args: [error], ); } /// `尝试打开文件: {path}` String tryToOpenFile(String path) { return Intl.message( '尝试打开文件: $path', name: 'tryToOpenFile', desc: '', args: [path], ); } /// `打开文件结果: {type} - {message}` String openFileResult(String type, String message) { return Intl.message( '打开文件结果: $type - $message', name: 'openFileResult', desc: '', args: [type, message], ); } /// `无法安装 APK 文件,可能需要在设置中开启"允许安装未知来源应用"` String get cannotInstallApkFile { return Intl.message( '无法安装 APK 文件,可能需要在设置中开启"允许安装未知来源应用"', name: 'cannotInstallApkFile', desc: '', args: [], ); } /// `没有权限安装 APK 文件,请在设置中开启安装权限` String get noPermissionToInstallApk { return Intl.message( '没有权限安装 APK 文件,请在设置中开启安装权限', name: 'noPermissionToInstallApk', desc: '', args: [], ); } /// `打开文件异常: {error}` String openFileException(String error) { return Intl.message( '打开文件异常: $error', name: 'openFileException', desc: '', args: [error], ); } /// `获取下载文件列表失败: {error}` String getDownloadFileListFailed(String error) { return Intl.message( '获取下载文件列表失败: $error', name: 'getDownloadFileListFailed', desc: '', args: [error], ); } /// `已清理下载目录` String get downloadDirectoryCleared { return Intl.message( '已清理下载目录', name: 'downloadDirectoryCleared', desc: '', args: [], ); } /// `清理下载目录失败: {error}` String clearDownloadDirectoryFailed(String error) { return Intl.message( '清理下载目录失败: $error', name: 'clearDownloadDirectoryFailed', desc: '', args: [error], ); } /// `已删除文件: {filename}` String fileDeletedLog(String filename) { return Intl.message( '已删除文件: $filename', name: 'fileDeletedLog', desc: '', args: [filename], ); } /// `删除文件失败: {error}` String deleteFileFailedLog(String error) { return Intl.message( '删除文件失败: $error', name: 'deleteFileFailedLog', desc: '', args: [error], ); } /// `准备下载...` String get preparingDownload { return Intl.message( '准备下载...', name: 'preparingDownload', desc: '', args: [], ); } /// `下载已取消` String get downloadCancelledStatus { return Intl.message( '下载已取消', name: 'downloadCancelledStatus', desc: '', args: [], ); } /// `通知管理器初始化成功` String get notificationManagerInitialized { return Intl.message( '通知管理器初始化成功', name: 'notificationManagerInitialized', desc: '', args: [], ); } /// `通知管理器初始化失败: {error}` String notificationManagerInitFailed(String error) { return Intl.message( '通知管理器初始化失败: $error', name: 'notificationManagerInitFailed', desc: '', args: [error], ); } /// `通知被点击: {payload}` String notificationClicked(String payload) { return Intl.message( '通知被点击: $payload', name: 'notificationClicked', desc: '', args: [payload], ); } /// `当前有 {count} 个文件在下载` String currentDownloadingFiles(int count) { return Intl.message( '当前有 $count 个文件在下载', name: 'currentDownloadingFiles', desc: '', args: [count], ); } /// `显示文件下载进度` String get downloadProgressDesc { return Intl.message( '显示文件下载进度', name: 'downloadProgressDesc', desc: '', args: [], ); } /// `查看下载` String get viewDownloads { return Intl.message('查看下载', name: 'viewDownloads', desc: '', args: []); } /// `显示下载进度通知失败: {error}` String showDownloadProgressNotificationFailed(String error) { return Intl.message( '显示下载进度通知失败: $error', name: 'showDownloadProgressNotificationFailed', desc: '', args: [error], ); } /// `{filename} 下载完毕` String downloadCompleteNotificationTitle(String filename) { return Intl.message( '$filename 下载完毕', name: 'downloadCompleteNotificationTitle', desc: '', args: [filename], ); } /// `点击跳转到下载管理` String get clickToJumpToDownloadManager { return Intl.message( '点击跳转到下载管理', name: 'clickToJumpToDownloadManager', desc: '', args: [], ); } /// `下载完成` String get downloadCompleteTitle { return Intl.message( '下载完成', name: 'downloadCompleteTitle', desc: '', args: [], ); } /// `{count} 个文件已完成,点击跳转到下载管理` String multipleFilesCompleted(int count) { return Intl.message( '$count 个文件已完成,点击跳转到下载管理', name: 'multipleFilesCompleted', desc: '', args: [count], ); } /// `下载完成` String get downloadCompleteChannel { return Intl.message( '下载完成', name: 'downloadCompleteChannel', desc: '', args: [], ); } /// `文件下载完成通知` String get downloadCompleteChannelDesc { return Intl.message( '文件下载完成通知', name: 'downloadCompleteChannelDesc', desc: '', args: [], ); } /// `打开下载管理` String get openDownloadManager { return Intl.message( '打开下载管理', name: 'openDownloadManager', desc: '', args: [], ); } /// `显示下载完成通知失败: {error}` String showDownloadCompleteNotificationFailed(String error) { return Intl.message( '显示下载完成通知失败: $error', name: 'showDownloadCompleteNotificationFailed', desc: '', args: [error], ); } /// `显示单个文件下载完成通知失败: {error}` String showSingleFileCompleteNotificationFailed(String error) { return Intl.message( '显示单个文件下载完成通知失败: $error', name: 'showSingleFileCompleteNotificationFailed', desc: '', args: [error], ); } /// `取消下载通知失败: {error}` String cancelDownloadNotificationFailed(String error) { return Intl.message( '取消下载通知失败: $error', name: 'cancelDownloadNotificationFailed', desc: '', args: [error], ); } /// `取消所有通知失败: {error}` String cancelAllNotificationsFailed(String error) { return Intl.message( '取消所有通知失败: $error', name: 'cancelAllNotificationsFailed', desc: '', args: [error], ); } /// `在文件管理器中显示` String get showInFileManager { return Intl.message( '在文件管理器中显示', name: 'showInFileManager', desc: '', args: [], ); } /// `大小: {size}` String fileSize(String size) { return Intl.message('大小: $size', name: 'fileSize', desc: '', args: [size]); } /// `时间: {time}` String fileTime(String time) { return Intl.message('时间: $time', name: 'fileTime', desc: '', args: [time]); } /// `确定` String get ok { return Intl.message('确定', name: 'ok', desc: '', args: []); } /// `打开文件管理器` String get openFileManager { return Intl.message('打开文件管理器', name: 'openFileManager', desc: '', args: []); } /// `已打开文件管理器` String get fileManagerOpened { return Intl.message( '已打开文件管理器', name: 'fileManagerOpened', desc: '', args: [], ); } /// `打开文件管理器失败: {error}` String openFileManagerFailed(String error) { return Intl.message( '打开文件管理器失败: $error', name: 'openFileManagerFailed', desc: '', args: [error], ); } /// `已打开下载目录` String get downloadDirectoryOpened { return Intl.message( '已打开下载目录', name: 'downloadDirectoryOpened', desc: '', args: [], ); } /// `打开下载目录失败: {error}` String openDownloadDirectoryFailed(String error) { return Intl.message( '打开下载目录失败: $error', name: 'openDownloadDirectoryFailed', desc: '', args: [error], ); } /// `下载目录路径未知` String get downloadDirectoryPathUnknown { return Intl.message( '下载目录路径未知', name: 'downloadDirectoryPathUnknown', desc: '', args: [], ); } /// `无法获取下载目录` String get cannotGetDownloadDirectoryError { return Intl.message( '无法获取下载目录', name: 'cannotGetDownloadDirectoryError', desc: '', args: [], ); } /// `开始下载: {filename}` String startDownloadFile(String filename) { return Intl.message( '开始下载: $filename', name: 'startDownloadFile', desc: '', args: [filename], ); } /// `下载完成: {filename}` String downloadCompleteFile(String filename) { return Intl.message( '下载完成: $filename', name: 'downloadCompleteFile', desc: '', args: [filename], ); } /// `下载失败: {filename}` String downloadFailedFile(String filename) { return Intl.message( '下载失败: $filename', name: 'downloadFailedFile', desc: '', args: [filename], ); } /// `用户取消下载` String get userCancelledDownloadError { return Intl.message( '用户取消下载', name: 'userCancelledDownloadError', desc: '', args: [], ); } /// `无法安装 APK 文件,可能需要在设置中开启"允许安装未知来源应用"` String get cannotInstallApkNeedPermission { return Intl.message( '无法安装 APK 文件,可能需要在设置中开启"允许安装未知来源应用"', name: 'cannotInstallApkNeedPermission', desc: '', args: [], ); } /// `没有权限安装 APK 文件,请在设置中开启安装权限` String get noPermissionToInstallApkFile { return Intl.message( '没有权限安装 APK 文件,请在设置中开启安装权限', name: 'noPermissionToInstallApkFile', desc: '', args: [], ); } /// `准备下载...` String get preparingDownloadStatus { return Intl.message( '准备下载...', name: 'preparingDownloadStatus', desc: '', args: [], ); } /// `下载已取消` String get downloadCancelledText { return Intl.message( '下载已取消', name: 'downloadCancelledText', desc: '', args: [], ); } /// `语言` String get language { return Intl.message('语言', name: 'language', desc: '', args: []); } /// `语言设置` String get languageSettings { return Intl.message('语言设置', name: 'languageSettings', desc: '', args: []); } /// `选择应用显示语言` String get languageSettingsDesc { return Intl.message( '选择应用显示语言', name: 'languageSettingsDesc', desc: '', args: [], ); } /// `跟随系统` String get followSystem { return Intl.message('跟随系统', name: 'followSystem', desc: '', args: []); } /// `简体中文` String get simplifiedChinese { return Intl.message('简体中文', name: 'simplifiedChinese', desc: '', args: []); } /// `English` String get english { return Intl.message('English', name: 'english', desc: '', args: []); } /// `疑难解答` String get troubleshooting { return Intl.message('疑难解答', name: 'troubleshooting', desc: '', args: []); } /// `常见问题与解决方案` String get troubleshootingDesc { return Intl.message( '常见问题与解决方案', name: 'troubleshootingDesc', desc: '', args: [], ); } /// `数据库未保存问题` String get databaseNotSavedIssue { return Intl.message( '数据库未保存问题', name: 'databaseNotSavedIssue', desc: '', args: [], ); } /// `如不手动关闭OpenList,则数据库可能不会被保存到对应的db文件中,如遇到此问题,请手动关闭以解决此问题。(开关位于主程序菜单OpenList界面,以及通知栏的通知上)` String get databaseNotSavedIssueDesc { return Intl.message( '如不手动关闭OpenList,则数据库可能不会被保存到对应的db文件中,如遇到此问题,请手动关闭以解决此问题。(开关位于主程序菜单OpenList界面,以及通知栏的通知上)', name: 'databaseNotSavedIssueDesc', desc: '', args: [], ); } /// `自启动相关说明` String get autoStartIssue { return Intl.message('自启动相关说明', name: 'autoStartIssue', desc: '', args: []); } /// `设置自启动时建议把app的电池优化一并关闭,当前在开启自启动后,系统重启时服务会自动在后台启动,但可能不会在通知栏弹出通知。请放心,服务已正常运行,您可以在通知栏快捷开关查看服务状态,或回到主界面查看服务开关确认服务是否已启动。` String get autoStartIssueDesc { return Intl.message( '设置自启动时建议把app的电池优化一并关闭,当前在开启自启动后,系统重启时服务会自动在后台启动,但可能不会在通知栏弹出通知。请放心,服务已正常运行,您可以在通知栏快捷开关查看服务状态,或回到主界面查看服务开关确认服务是否已启动。', name: 'autoStartIssueDesc', desc: '', args: [], ); } /// `正在下载` String get currentlyDownloading { return Intl.message( '正在下载', name: 'currentlyDownloading', desc: '', args: [], ); } /// `下载进度` String get downloadProgressChannel { return Intl.message( '下载进度', name: 'downloadProgressChannel', desc: '', args: [], ); } /// `确认下载` String get confirmDownload { return Intl.message('确认下载', name: 'confirmDownload', desc: '', args: []); } /// `是否要下载这个文件?` String get confirmDownloadMessage { return Intl.message( '是否要下载这个文件?', name: 'confirmDownloadMessage', desc: '', args: [], ); } /// `正在下载图片...` String get downloadingImage { return Intl.message( '正在下载图片...', name: 'downloadingImage', desc: '', args: [], ); } /// `稍后安装` String get laterInstall { return Intl.message('稍后安装', name: 'laterInstall', desc: '', args: []); } /// `立即安装` String get installNow { return Intl.message('立即安装', name: 'installNow', desc: '', args: []); } /// `直接下载` String get directDownloadMethod { return Intl.message( '直接下载', name: 'directDownloadMethod', desc: '', args: [], ); } /// `使用应用内下载器` String get directDownloadMethodDesc { return Intl.message( '使用应用内下载器', name: 'directDownloadMethodDesc', desc: '', args: [], ); } /// `浏览器下载` String get browserDownloadMethod { return Intl.message( '浏览器下载', name: 'browserDownloadMethod', desc: '', args: [], ); } /// `使用系统浏览器` String get browserDownloadMethodDesc { return Intl.message( '使用系统浏览器', name: 'browserDownloadMethodDesc', desc: '', args: [], ); } /// `分享链接` String get shareLink { return Intl.message('分享链接', name: 'shareLink', desc: '', args: []); } /// `分享下载链接` String get shareLinkDesc { return Intl.message('分享下载链接', name: 'shareLinkDesc', desc: '', args: []); } /// `查看` String get view { return Intl.message('查看', name: 'view', desc: '', args: []); } /// `下载功能测试` String get downloadFunctionTest { return Intl.message( '下载功能测试', name: 'downloadFunctionTest', desc: '', args: [], ); } /// `测试直接下载功能` String get testDirectDownloadFunction { return Intl.message( '测试直接下载功能', name: 'testDirectDownloadFunction', desc: '', args: [], ); } /// `测试下载JSON文件` String get testDownloadJsonFile { return Intl.message( '测试下载JSON文件', name: 'testDownloadJsonFile', desc: '', args: [], ); } /// `测试下载PNG图片` String get testDownloadPngImage { return Intl.message( '测试下载PNG图片', name: 'testDownloadPngImage', desc: '', args: [], ); } /// `测试下载大文件(1MB)` String get testDownloadLargeFile { return Intl.message( '测试下载大文件(1MB)', name: 'testDownloadLargeFile', desc: '', args: [], ); } /// `查看下载文件` String get viewDownloadFiles { return Intl.message( '查看下载文件', name: 'viewDownloadFiles', desc: '', args: [], ); } /// `请通过底部导航栏的"下载管理"查看下载文件` String get checkDownloadManagerForFiles { return Intl.message( '请通过底部导航栏的"下载管理"查看下载文件', name: 'checkDownloadManagerForFiles', desc: '', args: [], ); } /// `查看下载目录` String get viewDownloadDirectory { return Intl.message( '查看下载目录', name: 'viewDownloadDirectory', desc: '', args: [], ); } /// `获取失败` String get getDownloadPathFailed { return Intl.message( '获取失败', name: 'getDownloadPathFailed', desc: '', args: [], ); } /// `是否要打开下载测试页面?` String get openDownloadTestPage { return Intl.message( '是否要打开下载测试页面?', name: 'openDownloadTestPage', desc: '', args: [], ); } /// `说明:` String get description { return Intl.message('说明:', name: 'description', desc: '', args: []); } /// `• 文件将下载到系统下载目录\n• 下载过程会显示进度通知\n• 下载完成后可以选择打开文件\n• 如果文件名重复会自动添加序号\n• 请通过底部导航栏的\"下载管理\"查看下载文件` String get downloadInstructions { return Intl.message( '• 文件将下载到系统下载目录\\n• 下载过程会显示进度通知\\n• 下载完成后可以选择打开文件\\n• 如果文件名重复会自动添加序号\\n• 请通过底部导航栏的\\"下载管理\\"查看下载文件', name: 'downloadInstructions', desc: '', args: [], ); } /// `选择下载方式` String get selectDownloadMethod { return Intl.message( '选择下载方式', name: 'selectDownloadMethod', desc: '', args: [], ); } /// `批量下载完成` String get batchDownloadComplete { return Intl.message( '批量下载完成', name: 'batchDownloadComplete', desc: '', args: [], ); } /// `图片下载成功` String get imageDownloadSuccess { return Intl.message( '图片下载成功', name: 'imageDownloadSuccess', desc: '', args: [], ); } /// `请在下载目录查看图片` String get checkImageInDownloadFolder { return Intl.message( '请在下载目录查看图片', name: 'checkImageInDownloadFolder', desc: '', args: [], ); } /// `请在下载目录找到APK文件进行安装` String get findApkInDownloadFolder { return Intl.message( '请在下载目录找到APK文件进行安装', name: 'findApkInDownloadFolder', desc: '', args: [], ); } /// `正在下载第 {current}/{total} 个文件` String downloadingFileProgress(int current, int total) { return Intl.message( '正在下载第 $current/$total 个文件', name: 'downloadingFileProgress', desc: '', args: [current, total], ); } /// `第 {index} 个文件下载失败` String fileDownloadFailed(int index) { return Intl.message( '第 $index 个文件下载失败', name: 'fileDownloadFailed', desc: '', args: [index], ); } /// `APK文件已下载完成,是否要安装?` String get apkDownloadCompleteMessage { return Intl.message( 'APK文件已下载完成,是否要安装?', name: 'apkDownloadCompleteMessage', desc: '', args: [], ); } /// `OpenList` String get openlist { return Intl.message('OpenList', name: 'openlist', desc: '', args: []); } /// `OpenList Mobile` String get openlistMobile { return Intl.message( 'OpenList Mobile', name: 'openlistMobile', desc: '', args: [], ); } /// `开源许可证` String get openSourceLicenses { return Intl.message( '开源许可证', name: 'openSourceLicenses', desc: '', args: [], ); } /// `查看第三方许可证` String get viewThirdPartyLicenses { return Intl.message( '查看第三方许可证', name: 'viewThirdPartyLicenses', desc: '', args: [], ); } /// `修改OpenList配置文件` String get editOpenListConfig { return Intl.message( '修改OpenList配置文件', name: 'editOpenListConfig', desc: '', args: [], ); } /// `保存` String get save { return Intl.message('保存', name: 'save', desc: '', args: []); } /// `已保存` String get saved { return Intl.message('已保存', name: 'saved', desc: '', args: []); } /// `编辑` String get edit { return Intl.message('编辑', name: 'edit', desc: '', args: []); } /// `预览` String get preview { return Intl.message('预览', name: 'preview', desc: '', args: []); } /// `文件不存在,保存时将创建` String get fileNotFoundWillCreateOnSave { return Intl.message( '文件不存在,保存时将创建', name: 'fileNotFoundWillCreateOnSave', desc: '', args: [], ); } /// `加载失败:{error}` String loadFailed(String error) { return Intl.message( '加载失败:$error', name: 'loadFailed', desc: '', args: [error], ); } /// `保存失败:{error}` String saveFailed(String error) { return Intl.message( '保存失败:$error', name: 'saveFailed', desc: '', args: [error], ); } /// `JSON格式错误,第{line}行:{error}` String invalidJsonFormat(int line, String error) { return Intl.message( 'JSON格式错误,第$line行:$error', name: 'invalidJsonFormat', desc: '', args: [line, error], ); } /// `文件权限被拒绝,请检查应用权限` String get filePermissionDenied { return Intl.message( '文件权限被拒绝,请检查应用权限', name: 'filePermissionDenied', desc: '', args: [], ); } /// `配置已保存,请重启OpenList服务以生效` String get configSavedRestartRequired { return Intl.message( '配置已保存,请重启OpenList服务以生效', name: 'configSavedRestartRequired', desc: '', args: [], ); } /// `确认保存` String get confirmSaveConfigTitle { return Intl.message( '确认保存', name: 'confirmSaveConfigTitle', desc: '', args: [], ); } /// `修改配置可能导致服务不可用,确定保存吗?` String get confirmSaveConfigMessage { return Intl.message( '修改配置可能导致服务不可用,确定保存吗?', name: 'confirmSaveConfigMessage', desc: '', args: [], ); } /// `保存并重启` String get saveAndRestart { return Intl.message('保存并重启', name: 'saveAndRestart', desc: '', args: []); } /// `仅保存` String get saveOnly { return Intl.message('仅保存', name: 'saveOnly', desc: '', args: []); } /// `恢复备份` String get restoreBackup { return Intl.message('恢复备份', name: 'restoreBackup', desc: '', args: []); } /// `备份已恢复` String get backupRestored { return Intl.message('备份已恢复', name: 'backupRestored', desc: '', args: []); } /// `未找到备份文件` String get noBackupFound { return Intl.message('未找到备份文件', name: 'noBackupFound', desc: '', args: []); } /// `恢复备份失败:{error}` String restoreBackupFailed(String error) { return Intl.message( '恢复备份失败:$error', name: 'restoreBackupFailed', desc: '', args: [error], ); } /// `正在重启OpenList服务...` String get restartingService { return Intl.message( '正在重启OpenList服务...', name: 'restartingService', desc: '', args: [], ); } /// `服务重启成功` String get serviceRestartSuccess { return Intl.message( '服务重启成功', name: 'serviceRestartSuccess', desc: '', args: [], ); } /// `服务重启失败,请手动重启` String get serviceRestartFailed { return Intl.message( '服务重启失败,请手动重启', name: 'serviceRestartFailed', desc: '', args: [], ); } /// `服务重启仅支持Android系统` String get serviceRestartOnlyAndroid { return Intl.message( '服务重启仅支持Android系统', name: 'serviceRestartOnlyAndroid', desc: '', args: [], ); } } class AppLocalizationDelegate extends LocalizationsDelegate { const AppLocalizationDelegate(); List get supportedLocales { return const [ Locale.fromSubtags(languageCode: 'zh'), Locale.fromSubtags(languageCode: 'en'), ]; } @override bool isSupported(Locale locale) => _isSupported(locale); @override Future load(Locale locale) => S.load(locale); @override bool shouldReload(AppLocalizationDelegate old) => false; bool _isSupported(Locale locale) { for (var supportedLocale in supportedLocales) { if (supportedLocale.languageCode == locale.languageCode) { return true; } } return false; } } ================================================ FILE: lib/generated_api.dart ================================================ // Autogenerated from Pigeon (v26.0.2), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers import 'dart:async'; import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; PlatformException _createConnectionError(String channelName) { return PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel: "$channelName".', ); } List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { if (empty) { return []; } if (error == null) { return [result]; } return [error.code, error.message, error.details]; } class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); } else { super.writeValue(buffer, value); } } @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { default: return super.readValueOfType(type, buffer); } } } class AppConfig { /// Constructor for [AppConfig]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. AppConfig({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) : pigeonVar_binaryMessenger = binaryMessenger, pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); final String pigeonVar_messageChannelSuffix; Future isWakeLockEnabled() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.AppConfig.isWakeLockEnabled$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { return (pigeonVar_replyList[0] as bool?)!; } } Future setWakeLockEnabled(bool enabled) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.AppConfig.setWakeLockEnabled$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([enabled]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else { return; } } Future isStartAtBootEnabled() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.AppConfig.isStartAtBootEnabled$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { return (pigeonVar_replyList[0] as bool?)!; } } Future setStartAtBootEnabled(bool enabled) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.AppConfig.setStartAtBootEnabled$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([enabled]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else { return; } } Future isAutoCheckUpdateEnabled() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.AppConfig.isAutoCheckUpdateEnabled$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { return (pigeonVar_replyList[0] as bool?)!; } } Future setAutoCheckUpdateEnabled(bool enabled) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.AppConfig.setAutoCheckUpdateEnabled$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([enabled]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else { return; } } Future isAutoOpenWebPageEnabled() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.AppConfig.isAutoOpenWebPageEnabled$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { return (pigeonVar_replyList[0] as bool?)!; } } Future setAutoOpenWebPageEnabled(bool enabled) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.AppConfig.setAutoOpenWebPageEnabled$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([enabled]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else { return; } } Future getDataDir() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.AppConfig.getDataDir$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { return (pigeonVar_replyList[0] as String?)!; } } Future setDataDir(String dir) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.AppConfig.setDataDir$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([dir]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else { return; } } Future isSilentJumpAppEnabled() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.AppConfig.isSilentJumpAppEnabled$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { return (pigeonVar_replyList[0] as bool?)!; } } Future setSilentJumpAppEnabled(bool enabled) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.AppConfig.setSilentJumpAppEnabled$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([enabled]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else { return; } } } class NativeCommon { /// Constructor for [NativeCommon]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. NativeCommon({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) : pigeonVar_binaryMessenger = binaryMessenger, pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); final String pigeonVar_messageChannelSuffix; Future startActivityFromUri(String intentUri) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.NativeCommon.startActivityFromUri$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([intentUri]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { return (pigeonVar_replyList[0] as bool?)!; } } Future getDeviceSdkInt() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.NativeCommon.getDeviceSdkInt$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { return (pigeonVar_replyList[0] as int?)!; } } Future getDeviceCPUABI() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.NativeCommon.getDeviceCPUABI$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { return (pigeonVar_replyList[0] as String?)!; } } Future getVersionName() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.NativeCommon.getVersionName$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { return (pigeonVar_replyList[0] as String?)!; } } Future getVersionCode() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.NativeCommon.getVersionCode$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { return (pigeonVar_replyList[0] as int?)!; } } Future toast(String msg) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.NativeCommon.toast$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([msg]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else { return; } } Future longToast(String msg) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.NativeCommon.longToast$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([msg]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else { return; } } } class Android { /// Constructor for [Android]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. Android({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) : pigeonVar_binaryMessenger = binaryMessenger, pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); final String pigeonVar_messageChannelSuffix; Future addShortcut() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.Android.addShortcut$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else { return; } } Future startService() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.Android.startService$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else { return; } } Future setAdminPwd(String pwd) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.Android.setAdminPwd$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([pwd]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else { return; } } Future getOpenListHttpPort() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.Android.getOpenListHttpPort$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { return (pigeonVar_replyList[0] as int?)!; } } Future isRunning() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.Android.isRunning$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { return (pigeonVar_replyList[0] as bool?)!; } } Future getOpenListVersion() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.openlist_mobile.Android.getOpenListVersion$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { throw PlatformException( code: pigeonVar_replyList[0]! as String, message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { return (pigeonVar_replyList[0] as String?)!; } } } abstract class Event { static const MessageCodec pigeonChannelCodec = _PigeonCodec(); void onServiceStatusChanged(bool isRunning); void onServerLog(int level, String time, String log); static void setUp(Event? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; { final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( 'dev.flutter.pigeon.openlist_mobile.Event.onServiceStatusChanged$messageChannelSuffix', pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.openlist_mobile.Event.onServiceStatusChanged was null.'); final List args = (message as List?)!; final bool? arg_isRunning = (args[0] as bool?); assert(arg_isRunning != null, 'Argument for dev.flutter.pigeon.openlist_mobile.Event.onServiceStatusChanged was null, expected non-null bool.'); try { api.onServiceStatusChanged(arg_isRunning!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); } catch (e) { return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); } }); } } { final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( 'dev.flutter.pigeon.openlist_mobile.Event.onServerLog$messageChannelSuffix', pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.openlist_mobile.Event.onServerLog was null.'); final List args = (message as List?)!; final int? arg_level = (args[0] as int?); assert(arg_level != null, 'Argument for dev.flutter.pigeon.openlist_mobile.Event.onServerLog was null, expected non-null int.'); final String? arg_time = (args[1] as String?); assert(arg_time != null, 'Argument for dev.flutter.pigeon.openlist_mobile.Event.onServerLog was null, expected non-null String.'); final String? arg_log = (args[2] as String?); assert(arg_log != null, 'Argument for dev.flutter.pigeon.openlist_mobile.Event.onServerLog was null, expected non-null String.'); try { api.onServerLog(arg_level!, arg_time!, arg_log!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); } catch (e) { return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); } }); } } } } ================================================ FILE: lib/l10n/intl_en.arb ================================================ { "@@locale": "en", "appName": "OpenList", "desktopShortcut": "Desktop shortcut", "setAdminPassword": "Set Admin password", "moreOptions": "More options", "checkForUpdates": "Check for updates", "currentIsLatestVersion": "Current is latest version", "confirm": "OK", "cancel": "Cancel", "releasePage": "Release Page", "downloadApk": "Download APK", "about": "About", "general": "General", "autoCheckForUpdates": "Auto check for updates", "autoCheckForUpdatesDesc": "Check for updates when app starts", "wakeLock": "Wake lock", "wakeLockDesc": "Prevent CPU from sleeping when screen is off. (May cause app killed in background on some devices)", "bootAutoStartService": "Boot auto-start service", "bootAutoStartServiceDesc": "Automatically start OpenList service after boot. (Please make sure to grant auto-start permission)", "webPage": "Web Page", "settings": "Settings", "jumpToOtherApp": "Jump to other app?", "selectAppToOpen": "Select app to open", "goTo": "GO", "downloadThisFile": "Download this file?", "download": "download", "copiedToClipboard": "Copied to clipboard", "importantSettings": "Important settings", "uiSettings": "UI", "grantManagerStoragePermission": "Grant [Manage external storage] permission", "grantStoragePermissionDesc": "Mounting local storage is a must, otherwise no permission to read and write files", "grantStoragePermission": "Grant [external storage] permission", "grantNotificationPermission": "Grant [Notification] permission", "grantNotificationPermissionDesc": "Used for foreground service keep alive", "autoStartWebPage": "Set web page as startup page", "autoStartWebPageDesc": "Default page when opening main interface", "dataDirectory": "data Directory", "setDefaultDirectory": "Set as default directory?", "silentJumpApp": "Silent jump app", "silentJumpAppDesc": "Jump to other app without prompt", "newVersionFound": "New Version Found", "directDownloadApk": "Direct Download APK", "initializingNotificationManager": "Initializing notification manager", "downloadManager": "Download", "downloadManagerWithCount": "Download ({count})", "@downloadManagerWithCount": { "placeholders": { "count": { "type": "int" } } }, "modifyAdminPassword": "Modify Admin Password", "directDownload": "Direct Download", "browserDownload": "Browser Download", "loadDownloadFilesFailed": "Failed to load download files", "noActiveDownloads": "No active downloads", "downloadFailed": "Download failed", "startTime": "Start time", "noCompletedDownloads": "No completed downloads", "completedTime": "Completed time", "size": "Size", "openFile": "Open file", "deleteRecord": "Delete record", "deleteFile": "Delete file", "confirmClear": "Confirm Clear", "confirmClearAllFiles": "Are you sure you want to clear all download files? This action cannot be undone.", "cleared": "Download directory cleared", "clearFailed": "Clear failed", "cancelDownload": "Cancel Download", "confirmCancelDownload": "Are you sure you want to cancel downloading \"{filename}\"?", "@confirmCancelDownload": { "placeholders": { "filename": { "type": "String" } } }, "continueDownload": "Continue Download", "confirmDeleteRecord": "Are you sure you want to delete the download record of \"{filename}\"?", "@confirmDeleteRecord": { "placeholders": { "filename": { "type": "String" } } }, "delete": "Delete", "shareFile": "Share file", "shareFeatureNotImplemented": "Share feature not implemented yet", "fileInfo": "File info", "fileName": "File name", "modifiedTime": "Modified time", "filePath": "Path", "confirmDelete": "Confirm Delete", "confirmDeleteFile": "Are you sure you want to delete file \"{filename}\"? This action cannot be undone.", "@confirmDeleteFile": { "placeholders": { "filename": { "type": "String" } } }, "fileDeleted": "File deleted", "deleteFailed": "Delete failed", "clear": "Clear", "noAppToOpenFile": "No app found to open this file", "viewLocation": "View Location", "fileNotFound": "File not found or has been deleted", "noPermissionToOpenFile": "No permission to open this file", "openFileFailed": "Failed to open file: {error}", "@openFileFailed": { "placeholders": { "error": { "type": "String" } } }, "fileLocation": "File Location", "fileSavedTo": "File saved to:", "fileLocationTip": "You can use a file manager to find this file, or try installing the appropriate app to open it.", "downloadDirectory": "Download Directory", "openDirectory": "Open Directory", "clearRecords": "Clear Records", "clearAll": "Clear All", "downloadRecordsCleared": "Download records cleared", "inProgress": "In Progress", "completed": "Completed", "refresh": "Refresh", "pending": "Pending", "downloading": "Downloading", "failed": "Failed", "cancelled": "Cancelled", "cannotGetDownloadDirectory": "Cannot get download directory", "startDownload": "Start download: {filename}", "@startDownload": { "placeholders": { "filename": { "type": "String" } } }, "downloadProgress": "Download progress: {progress}%", "@downloadProgress": { "placeholders": { "progress": { "type": "String" } } }, "downloadComplete": "Download complete: {filename}", "@downloadComplete": { "placeholders": { "filename": { "type": "String" } } }, "open": "Open", "downloadCancelled": "Download cancelled: {url}", "@downloadCancelled": { "placeholders": { "url": { "type": "String" } } }, "downloadFailedWithError": "Download failed: {filename}", "@downloadFailedWithError": { "placeholders": { "filename": { "type": "String" } } }, "userCancelledDownload": "User cancelled download", "cannotGetBaseDownloadDirectory": "Cannot get base download directory", "createOpenListDownloadDirectory": "Create OpenList download directory: {path}", "@createOpenListDownloadDirectory": { "placeholders": { "path": { "type": "String" } } }, "createOpenListDirectoryFailed": "Failed to create OpenList directory: {error}", "@createOpenListDirectoryFailed": { "placeholders": { "error": { "type": "String" } } }, "openListDownloadDirectory": "OpenList download directory: {path}", "@openListDownloadDirectory": { "placeholders": { "path": { "type": "String" } } }, "getDownloadDirectoryFailed": "Failed to get download directory: {error}", "@getDownloadDirectoryFailed": { "placeholders": { "error": { "type": "String" } } }, "parseFilenameFailed": "Failed to parse filename: {error}", "@parseFilenameFailed": { "placeholders": { "error": { "type": "String" } } }, "needInstallPermission": "Install Permission Required", "needInstallPermissionDesc": "To install APK files, install permission is required. Please enable it manually in settings.", "goToSettings": "Go to Settings", "needInstallPermissionToInstallApk": "Install permission is required to install APK files", "checkInstallPermissionFailed": "Failed to check install permission: {error}", "@checkInstallPermissionFailed": { "placeholders": { "error": { "type": "String" } } }, "tryToOpenFile": "Trying to open file: {path}", "@tryToOpenFile": { "placeholders": { "path": { "type": "String" } } }, "openFileResult": "Open file result: {type} - {message}", "@openFileResult": { "placeholders": { "type": { "type": "String" }, "message": { "type": "String" } } }, "cannotInstallApkFile": "Cannot install APK file, you may need to enable \"Install unknown apps\" in settings", "noPermissionToInstallApk": "No permission to install APK file, please enable install permission in settings", "openFileException": "Open file exception: {error}", "@openFileException": { "placeholders": { "error": { "type": "String" } } }, "getDownloadFileListFailed": "Failed to get download file list: {error}", "@getDownloadFileListFailed": { "placeholders": { "error": { "type": "String" } } }, "downloadDirectoryCleared": "Download directory cleared", "clearDownloadDirectoryFailed": "Failed to clear download directory: {error}", "@clearDownloadDirectoryFailed": { "placeholders": { "error": { "type": "String" } } }, "fileDeletedLog": "File deleted: {filename}", "@fileDeletedLog": { "placeholders": { "filename": { "type": "String" } } }, "deleteFileFailedLog": "Failed to delete file: {error}", "@deleteFileFailedLog": { "placeholders": { "error": { "type": "String" } } }, "preparingDownload": "Preparing download...", "downloadCancelledStatus": "Download cancelled", "notificationManagerInitialized": "Notification manager initialized successfully", "notificationManagerInitFailed": "Failed to initialize notification manager: {error}", "@notificationManagerInitFailed": { "placeholders": { "error": { "type": "String" } } }, "notificationClicked": "Notification clicked: {payload}", "@notificationClicked": { "placeholders": { "payload": { "type": "String" } } }, "currentDownloadingFiles": "Currently {count} files are downloading", "@currentDownloadingFiles": { "placeholders": { "count": { "type": "int" } } }, "downloadProgressDesc": "Show file download progress", "viewDownloads": "View Downloads", "showDownloadProgressNotificationFailed": "Failed to show download progress notification: {error}", "@showDownloadProgressNotificationFailed": { "placeholders": { "error": { "type": "String" } } }, "downloadCompleteNotificationTitle": "{filename} download completed", "@downloadCompleteNotificationTitle": { "placeholders": { "filename": { "type": "String" } } }, "clickToJumpToDownloadManager": "Click to jump to download manager", "downloadCompleteTitle": "Download Complete", "multipleFilesCompleted": "{count} files completed, click to jump to download manager", "@multipleFilesCompleted": { "placeholders": { "count": { "type": "int" } } }, "downloadCompleteChannel": "Download Complete", "downloadCompleteChannelDesc": "File download complete notification", "openDownloadManager": "Open Download Manager", "showDownloadCompleteNotificationFailed": "Failed to show download complete notification: {error}", "@showDownloadCompleteNotificationFailed": { "placeholders": { "error": { "type": "String" } } }, "showSingleFileCompleteNotificationFailed": "Failed to show single file complete notification: {error}", "@showSingleFileCompleteNotificationFailed": { "placeholders": { "error": { "type": "String" } } }, "cancelDownloadNotificationFailed": "Failed to cancel download notification: {error}", "@cancelDownloadNotificationFailed": { "placeholders": { "error": { "type": "String" } } }, "cancelAllNotificationsFailed": "Failed to cancel all notifications: {error}", "@cancelAllNotificationsFailed": { "placeholders": { "error": { "type": "String" } } }, "showInFileManager": "Show in file manager", "fileSize": "Size: {size}", "@fileSize": { "placeholders": { "size": { "type": "String" } } }, "fileTime": "Time: {time}", "@fileTime": { "placeholders": { "time": { "type": "String" } } }, "ok": "OK", "openFileManager": "Open File Manager", "fileManagerOpened": "File manager opened", "openFileManagerFailed": "Failed to open file manager: {error}", "@openFileManagerFailed": { "placeholders": { "error": { "type": "String" } } }, "downloadDirectoryOpened": "Download directory opened", "openDownloadDirectoryFailed": "Failed to open download directory: {error}", "@openDownloadDirectoryFailed": { "placeholders": { "error": { "type": "String" } } }, "downloadDirectoryPathUnknown": "Download directory path unknown", "cannotGetDownloadDirectoryError": "Cannot get download directory", "startDownloadFile": "Start download: {filename}", "@startDownloadFile": { "placeholders": { "filename": { "type": "String" } } }, "downloadCompleteFile": "Download complete: {filename}", "@downloadCompleteFile": { "placeholders": { "filename": { "type": "String" } } }, "downloadFailedFile": "Download failed: {filename}", "@downloadFailedFile": { "placeholders": { "filename": { "type": "String" } } }, "userCancelledDownloadError": "User cancelled download", "cannotInstallApkNeedPermission": "Cannot install APK file, you may need to enable \"Install unknown apps\" in settings", "noPermissionToInstallApkFile": "No permission to install APK file, please enable install permission in settings", "preparingDownloadStatus": "Preparing download...", "downloadCancelledText": "Download cancelled", "language": "Language", "languageSettings": "Language Settings", "languageSettingsDesc": "Select app display language", "followSystem": "Follow System", "simplifiedChinese": "简体中文", "english": "English", "troubleshooting": "Troubleshooting", "troubleshootingDesc": "Common issues and solutions", "databaseNotSavedIssue": "Database Not Saved Issue", "databaseNotSavedIssueDesc": "If you don't manually close OpenList, the database may not be saved to the corresponding db file. If you encounter this issue, please manually close the app to resolve it. (The switch is located in the main program menu on the OpenList interface, as well as in the notification bar)", "autoStartIssue": "Auto-Start Information", "autoStartIssueDesc": "When enabling auto-start, it's recommended to disable battery optimization for the app. Currently, after enabling auto-start, the service will automatically start in the background after system reboot, but may not show a notification in the notification bar. Rest assured, the service is running normally. You can check the service status through the quick settings tile in the notification shade, or return to the main interface to check the service toggle to confirm if the service has started.", "currentlyDownloading": "Downloading", "downloadProgressChannel": "Download Progress", "confirmDownload": "Confirm Download", "confirmDownloadMessage": "Do you want to download this file?", "downloadingImage": "Downloading image...", "laterInstall": "Install Later", "installNow": "Install Now", "directDownloadMethod": "Direct Download", "directDownloadMethodDesc": "Use in-app downloader", "browserDownloadMethod": "Browser Download", "browserDownloadMethodDesc": "Use system browser", "shareLink": "Share Link", "shareLinkDesc": "Share download link", "view": "View", "downloadFunctionTest": "Download Function Test", "testDirectDownloadFunction": "Test direct download function", "testDownloadJsonFile": "Test download JSON file", "testDownloadPngImage": "Test download PNG image", "testDownloadLargeFile": "Test download large file (1MB)", "viewDownloadFiles": "View download files", "checkDownloadManagerForFiles": "Please check download manager via bottom navigation bar to view download files", "viewDownloadDirectory": "View download directory", "getDownloadPathFailed": "Failed to get", "openDownloadTestPage": "Do you want to open download test page?", "description": "Description:", "downloadInstructions": "• Files will be downloaded to system download directory\\n• Download progress will show notifications\\n• You can choose to open file after download completes\\n• If filename exists, a number will be added automatically\\n• Please check download manager via bottom navigation bar to view files", "selectDownloadMethod": "Select download method", "batchDownloadComplete": "Batch download complete", "imageDownloadSuccess": "Image download success", "checkImageInDownloadFolder": "Please check image in download folder", "findApkInDownloadFolder": "Please find APK file in download folder to install", "downloadingFileProgress": "Downloading file {current}/{total}", "@downloadingFileProgress": { "placeholders": { "current": { "type": "int" }, "total": { "type": "int" } } }, "fileDownloadFailed": "File {index} download failed", "@fileDownloadFailed": { "placeholders": { "index": { "type": "int" } } }, "apkDownloadCompleteMessage": "APK file download completed, do you want to install?", "openlist": "OpenList", "openlistMobile": "OpenList Mobile", "openSourceLicenses": "Open Source Licenses", "viewThirdPartyLicenses": "View third-party licenses", "editOpenListConfig": "Edit OpenList Config", "save": "Save", "saved": "Saved", "edit": "Edit", "preview": "Preview", "fileNotFoundWillCreateOnSave": "File not found. Will create on save.", "loadFailed": "Load failed: {error}", "@loadFailed": { "placeholders": { "error": { "type": "String" } } }, "saveFailed": "Save failed: {error}", "@saveFailed": { "placeholders": { "error": { "type": "String" } } }, "invalidJsonFormat": "Invalid JSON format at line {line}: {error}", "@invalidJsonFormat": { "placeholders": { "line": { "type": "int" }, "error": { "type": "String" } } }, "filePermissionDenied": "File permission denied. Please check app permissions.", "configSavedRestartRequired": "Config saved. Please restart OpenList service to take effect.", "confirmSaveConfigTitle": "Confirm Save", "confirmSaveConfigMessage": "Modifying configuration may cause service unavailable. Are you sure to save?", "saveAndRestart": "Save and Restart", "saveOnly": "Save Only", "restoreBackup": "Restore Backup", "backupRestored": "Backup restored successfully", "noBackupFound": "No backup file found", "restoreBackupFailed": "Restore backup failed: {error}", "@restoreBackupFailed": { "placeholders": { "error": { "type": "String" } } }, "restartingService": "Restarting OpenList service...", "serviceRestartSuccess": "Service restarted successfully", "serviceRestartFailed": "Failed to restart service. Please restart manually.", "serviceRestartOnlyAndroid": "Service restart is only supported on Android" } ================================================ FILE: lib/l10n/intl_zh.arb ================================================ { "@@locale": "zh", "appName": "OpenList", "desktopShortcut": "桌面快捷方式", "setAdminPassword": "设置Admin密码", "moreOptions": "更多选项", "checkForUpdates": "检查更新", "currentIsLatestVersion": "已经是最新版本", "confirm": "确认", "cancel": "取消", "releasePage": "发布页面", "downloadApk": "下载APK", "about": "关于", "general": "通用", "autoCheckForUpdates": "自动检查更新", "autoCheckForUpdatesDesc": "启动时自动检查更新", "wakeLock": "唤醒锁", "wakeLockDesc": "开启防止锁屏后CPU休眠,保持进程在后台运行。(部分系统可能导致杀后台)", "bootAutoStartService": "开机自启动服务", "bootAutoStartServiceDesc": "在开机后自动启动OpenList服务。(请确保授予自启动权限)", "webPage": "网页", "settings": "设置", "selectAppToOpen": "选择应用打开", "goTo": "前往", "downloadThisFile": "下载此文件吗?", "download": "下载", "copiedToClipboard": "已复制到剪贴板", "importantSettings": "重要", "uiSettings": "界面", "grantManagerStoragePermission": "申请【所有文件访问权限】", "grantStoragePermissionDesc": "挂载本地存储时必须授予,否则无权限读写文件", "grantStoragePermission": "申请【读写外置存储权限】", "grantNotificationPermission": "申请【通知权限】", "grantNotificationPermissionDesc": "用于前台服务保活", "autoStartWebPage": "将网页设置为打开首页", "jumpToOtherApp": "跳转到其他APP ?", "autoStartWebPageDesc": "打开主界面时的首页", "dataDirectory": "data 文件夹路径", "setDefaultDirectory": "是否设为初始目录?", "silentJumpApp": "静默跳转APP", "silentJumpAppDesc": "跳转APP时,不弹出提示框", "newVersionFound": "发现新版本", "directDownloadApk": "直接下载APK", "initializingNotificationManager": "初始化通知管理器", "downloadManager": "下载管理", "downloadManagerWithCount": "下载管理({count})", "@downloadManagerWithCount": { "placeholders": { "count": { "type": "int" } } }, "modifyAdminPassword": "修改Admin密码", "directDownload": "直接下载", "browserDownload": "浏览器下载", "loadDownloadFilesFailed": "加载下载文件失败", "noActiveDownloads": "暂无进行中的下载", "downloadFailed": "下载失败", "startTime": "开始时间", "noCompletedDownloads": "暂无已完成的下载", "completedTime": "完成时间", "size": "大小", "openFile": "打开文件", "deleteRecord": "删除记录", "deleteFile": "删除文件", "confirmClear": "确认清空", "confirmClearAllFiles": "确定要清空所有下载文件吗?此操作不可撤销。", "cleared": "已清空下载目录", "clearFailed": "清空失败", "cancelDownload": "取消下载", "confirmCancelDownload": "确定要取消下载 \"{filename}\" 吗?", "@confirmCancelDownload": { "placeholders": { "filename": { "type": "String" } } }, "continueDownload": "继续下载", "confirmDeleteRecord": "确定要删除 \"{filename}\" 的下载记录吗?", "@confirmDeleteRecord": { "placeholders": { "filename": { "type": "String" } } }, "delete": "删除", "shareFile": "分享文件", "shareFeatureNotImplemented": "分享功能待实现", "fileInfo": "文件信息", "fileName": "文件名", "modifiedTime": "修改时间", "filePath": "路径", "confirmDelete": "确认删除", "confirmDeleteFile": "确定要删除文件 \"{filename}\" 吗?此操作不可撤销。", "@confirmDeleteFile": { "placeholders": { "filename": { "type": "String" } } }, "fileDeleted": "文件已删除", "deleteFailed": "删除失败", "clear": "清空", "noAppToOpenFile": "没有找到可以打开此文件的应用", "viewLocation": "查看位置", "fileNotFound": "文件不存在或已被删除", "noPermissionToOpenFile": "没有权限打开此文件", "openFileFailed": "打开文件失败: {error}", "@openFileFailed": { "placeholders": { "error": { "type": "String" } } }, "fileLocation": "文件位置", "fileSavedTo": "文件已保存到:", "fileLocationTip": "您可以使用文件管理器找到此文件,或者尝试安装相应的应用来打开它。", "downloadDirectory": "下载目录", "openDirectory": "打开目录", "clearRecords": "清空记录", "clearAll": "清空所有", "downloadRecordsCleared": "已清空下载记录", "inProgress": "进行中", "completed": "已完成", "refresh": "刷新", "pending": "等待中", "downloading": "下载中", "failed": "失败", "cancelled": "已取消", "cannotGetDownloadDirectory": "无法获取下载目录", "startDownload": "开始下载: {filename}", "@startDownload": { "placeholders": { "filename": { "type": "String" } } }, "downloadProgress": "下载进度: {progress}%", "@downloadProgress": { "placeholders": { "progress": { "type": "String" } } }, "downloadComplete": "下载完成: {filename}", "@downloadComplete": { "placeholders": { "filename": { "type": "String" } } }, "open": "打开", "downloadCancelled": "下载已取消: {url}", "@downloadCancelled": { "placeholders": { "url": { "type": "String" } } }, "downloadFailedWithError": "下载失败: {filename}", "@downloadFailedWithError": { "placeholders": { "filename": { "type": "String" } } }, "userCancelledDownload": "用户取消下载", "cannotGetBaseDownloadDirectory": "无法获取基础下载目录", "createOpenListDownloadDirectory": "创建OpenList下载目录: {path}", "@createOpenListDownloadDirectory": { "placeholders": { "path": { "type": "String" } } }, "createOpenListDirectoryFailed": "创建OpenList目录失败: {error}", "@createOpenListDirectoryFailed": { "placeholders": { "error": { "type": "String" } } }, "openListDownloadDirectory": "OpenList下载目录: {path}", "@openListDownloadDirectory": { "placeholders": { "path": { "type": "String" } } }, "getDownloadDirectoryFailed": "获取下载目录失败: {error}", "@getDownloadDirectoryFailed": { "placeholders": { "error": { "type": "String" } } }, "parseFilenameFailed": "解析文件名失败: {error}", "@parseFilenameFailed": { "placeholders": { "error": { "type": "String" } } }, "needInstallPermission": "需要安装权限", "needInstallPermissionDesc": "为了安装 APK 文件,需要授予安装权限。请在设置中手动开启。", "goToSettings": "去设置", "needInstallPermissionToInstallApk": "需要安装权限才能安装 APK 文件", "checkInstallPermissionFailed": "检查安装权限失败: {error}", "@checkInstallPermissionFailed": { "placeholders": { "error": { "type": "String" } } }, "tryToOpenFile": "尝试打开文件: {path}", "@tryToOpenFile": { "placeholders": { "path": { "type": "String" } } }, "openFileResult": "打开文件结果: {type} - {message}", "@openFileResult": { "placeholders": { "type": { "type": "String" }, "message": { "type": "String" } } }, "cannotInstallApkFile": "无法安装 APK 文件,可能需要在设置中开启\"允许安装未知来源应用\"", "noPermissionToInstallApk": "没有权限安装 APK 文件,请在设置中开启安装权限", "openFileException": "打开文件异常: {error}", "@openFileException": { "placeholders": { "error": { "type": "String" } } }, "getDownloadFileListFailed": "获取下载文件列表失败: {error}", "@getDownloadFileListFailed": { "placeholders": { "error": { "type": "String" } } }, "downloadDirectoryCleared": "已清理下载目录", "clearDownloadDirectoryFailed": "清理下载目录失败: {error}", "@clearDownloadDirectoryFailed": { "placeholders": { "error": { "type": "String" } } }, "fileDeletedLog": "已删除文件: {filename}", "@fileDeletedLog": { "placeholders": { "filename": { "type": "String" } } }, "deleteFileFailedLog": "删除文件失败: {error}", "@deleteFileFailedLog": { "placeholders": { "error": { "type": "String" } } }, "preparingDownload": "准备下载...", "downloadCancelledStatus": "下载已取消", "notificationManagerInitialized": "通知管理器初始化成功", "notificationManagerInitFailed": "通知管理器初始化失败: {error}", "@notificationManagerInitFailed": { "placeholders": { "error": { "type": "String" } } }, "notificationClicked": "通知被点击: {payload}", "@notificationClicked": { "placeholders": { "payload": { "type": "String" } } }, "currentDownloadingFiles": "当前有 {count} 个文件在下载", "@currentDownloadingFiles": { "placeholders": { "count": { "type": "int" } } }, "downloadProgressDesc": "显示文件下载进度", "viewDownloads": "查看下载", "showDownloadProgressNotificationFailed": "显示下载进度通知失败: {error}", "@showDownloadProgressNotificationFailed": { "placeholders": { "error": { "type": "String" } } }, "downloadCompleteNotificationTitle": "{filename} 下载完毕", "@downloadCompleteNotificationTitle": { "placeholders": { "filename": { "type": "String" } } }, "clickToJumpToDownloadManager": "点击跳转到下载管理", "downloadCompleteTitle": "下载完成", "multipleFilesCompleted": "{count} 个文件已完成,点击跳转到下载管理", "@multipleFilesCompleted": { "placeholders": { "count": { "type": "int" } } }, "downloadCompleteChannel": "下载完成", "downloadCompleteChannelDesc": "文件下载完成通知", "openDownloadManager": "打开下载管理", "showDownloadCompleteNotificationFailed": "显示下载完成通知失败: {error}", "@showDownloadCompleteNotificationFailed": { "placeholders": { "error": { "type": "String" } } }, "showSingleFileCompleteNotificationFailed": "显示单个文件下载完成通知失败: {error}", "@showSingleFileCompleteNotificationFailed": { "placeholders": { "error": { "type": "String" } } }, "cancelDownloadNotificationFailed": "取消下载通知失败: {error}", "@cancelDownloadNotificationFailed": { "placeholders": { "error": { "type": "String" } } }, "cancelAllNotificationsFailed": "取消所有通知失败: {error}", "@cancelAllNotificationsFailed": { "placeholders": { "error": { "type": "String" } } }, "showInFileManager": "在文件管理器中显示", "fileSize": "大小: {size}", "@fileSize": { "placeholders": { "size": { "type": "String" } } }, "fileTime": "时间: {time}", "@fileTime": { "placeholders": { "time": { "type": "String" } } }, "ok": "确定", "openFileManager": "打开文件管理器", "fileManagerOpened": "已打开文件管理器", "openFileManagerFailed": "打开文件管理器失败: {error}", "@openFileManagerFailed": { "placeholders": { "error": { "type": "String" } } }, "downloadDirectoryOpened": "已打开下载目录", "openDownloadDirectoryFailed": "打开下载目录失败: {error}", "@openDownloadDirectoryFailed": { "placeholders": { "error": { "type": "String" } } }, "downloadDirectoryPathUnknown": "下载目录路径未知", "cannotGetDownloadDirectoryError": "无法获取下载目录", "startDownloadFile": "开始下载: {filename}", "@startDownloadFile": { "placeholders": { "filename": { "type": "String" } } }, "downloadCompleteFile": "下载完成: {filename}", "@downloadCompleteFile": { "placeholders": { "filename": { "type": "String" } } }, "downloadFailedFile": "下载失败: {filename}", "@downloadFailedFile": { "placeholders": { "filename": { "type": "String" } } }, "userCancelledDownloadError": "用户取消下载", "cannotInstallApkNeedPermission": "无法安装 APK 文件,可能需要在设置中开启\"允许安装未知来源应用\"", "noPermissionToInstallApkFile": "没有权限安装 APK 文件,请在设置中开启安装权限", "preparingDownloadStatus": "准备下载...", "downloadCancelledText": "下载已取消", "language": "语言", "languageSettings": "语言设置", "languageSettingsDesc": "选择应用显示语言", "followSystem": "跟随系统", "simplifiedChinese": "简体中文", "english": "English", "troubleshooting": "疑难解答", "troubleshootingDesc": "常见问题与解决方案", "databaseNotSavedIssue": "数据库未保存问题", "databaseNotSavedIssueDesc": "如不手动关闭OpenList,则数据库可能不会被保存到对应的db文件中,如遇到此问题,请手动关闭以解决此问题。(开关位于主程序菜单OpenList界面,以及通知栏的通知上)", "autoStartIssue": "自启动相关说明", "autoStartIssueDesc": "设置自启动时建议把app的电池优化一并关闭,当前在开启自启动后,系统重启时服务会自动在后台启动,但可能不会在通知栏弹出通知。请放心,服务已正常运行,您可以在通知栏快捷开关查看服务状态,或回到主界面查看服务开关确认服务是否已启动。", "currentlyDownloading": "正在下载", "downloadProgressChannel": "下载进度", "confirmDownload": "确认下载", "confirmDownloadMessage": "是否要下载这个文件?", "downloadingImage": "正在下载图片...", "laterInstall": "稍后安装", "installNow": "立即安装", "directDownloadMethod": "直接下载", "directDownloadMethodDesc": "使用应用内下载器", "browserDownloadMethod": "浏览器下载", "browserDownloadMethodDesc": "使用系统浏览器", "shareLink": "分享链接", "shareLinkDesc": "分享下载链接", "view": "查看", "downloadFunctionTest": "下载功能测试", "testDirectDownloadFunction": "测试直接下载功能", "testDownloadJsonFile": "测试下载JSON文件", "testDownloadPngImage": "测试下载PNG图片", "testDownloadLargeFile": "测试下载大文件(1MB)", "viewDownloadFiles": "查看下载文件", "checkDownloadManagerForFiles": "请通过底部导航栏的\"下载管理\"查看下载文件", "viewDownloadDirectory": "查看下载目录", "getDownloadPathFailed": "获取失败", "openDownloadTestPage": "是否要打开下载测试页面?", "description": "说明:", "downloadInstructions": "• 文件将下载到系统下载目录\\n• 下载过程会显示进度通知\\n• 下载完成后可以选择打开文件\\n• 如果文件名重复会自动添加序号\\n• 请通过底部导航栏的\\\"下载管理\\\"查看下载文件", "selectDownloadMethod": "选择下载方式", "batchDownloadComplete": "批量下载完成", "imageDownloadSuccess": "图片下载成功", "checkImageInDownloadFolder": "请在下载目录查看图片", "findApkInDownloadFolder": "请在下载目录找到APK文件进行安装", "downloadingFileProgress": "正在下载第 {current}/{total} 个文件", "@downloadingFileProgress": { "placeholders": { "current": { "type": "int" }, "total": { "type": "int" } } }, "fileDownloadFailed": "第 {index} 个文件下载失败", "@fileDownloadFailed": { "placeholders": { "index": { "type": "int" } } }, "apkDownloadCompleteMessage": "APK文件已下载完成,是否要安装?", "openlist": "OpenList", "openlistMobile": "OpenList Mobile", "openSourceLicenses": "开源许可证", "viewThirdPartyLicenses": "查看第三方许可证", "editOpenListConfig": "修改OpenList配置文件", "save": "保存", "saved": "已保存", "edit": "编辑", "preview": "预览", "fileNotFoundWillCreateOnSave": "文件不存在,保存时将创建", "loadFailed": "加载失败:{error}", "@loadFailed": { "placeholders": { "error": { "type": "String" } } }, "saveFailed": "保存失败:{error}", "@saveFailed": { "placeholders": { "error": { "type": "String" } } }, "invalidJsonFormat": "JSON格式错误,第{line}行:{error}", "@invalidJsonFormat": { "placeholders": { "line": { "type": "int" }, "error": { "type": "String" } } }, "filePermissionDenied": "文件权限被拒绝,请检查应用权限", "configSavedRestartRequired": "配置已保存,请重启OpenList服务以生效", "confirmSaveConfigTitle": "确认保存", "confirmSaveConfigMessage": "修改配置可能导致服务不可用,确定保存吗?", "saveAndRestart": "保存并重启", "saveOnly": "仅保存", "restoreBackup": "恢复备份", "backupRestored": "备份已恢复", "noBackupFound": "未找到备份文件", "restoreBackupFailed": "恢复备份失败:{error}", "@restoreBackupFailed": { "placeholders": { "error": { "type": "String" } } }, "restartingService": "正在重启OpenList服务...", "serviceRestartSuccess": "服务重启成功", "serviceRestartFailed": "服务重启失败,请手动重启", "serviceRestartOnlyAndroid": "服务重启仅支持Android系统" } ================================================ FILE: lib/main.dart ================================================ import 'package:openlist_mobile/generated/l10n.dart'; import 'package:openlist_mobile/pages/openlist/openlist.dart'; import 'package:openlist_mobile/pages/app_update_dialog.dart'; import 'package:openlist_mobile/pages/settings/settings.dart'; import 'package:openlist_mobile/pages/web/web.dart'; import 'package:openlist_mobile/pages/download_manager_page.dart'; import 'package:openlist_mobile/utils/download_manager.dart'; import 'package:openlist_mobile/utils/notification_manager.dart'; import 'package:openlist_mobile/utils/service_manager.dart'; import 'package:openlist_mobile/utils/language_controller.dart'; import 'package:fade_indexed_stack/fade_indexed_stack.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; import 'contant/native_bridge.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize language controller Get.put(LanguageController()); // Initialize notification manager await NotificationManager.initialize(); // Initialize service manager (supports both Android and iOS) await ServiceManager.instance.initialize(); // For iOS: Ensure service is started on first launch if (defaultTargetPlatform == TargetPlatform.iOS) { try { // Check if service is running final isRunning = await ServiceManager.instance.checkServiceStatus(); if (!isRunning) { // Start service automatically on iOS await ServiceManager.instance.startService(); } } catch (e) { debugPrint('Failed to start iOS service on launch: $e'); } } // Android WebView debugging if (!kIsWeb && kDebugMode && defaultTargetPlatform == TargetPlatform.android) { await InAppWebViewController.setWebContentsDebuggingEnabled(kDebugMode); } runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return GetBuilder( builder: (languageController) { // 如果语言控制器设置为跟随系统,则使用null让系统自动选择 // 否则使用指定的locale Locale? appLocale = languageController.locale; return GetMaterialApp( title: 'OpenList', themeMode: ThemeMode.system, theme: ThemeData( useMaterial3: true, colorSchemeSeed: Colors.blueGrey, inputDecorationTheme: const InputDecorationTheme( border: OutlineInputBorder(), ), ), darkTheme: ThemeData( useMaterial3: true, brightness: Brightness.dark, colorSchemeSeed: Colors.blueGrey, /* dark theme settings */ ), locale: appLocale, fallbackLocale: const Locale('en'), supportedLocales: S.delegate.supportedLocales, localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], home: const MyHomePage(title: ""), ); }, ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key, required this.title}); final String title; static const webPageIndex = 0; @override Widget build(BuildContext context) { final controller = Get.put(_MainController()); return Scaffold( body: Obx( () => FadeIndexedStack( lazy: true, index: controller.selectedIndex.value, children: [ WebScreen(key: webGlobalKey), const OpenListScreen(), const DownloadManagerPage(), const SettingsScreen() ], ), ), bottomNavigationBar: Obx(() => NavigationBar( destinations: [ NavigationDestination( icon: const Icon(Icons.preview), label: S.current.webPage, ), NavigationDestination( icon: SvgPicture.asset( "assets/openlist.svg", color: Theme.of(context).hintColor, width: 32, height: 32, ), label: S.current.appName, ), NavigationDestination( icon: const Icon(Icons.arrow_downward), label: _getDownloadLabel(), ), NavigationDestination( icon: const Icon(Icons.settings), label: S.current.settings, ), ], selectedIndex: controller.selectedIndex.value, onDestinationSelected: (int index) { // Web if (controller.selectedIndex.value == webPageIndex && controller.selectedIndex.value == webPageIndex) { webGlobalKey.currentState?.onClickNavigationBar(); } controller.setPageIndex(index); }))); } String _getDownloadLabel() { int activeCount = DownloadManager.activeTasks.length; if (activeCount > 0) { return S.current.downloadManagerWithCount(activeCount); } else { return S.current.downloadManager; } } } class _MainController extends GetxController { final selectedIndex = 1.obs; setPageIndex(int index) { selectedIndex.value = index; } @override void onInit() async { final webPage = await NativeBridge.appConfig.isAutoOpenWebPageEnabled(); if (webPage) { setPageIndex(MyHomePage.webPageIndex); } WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { if (await NativeBridge.appConfig.isAutoCheckUpdateEnabled()) { AppUpdateDialog.checkUpdateAndShowDialog(Get.context!, null); } }); super.onInit(); } } ================================================ FILE: lib/pages/app_update_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'dart:io' show Platform; import 'package:flutter_smooth_markdown/flutter_smooth_markdown.dart'; import '../generated/l10n.dart'; import '../utils/update_checker.dart'; import '../utils/app_store_update.dart'; import '../utils/intent_utils.dart'; import '../utils/download_manager.dart'; class AppUpdateDialog extends StatelessWidget { final String content; final String apkUrl; final String htmlUrl; final String version; const AppUpdateDialog( {super.key, required this.content, required this.apkUrl, required this.version, required this.htmlUrl}); static checkUpdateAndShowDialog( BuildContext context, ValueChanged? checkFinished) async { if (Platform.isIOS) { try { final hasNewVersion = await AppStoreUpdate.checkAndShowUpdate(); checkFinished?.call(hasNewVersion); } catch (_) { checkFinished?.call(false); } return; } final checker = UpdateChecker(owner: "openlistteam", repo: "OpenList-Mobile"); await checker.downloadData(); final hasNewVersion = await checker.hasNewVersion(); checkFinished?.call(hasNewVersion); if (hasNewVersion) { showDialog( context: context, barrierDismissible: false, barrierColor: Colors.black.withOpacity(0.5), builder: (context) { return AppUpdateDialog( content: checker.getUpdateContent(), apkUrl: checker.getApkDownloadUrl(), htmlUrl: checker.getHtmlUrl(), version: checker.getTag(), ); }, ); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final hasValidApkUrl = apkUrl.trim().isNotEmpty && Uri.tryParse(apkUrl) != null; return AlertDialog( title: Row( children: [ Icon( Icons.system_update, color: theme.colorScheme.primary, size: 28, ), const SizedBox(width: 12), Expanded( child: Text(S.of(context).newVersionFound), ), ], ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: theme.colorScheme.primaryContainer, borderRadius: BorderRadius.circular(20), ), child: Text( version, style: TextStyle( fontWeight: FontWeight.bold, color: theme.colorScheme.onPrimaryContainer, ), ), ), const SizedBox(height: 16), Card( margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.all(12), child: SmoothMarkdown( data: content, onTapLink: (url) { IntentUtils.getUrlIntent(url).launchChooser(url); }, ), ), ), const SizedBox(height: 16), Text( S.of(context).selectDownloadMethod, style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), if (Platform.isAndroid && hasValidApkUrl) ...[ Card( margin: EdgeInsets.zero, child: ListTile( leading: Icon( Icons.download, color: theme.colorScheme.primary, ), title: Text(S.of(context).directDownloadApk), subtitle: Text(S.of(context).directDownloadMethodDesc), trailing: const Icon(Icons.chevron_right), onTap: () async { Navigator.pop(context); DownloadManager.downloadFileInBackground( url: apkUrl, filename: 'OpenList_$version.apk', ); }, ), ), const SizedBox(height: 8), Card( margin: EdgeInsets.zero, child: ListTile( leading: Icon( Icons.open_in_browser, color: theme.colorScheme.secondary, ), title: Text(S.of(context).downloadApk), subtitle: Text(S.of(context).browserDownloadMethodDesc), trailing: const Icon(Icons.chevron_right), onTap: () { Navigator.pop(context); IntentUtils.getUrlIntent(apkUrl) .launchChooser(S.of(context).downloadApk); }, ), ), const SizedBox(height: 8), ], Card( margin: EdgeInsets.zero, child: ListTile( leading: Icon( Icons.article_outlined, color: theme.colorScheme.tertiary, ), title: Text(S.of(context).releasePage), trailing: const Icon(Icons.chevron_right), onTap: () { Navigator.pop(context); IntentUtils.getUrlIntent(htmlUrl) .launchChooser(S.of(context).releasePage); }, ), ), ], ), ), actions: [ TextButton( child: Text(S.of(context).cancel), onPressed: () { Navigator.pop(context); }, ), ], ); } } ================================================ FILE: lib/pages/download_manager_page.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:open_filex/open_filex.dart'; import 'package:open_file_manager/open_file_manager.dart'; import '../utils/download_manager.dart'; import '../generated/l10n.dart'; /// 下载文件管理页面 class DownloadManagerPage extends StatefulWidget { const DownloadManagerPage({Key? key}) : super(key: key); @override State createState() => _DownloadManagerPageState(); } class _DownloadManagerPageState extends State with TickerProviderStateMixin { List _downloadedFiles = []; bool _isLoading = true; String? _downloadPath; late TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _loadDownloadedFiles(); // 定期刷新活跃任务状态 _startPeriodicRefresh(); } @override void dispose() { _tabController.dispose(); super.dispose(); } void _startPeriodicRefresh() { // 每秒刷新一次活跃任务状态 Stream.periodic(const Duration(seconds: 1)).listen((_) { if (mounted && _tabController.index == 0) { setState(() {}); } }); } Future _loadDownloadedFiles() async { setState(() { _isLoading = true; }); try { _downloadedFiles = await DownloadManager.getDownloadedFiles(); _downloadPath = await DownloadManager.getDownloadDirectoryPath(); } catch (e) { print('${S.current.loadDownloadFilesFailed}: $e'); } setState(() { _isLoading = false; }); } String _formatFileSize(int bytes) { if (bytes < 1024) return '${bytes}B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}KB'; if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}MB'; return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)}GB'; } String _formatDateTime(DateTime dateTime) { return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ' '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'; } IconData _getFileIcon(String filename) { String extension = filename.split('.').last.toLowerCase(); switch (extension) { case 'pdf': return Icons.picture_as_pdf; case 'jpg': case 'jpeg': case 'png': case 'gif': return Icons.image; case 'mp4': case 'avi': case 'mkv': return Icons.video_file; case 'mp3': case 'wav': return Icons.audio_file; case 'zip': case 'rar': case '7z': return Icons.archive; case 'doc': case 'docx': return Icons.description; case 'xls': case 'xlsx': return Icons.table_chart; case 'apk': return Icons.android; default: return Icons.insert_drive_file; } } Color _getStatusColor(DownloadStatus status) { switch (status) { case DownloadStatus.pending: return Colors.orange; case DownloadStatus.downloading: return Colors.blue; case DownloadStatus.completed: return Colors.green; case DownloadStatus.failed: return Colors.red; case DownloadStatus.cancelled: return Colors.grey; } } Widget _buildActiveTasksTab() { List activeTasks = DownloadManager.activeTasks; if (activeTasks.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.download_outlined, size: 64, color: Colors.grey, ), const SizedBox(height: 16), Text( S.of(context).noActiveDownloads, style: const TextStyle( fontSize: 16, color: Colors.grey, ), ), ], ), ); } return ListView.builder( itemCount: activeTasks.length, itemBuilder: (context, index) { DownloadTask task = activeTasks[index]; return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( _getFileIcon(task.filename), size: 32, color: _getStatusColor(task.status), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( task.filename, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( task.statusText, style: TextStyle( fontSize: 14, color: _getStatusColor(task.status), ), ), ], ), ), if (task.status == DownloadStatus.downloading) IconButton( icon: const Icon(Icons.cancel, color: Colors.red), onPressed: () { _confirmCancelDownload(task); }, ), ], ), const SizedBox(height: 12), if (task.status == DownloadStatus.downloading) ...[ LinearProgressIndicator( value: task.progress, backgroundColor: Colors.grey[300], valueColor: AlwaysStoppedAnimation( _getStatusColor(task.status), ), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( task.progressText, style: const TextStyle(fontSize: 12, color: Colors.grey), ), Text( '${(task.progress * 100).toStringAsFixed(1)}%', style: const TextStyle(fontSize: 12, color: Colors.grey), ), ], ), ] else if (task.status == DownloadStatus.failed) ...[ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.red[50], borderRadius: BorderRadius.circular(4), ), child: Row( children: [ const Icon(Icons.error, color: Colors.red, size: 16), const SizedBox(width: 8), Expanded( child: Text( task.errorMessage ?? S.of(context).downloadFailed, style: const TextStyle( fontSize: 12, color: Colors.red, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ), ], const SizedBox(height: 8), Text( '${S.of(context).startTime}: ${_formatDateTime(task.startTime)}', style: const TextStyle(fontSize: 12, color: Colors.grey), ), ], ), ), ); }, ); } Widget _buildCompletedTasksTab() { List completedTasks = DownloadManager.completedTasks; if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (completedTasks.isEmpty && _downloadedFiles.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.download_done, size: 64, color: Colors.grey, ), const SizedBox(height: 16), Text( S.of(context).noCompletedDownloads, style: const TextStyle( fontSize: 16, color: Colors.grey, ), ), ], ), ); } return RefreshIndicator( onRefresh: _loadDownloadedFiles, child: ListView( children: [ // 显示任务记录中的已完成下载 ...completedTasks.map((task) => Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: ListTile( leading: Icon( _getFileIcon(task.filename), size: 32, color: _getStatusColor(task.status), ), title: Text( task.filename, maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( task.statusText, style: TextStyle(color: _getStatusColor(task.status)), ), if (task.endTime != null) Text('${S.of(context).completedTime}: ${_formatDateTime(task.endTime!)}'), if (task.totalBytes > 0) Text('${S.of(context).size}: ${_formatFileSize(task.totalBytes)}'), ], ), trailing: PopupMenuButton( onSelected: (value) { switch (value) { case 'open': if (task.status == DownloadStatus.completed) { _openFile(task.filePath); } break; case 'open_folder': if (task.status == DownloadStatus.completed) { _openFileManager(task.filePath); } break; case 'delete_record': _confirmDeleteTaskRecord(task); break; case 'delete_file': if (task.status == DownloadStatus.completed) { _confirmDeleteFile(task.filename); } break; } }, itemBuilder: (context) => [ if (task.status == DownloadStatus.completed) PopupMenuItem( value: 'open', child: Row( children: [ const Icon(Icons.open_in_new), const SizedBox(width: 8), Text(S.of(context).openFile), ], ), ), if (task.status == DownloadStatus.completed) PopupMenuItem( value: 'open_folder', child: Row( children: [ const Icon(Icons.folder_open), const SizedBox(width: 8), Text(S.of(context).showInFileManager), ], ), ), PopupMenuItem( value: 'delete_record', child: Row( children: [ const Icon(Icons.delete_outline), const SizedBox(width: 8), Text(S.of(context).deleteRecord), ], ), ), if (task.status == DownloadStatus.completed) PopupMenuItem( value: 'delete_file', child: Row( children: [ const Icon(Icons.delete, color: Colors.red), const SizedBox(width: 8), Text(S.of(context).deleteFile, style: const TextStyle(color: Colors.red)), ], ), ), ], ), ), )), // 显示文件系统中的其他下载文件 ..._downloadedFiles.where((file) { String filename = file.path.split('/').last; // 过滤掉已经在任务记录的文件 return !completedTasks.any((task) => task.filename == filename); }).map((file) { String filename = file.path.split('/').last; FileStat stat = file.statSync(); return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: ListTile( leading: Icon( _getFileIcon(filename), size: 32, color: Theme.of(context).primaryColor, ), title: Text( filename, maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('${S.of(context).size}: ${_formatFileSize(stat.size)}'), Text('${S.of(context).modifiedTime}: ${_formatDateTime(stat.modified)}'), ], ), trailing: const Icon(Icons.more_vert), onTap: () => _showFileOptions(file), ), ); }), ], ), ); } void _confirmCancelDownload(DownloadTask task) { showDialog( context: context, builder: (context) => AlertDialog( title: Text(S.of(context).cancelDownload), content: Text(S.of(context).confirmCancelDownload(task.filename)), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(S.of(context).continueDownload), ), TextButton( onPressed: () { Navigator.pop(context); DownloadManager.cancelDownload(task.id); setState(() {}); }, child: Text(S.of(context).cancelDownload, style: const TextStyle(color: Colors.red)), ), ], ), ); } void _confirmDeleteTaskRecord(DownloadTask task) { showDialog( context: context, builder: (context) => AlertDialog( title: Text(S.of(context).deleteRecord), content: Text(S.of(context).confirmDeleteRecord(task.filename)), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(S.of(context).cancel), ), TextButton( onPressed: () { Navigator.pop(context); DownloadManager.removeTask(task.id); setState(() {}); }, child: Text(S.of(context).delete, style: const TextStyle(color: Colors.red)), ), ], ), ); } void _showFileOptions(FileSystemEntity file) { String filename = file.path.split('/').last; showModalBottomSheet( context: context, builder: (context) => Container( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( filename, style: Theme.of(context).textTheme.titleMedium, maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 16), ListTile( leading: const Icon(Icons.open_in_new), title: Text(S.of(context).openFile), onTap: () async { Navigator.pop(context); await _openFile(file.path); }, ), ListTile( leading: const Icon(Icons.folder_open), title: Text(S.of(context).showInFileManager), onTap: () { Navigator.pop(context); _openFileManager(file.path); }, ), ListTile( leading: const Icon(Icons.share), title: Text(S.of(context).shareFile), onTap: () { Navigator.pop(context); // 这里可以添加分享功能 Get.showSnackbar(GetSnackBar( message: S.of(context).shareFeatureNotImplemented, duration: const Duration(seconds: 2), )); }, ), ListTile( leading: const Icon(Icons.info), title: Text(S.of(context).fileInfo), onTap: () { Navigator.pop(context); _showFileInfo(file); }, ), ListTile( leading: const Icon(Icons.delete, color: Colors.red), title: Text(S.of(context).deleteFile, style: const TextStyle(color: Colors.red)), onTap: () { Navigator.pop(context); _confirmDeleteFile(filename); }, ), ], ), ), ); } void _showFileInfo(FileSystemEntity file) { String filename = file.path.split('/').last; FileStat stat = file.statSync(); showDialog( context: context, builder: (context) => AlertDialog( title: Text(S.of(context).fileInfo), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('${S.of(context).fileName}: $filename'), const SizedBox(height: 8), Text('${S.of(context).size}: ${_formatFileSize(stat.size)}'), const SizedBox(height: 8), Text('${S.of(context).modifiedTime}: ${_formatDateTime(stat.modified)}'), const SizedBox(height: 8), Text('${S.of(context).filePath}: ${file.path}'), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(S.of(context).ok), ), ], ), ); } void _confirmDeleteFile(String filename) { showDialog( context: context, builder: (context) => AlertDialog( title: Text(S.of(context).confirmDelete), content: Text(S.of(context).confirmDeleteFile(filename)), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(S.of(context).cancel), ), TextButton( onPressed: () async { Navigator.pop(context); bool success = await DownloadManager.deleteFile(filename); if (success) { Get.showSnackbar(GetSnackBar( message: S.of(context).fileDeleted, duration: const Duration(seconds: 2), )); _loadDownloadedFiles(); // 刷新列表 } else { Get.showSnackbar(GetSnackBar( message: S.of(context).deleteFailed, duration: const Duration(seconds: 2), )); } }, child: Text(S.of(context).delete, style: const TextStyle(color: Colors.red)), ), ], ), ); } void _confirmClearAll() { showDialog( context: context, builder: (context) => AlertDialog( title: Text(S.of(context).confirmClear), content: Text(S.of(context).confirmClearAllFiles), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(S.of(context).cancel), ), TextButton( onPressed: () async { Navigator.pop(context); bool success = await DownloadManager.clearDownloadDirectory(); if (success) { DownloadManager.clearCompletedTasks(); Get.showSnackbar(GetSnackBar( message: S.of(context).cleared, duration: const Duration(seconds: 2), )); _loadDownloadedFiles(); // 刷新列表 setState(() {}); } else { Get.showSnackbar(GetSnackBar( message: S.of(context).clearFailed, duration: const Duration(seconds: 2), )); } }, child: Text(S.of(context).clear, style: const TextStyle(color: Colors.red)), ), ], ), ); } /// 打开文件 Future _openFile(String filePath) async { try { final result = await OpenFilex.open(filePath); switch (result.type) { case ResultType.done: // 文件成功打开,不需要额外提示 break; case ResultType.noAppToOpen: Get.showSnackbar(GetSnackBar( message: S.of(context).noAppToOpenFile, duration: const Duration(seconds: 3), mainButton: TextButton( onPressed: () { _showFileLocation(filePath); }, child: Text(S.of(context).viewLocation), ), )); break; case ResultType.fileNotFound: Get.showSnackbar(GetSnackBar( message: S.of(context).fileNotFound, duration: const Duration(seconds: 3), )); break; case ResultType.permissionDenied: Get.showSnackbar(GetSnackBar( message: S.of(context).noPermissionToOpenFile, duration: const Duration(seconds: 3), )); break; case ResultType.error: Get.showSnackbar(GetSnackBar( message: S.of(context).openFileFailed(result.message ?? ''), duration: const Duration(seconds: 3), mainButton: TextButton( onPressed: () { _showFileLocation(filePath); }, child: Text(S.of(context).viewLocation), ), )); break; } } catch (e) { Get.showSnackbar(GetSnackBar( message: S.of(context).openFileFailed(e.toString()), duration: const Duration(seconds: 3), mainButton: TextButton( onPressed: () { _showFileLocation(filePath); }, child: Text(S.of(context).viewLocation), ), )); } } /// 打开文件管理器并跳转到指定文件位置 Future _openFileManager(String filePath) async { try { // 获取文件所在目录 String directoryPath = filePath.substring(0, filePath.lastIndexOf('/')); // 尝试打开文件管理器并定位到文件 await openFileManager( androidConfig: AndroidConfig( folderType: AndroidFolderType.other, folderPath: directoryPath, ), iosConfig: IosConfig( folderPath: directoryPath, ), ); Get.showSnackbar(GetSnackBar( message: S.of(context).fileManagerOpened, duration: const Duration(seconds: 2), )); } catch (e) { print('打开文件管理器失败: $e'); Get.showSnackbar(GetSnackBar( message: S.of(context).openFileManagerFailed(e.toString()), duration: const Duration(seconds: 3), )); } } /// 打开下载目录 Future _openDownloadDirectory() async { if (_downloadPath != null) { try { await openFileManager( androidConfig: AndroidConfig( folderType: AndroidFolderType.other, folderPath: _downloadPath!, ), iosConfig: IosConfig( folderPath: _downloadPath!, ), ); Get.showSnackbar(GetSnackBar( message: S.of(context).downloadDirectoryOpened, duration: const Duration(seconds: 2), )); } catch (e) { print('打开下载目录失败: $e'); Get.showSnackbar(GetSnackBar( message: S.of(context).openDownloadDirectoryFailed(e.toString()), duration: const Duration(seconds: 3), )); } } else { Get.showSnackbar(GetSnackBar( message: S.of(context).downloadDirectoryPathUnknown, duration: const Duration(seconds: 2), )); } } /// 显示文件位置信息 void _showFileLocation(String filePath) { Get.dialog( AlertDialog( title: Text(S.of(context).fileLocation), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(S.of(context).fileSavedTo), const SizedBox(height: 8), SelectableText( filePath, style: const TextStyle(fontSize: 12), ), const SizedBox(height: 16), Text( S.of(context).fileLocationTip, style: const TextStyle(fontSize: 14, color: Colors.grey), ), ], ), actions: [ TextButton( onPressed: () => Get.back(), child: Text(S.of(context).cancel), ), TextButton( onPressed: () { Get.back(); _openFileManager(filePath); }, child: Text(S.of(context).openFileManager), ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(S.of(context).downloadManager), bottom: TabBar( controller: _tabController, tabs: [ Tab( icon: const Icon(Icons.downloading), text: '${S.of(context).inProgress} (${DownloadManager.activeTasks.length})', ), Tab( icon: const Icon(Icons.download_done), text: '${S.of(context).completed} (${DownloadManager.completedTasks.length + _downloadedFiles.length})', ), ], ), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: () { _loadDownloadedFiles(); setState(() {}); }, ), PopupMenuButton( onSelected: (value) { switch (value) { case 'clear_records': DownloadManager.clearCompletedTasks(); setState(() {}); Get.showSnackbar(GetSnackBar( message: S.of(context).downloadRecordsCleared, duration: const Duration(seconds: 2), )); break; case 'clear_all': _confirmClearAll(); break; case 'open_folder': _openDownloadDirectory(); break; } }, itemBuilder: (context) => [ PopupMenuItem( value: 'open_folder', child: Row( children: [ const Icon(Icons.folder_open), const SizedBox(width: 8), Text(S.of(context).openDirectory), ], ), ), PopupMenuItem( value: 'clear_records', child: Row( children: [ const Icon(Icons.clear_all), const SizedBox(width: 8), Text(S.of(context).clearRecords), ], ), ), PopupMenuItem( value: 'clear_all', child: Row( children: [ const Icon(Icons.delete_forever, color: Colors.red), const SizedBox(width: 8), Text(S.of(context).clearAll, style: const TextStyle(color: Colors.red)), ], ), ), ], ), ], ), body: TabBarView( controller: _tabController, children: [ _buildActiveTasksTab(), _buildCompletedTasksTab(), ], ), ); } } ================================================ FILE: lib/pages/openlist/about_dialog.dart ================================================ import 'dart:ffi'; import 'package:openlist_mobile/contant/native_bridge.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; import '../../generated/l10n.dart'; import '../../generated_api.dart'; import '../../utils/intent_utils.dart'; class AppAboutDialog extends StatefulWidget { const AppAboutDialog({super.key}); @override State createState() { return _AppAboutDialogState(); } } class _AppAboutDialogState extends State { String _openlistVersion = ""; String _version = ""; int _versionCode = 0; Future updateVer() async { _openlistVersion = await Android().getOpenListVersion(); _version = await NativeBridge.common.getVersionName(); _versionCode = await NativeBridge.common.getVersionCode(); return null; } @override void initState() { updateVer().then((value) => setState(() {})); super.initState(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final openlistUrl = "https://github.com/OpenListTeam/OpenList/releases/tag/$_openlistVersion"; final appUrl = "https://github.com/OpenListTeam/OpenList-Mobile/releases/tag/$_version"; return Dialog( child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ SvgPicture.asset( "assets/openlist.svg", width: 72, height: 72, ), const SizedBox(height: 16), Text( S.of(context).appName, style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( '$_version ($_versionCode)', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 24), Align( alignment: Alignment.centerLeft, child: Text( S.of(context).about, style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 8), Card( margin: EdgeInsets.zero, child: ListTile( leading: Icon( Icons.folder_open, color: theme.colorScheme.primary, ), title: Text(S.of(context).openlist), subtitle: Text(_openlistVersion.isNotEmpty ? _openlistVersion : S.of(context).about), trailing: const Icon(Icons.open_in_new, size: 20), onTap: () { IntentUtils.getUrlIntent(openlistUrl).launchChooser(S.of(context).openlist); }, onLongPress: () { Clipboard.setData(ClipboardData(text: openlistUrl)); Get.showSnackbar(GetSnackBar( message: S.of(context).copiedToClipboard, duration: const Duration(seconds: 1), )); }, ), ), const SizedBox(height: 8), Card( margin: EdgeInsets.zero, child: ListTile( leading: Icon( Icons.phone_android, color: theme.colorScheme.secondary, ), title: Text(S.of(context).openlistMobile), subtitle: Text(_version), trailing: const Icon(Icons.open_in_new, size: 20), onTap: () { IntentUtils.getUrlIntent(appUrl).launchChooser(S.of(context).openlistMobile); }, onLongPress: () { Clipboard.setData(ClipboardData(text: appUrl)); Get.showSnackbar(GetSnackBar( message: S.of(context).copiedToClipboard, duration: const Duration(seconds: 1), )); }, ), ), const SizedBox(height: 8), Card( margin: EdgeInsets.zero, child: ListTile( leading: Icon( Icons.description_outlined, color: theme.colorScheme.tertiary, ), title: Text(S.of(context).openSourceLicenses), subtitle: Text(S.of(context).viewThirdPartyLicenses), trailing: const Icon(Icons.chevron_right), onTap: () { showLicensePage( context: context, applicationName: S.of(context).appName, applicationVersion: '$_version ($_versionCode)', applicationIcon: Padding( padding: const EdgeInsets.all(8.0), child: SvgPicture.asset( "assets/openlist.svg", width: 48, height: 48, ), ), ); }, ), ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: FilledButton.tonal( onPressed: () => Navigator.pop(context), child: Text(S.of(context).ok), ), ), ], ), ), ), ); } } ================================================ FILE: lib/pages/openlist/config_editor_page.dart ================================================ import 'dart:io'; import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_highlight/flutter_highlight.dart'; import 'package:flutter_highlight/themes/monokai-sublime.dart'; import 'package:flutter_highlight/themes/github.dart'; import 'package:get/get.dart'; import '../../contant/native_bridge.dart'; import '../../generated/l10n.dart'; import '../../utils/service_manager.dart'; /// Config.json Editor with validation, backup, and real-time syntax checking class ConfigEditorPage extends StatefulWidget { const ConfigEditorPage({Key? key}) : super(key: key); @override State createState() => _ConfigEditorPageState(); } class _ConfigEditorPageState extends State { final TextEditingController _controller = TextEditingController(); String _filePath = ''; String _backupFilePath = ''; bool _isLoading = true; bool _isPreview = false; String? _errorMessage; String? _jsonErrorMessage; int? _jsonErrorLine; Timer? _debounceTimer; @override void initState() { super.initState(); _loadConfigFile(); // Real-time JSON validation _controller.addListener(_validateJson); } /// Real-time JSON syntax validation with debounce void _validateJson() { if (_isPreview) return; // Skip validation in preview mode // Cancel previous timer to implement debounce _debounceTimer?.cancel(); // Create new timer with 300ms delay _debounceTimer = Timer(const Duration(milliseconds: 300), () { final text = _controller.text.trim(); if (text.isEmpty) { if (mounted) { setState(() { _jsonErrorMessage = null; _jsonErrorLine = null; }); } return; } try { jsonDecode(text); if (mounted) { setState(() { _jsonErrorMessage = null; _jsonErrorLine = null; }); } } on FormatException catch (e) { if (mounted) { // Extract line number from error message final match = RegExp(r'line (\d+)').firstMatch(e.message); setState(() { _jsonErrorMessage = e.message; _jsonErrorLine = match != null ? int.tryParse(match.group(1) ?? '') : null; }); } } }); } /// Load config file with permission checking Future _loadConfigFile() async { try { setState(() { _isLoading = true; _errorMessage = null; }); final dataDir = await NativeBridge.appConfig.getDataDir(); _filePath = '$dataDir/config.json'; _backupFilePath = '$dataDir/config.json.backup'; final file = File(_filePath); if (await file.exists()) { _controller.text = await file.readAsString(); } else { _controller.text = '{\n \n}'; if (mounted) { setState(() { _errorMessage = S.of(context).fileNotFoundWillCreateOnSave; }); } } } on FileSystemException catch (e) { if (mounted) { setState(() { _errorMessage = e.osError?.errorCode == 13 ? S.of(context).filePermissionDenied : S.of(context).loadFailed(e.message); }); } } catch (e) { if (mounted) { setState(() { _errorMessage = S.of(context).loadFailed(e.toString()); }); } } finally { if (mounted) { setState(() => _isLoading = false); } } } /// Restore from backup file Future _restoreBackup() async { try { final backupFile = File(_backupFilePath); if (!await backupFile.exists()) { if (mounted) { Get.showSnackbar(GetSnackBar( message: S.of(context).noBackupFound, duration: const Duration(seconds: 2), )); } return; } final backupContent = await backupFile.readAsString(); setState(() { _controller.text = backupContent; }); if (mounted) { Get.showSnackbar(GetSnackBar( message: S.of(context).backupRestored, duration: const Duration(seconds: 2), )); } } catch (e) { if (mounted) { Get.showSnackbar(GetSnackBar( message: S.of(context).restoreBackupFailed(e.toString()), duration: const Duration(seconds: 2), )); } } } /// Show confirmation dialog before saving /// Provides three options: Cancel, Save Only, Save and Restart Future _showSaveConfirmation() async { final result = await showDialog( context: context, builder: (context) => AlertDialog( title: Text(S.of(context).confirmSaveConfigTitle), content: Text(S.of(context).confirmSaveConfigMessage), actions: [ TextButton( onPressed: () => Navigator.of(context).pop('cancel'), child: Text(S.of(context).cancel), ), TextButton( onPressed: () => Navigator.of(context).pop('save'), child: Text(S.of(context).saveOnly), ), FilledButton( onPressed: () => Navigator.of(context).pop('save_restart'), child: Text(S.of(context).saveAndRestart), ), ], ), ); if (result == 'save' || result == 'save_restart') { final saveSuccess = await _saveConfigFile(); // Restart service if requested and save was successful if (saveSuccess && result == 'save_restart') { await _restartOpenListService(); } } } /// Restart OpenList service after config changes /// Calls ServiceManager.instance.restartService() to stop and start the service /// Only works on Android platform Future _restartOpenListService() async { if (!Platform.isAndroid) { if (mounted) { Get.showSnackbar(GetSnackBar( message: S.of(context).serviceRestartOnlyAndroid, duration: const Duration(seconds: 2), )); } return; } try { // Show loading indicator if (mounted) { Get.showSnackbar(GetSnackBar( message: S.of(context).restartingService, duration: const Duration(seconds: 2), showProgressIndicator: true, )); } // Restart service via ServiceManager final success = await ServiceManager.instance.restartService(); if (mounted) { if (success) { Get.showSnackbar(GetSnackBar( message: S.of(context).serviceRestartSuccess, duration: const Duration(seconds: 3), )); } else { Get.showSnackbar(GetSnackBar( message: S.of(context).serviceRestartFailed, duration: const Duration(seconds: 3), backgroundColor: Colors.orange, )); } } } catch (e) { if (mounted) { Get.showSnackbar(GetSnackBar( message: S.of(context).saveFailed(e.toString()), duration: const Duration(seconds: 3), backgroundColor: Colors.red, )); } } } /// Save config file with JSON validation and backup mechanism /// Returns true if save was successful, false otherwise Future _saveConfigFile() async { final text = _controller.text.trim(); // Validate JSON format before saving try { jsonDecode(text); } on FormatException catch (e) { if (mounted) { final match = RegExp(r'line (\d+)').firstMatch(e.message); final line = match != null ? int.tryParse(match.group(1) ?? '') : null; Get.showSnackbar(GetSnackBar( message: line != null ? S.of(context).invalidJsonFormat(line, e.message) : S.of(context).saveFailed(e.message), duration: const Duration(seconds: 3), backgroundColor: Colors.red, )); } return false; } File? backupFile; try { final file = File(_filePath); // Create backup before saving if (await file.exists()) { backupFile = File(_backupFilePath); await file.copy(_backupFilePath); } // Ensure parent directory exists await file.parent.create(recursive: true); // Write new config await file.writeAsString(text); if (mounted) { Get.showSnackbar(GetSnackBar( message: S.of(context).saved, duration: const Duration(seconds: 2), )); } return true; } on FileSystemException catch (e) { // Restore backup on failure if (backupFile != null && await backupFile.exists()) { try { await backupFile.copy(_filePath); } catch (_) {} } if (mounted) { final errorMsg = e.osError?.errorCode == 13 ? S.of(context).filePermissionDenied : S.of(context).saveFailed(e.message); Get.showSnackbar(GetSnackBar( message: errorMsg, duration: const Duration(seconds: 3), backgroundColor: Colors.red, )); } return false; } catch (e) { // Restore backup on failure if (backupFile != null && await backupFile.exists()) { try { await backupFile.copy(_filePath); } catch (_) {} } if (mounted) { Get.showSnackbar(GetSnackBar( message: S.of(context).saveFailed(e.toString()), duration: const Duration(seconds: 3), backgroundColor: Colors.red, )); } return false; } } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( appBar: AppBar( title: const Text('config.json'), actions: [ // Restore backup button IconButton( icon: const Icon(Icons.restore), onPressed: _restoreBackup, tooltip: S.of(context).restoreBackup, ), // Toggle preview/edit mode IconButton( icon: Icon(_isPreview ? Icons.edit : Icons.visibility), onPressed: () => setState(() => _isPreview = !_isPreview), tooltip: _isPreview ? S.of(context).edit : S.of(context).preview, ), // Reload file IconButton( icon: const Icon(Icons.refresh), onPressed: _loadConfigFile, tooltip: S.of(context).refresh, ), // Save with confirmation IconButton( icon: const Icon(Icons.save), onPressed: _showSaveConfirmation, tooltip: S.of(context).save, ), ], ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Warning message banner if (_errorMessage != null) Container( color: Colors.orange.withOpacity(0.2), padding: const EdgeInsets.all(8), child: Row( children: [ const Icon(Icons.warning, color: Colors.orange, size: 20), const SizedBox(width: 8), Expanded( child: Text(_errorMessage!, style: const TextStyle(color: Colors.orange, fontSize: 12)), ), ], ), ), // File path display Container( color: Theme.of(context).colorScheme.surfaceContainerHighest, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Text(_filePath, style: Theme.of(context).textTheme.bodySmall), ), // Editor or preview Expanded( child: _isPreview ? SingleChildScrollView( padding: const EdgeInsets.all(16), child: HighlightView( _controller.text, language: 'json', theme: isDark ? monokaiSublimeTheme : githubTheme, textStyle: const TextStyle( fontFamily: 'monospace', fontSize: 14, ), ), ) : TextField( controller: _controller, maxLines: null, expands: true, style: const TextStyle( fontFamily: 'monospace', fontSize: 14, ), decoration: const InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.all(16), ), ), ), // JSON syntax error display at bottom if (_jsonErrorMessage != null && !_isPreview) Container( color: Colors.red.withOpacity(0.1), padding: const EdgeInsets.all(12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(Icons.error, color: Colors.red, size: 20), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ if (_jsonErrorLine != null) Text( 'Line $_jsonErrorLine', style: const TextStyle( color: Colors.red, fontWeight: FontWeight.bold, fontSize: 12, ), ), Text( _jsonErrorMessage!, style: const TextStyle( color: Colors.red, fontSize: 11, ), ), ], ), ), ], ), ), ], ), ); } @override void dispose() { _debounceTimer?.cancel(); _controller.dispose(); super.dispose(); } } ================================================ FILE: lib/pages/openlist/log_level_view.dart ================================================ import 'package:flutter/cupertino.dart'; import '../../contant/log_level.dart'; class LogLevelView extends StatefulWidget { final int level; const LogLevelView({super.key, required this.level}); @override State createState() => _LogLevelViewState(); } class _LogLevelViewState extends State { @override Widget build(BuildContext context) { final s = LogLevel.toStr(widget.level); final c = LogLevel.toColor(widget.level); return Text(s, style: TextStyle(color: c)); } } ================================================ FILE: lib/pages/openlist/log_list_view.dart ================================================ import 'package:openlist_mobile/pages/openlist/log_level_view.dart'; import 'package:flutter/material.dart'; class Log { final int level; final String time; final String content; Log(this.level, this.time, this.content); } class LogListView extends StatefulWidget { const LogListView({Key? key, required this.logs, this.controller}) : super(key: key); final List logs; final ScrollController? controller; @override State createState() => _LogListViewState(); } class _LogListViewState extends State { @override Widget build(BuildContext context) { return ListView.builder( itemCount: widget.logs.length, controller: widget.controller, itemBuilder: (context, index) { final log = widget.logs[index]; return ListTile( dense: true, title: SelectableText(log.content), subtitle: SelectableText(log.time), leading: LogLevelView(level: log.level), ); }, ); } } ================================================ FILE: lib/pages/openlist/openlist.dart ================================================ import 'dart:io'; import 'package:openlist_mobile/generated_api.dart'; import 'package:openlist_mobile/pages/openlist/about_dialog.dart'; import 'package:openlist_mobile/pages/openlist/pwd_edit_dialog.dart'; import 'package:openlist_mobile/pages/openlist/config_editor_page.dart'; import 'package:openlist_mobile/pages/app_update_dialog.dart'; import 'package:openlist_mobile/widgets/switch_floating_action_button.dart'; import 'package:openlist_mobile/utils/service_manager.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../generated/l10n.dart'; import 'log_list_view.dart'; class OpenListScreen extends StatelessWidget { const OpenListScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final ui = Get.put(OpenListController()); return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.primaryContainer, title: Obx(() => Text("OpenList - ${ui.openlistVersion.value}")), actions: [ IconButton( tooltip: S.current.setAdminPassword, onPressed: () { showDialog( context: context, builder: (context) => PwdEditDialog(onConfirm: (pwd) async { try { await Android().setAdminPwd(pwd); Get.showSnackbar(GetSnackBar( title: S.current.setAdminPassword, message: pwd, duration: const Duration(seconds: 1))); } catch (e) { Get.showSnackbar(GetSnackBar( title: S.current.setAdminPassword, message: 'Error: $e', duration: const Duration(seconds: 2))); } })); }, icon: const Icon(Icons.password), ), IconButton( tooltip: S.of(context).editOpenListConfig, onPressed: () { Get.to(() => const ConfigEditorPage()); }, icon: const Icon(Icons.edit_note), ), // Desktop shortcut is only available on Android if (Platform.isAndroid) IconButton( tooltip: S.of(context).desktopShortcut, onPressed: () async { try { await Android().addShortcut(); Get.showSnackbar(GetSnackBar( message: S.of(context).desktopShortcut, duration: const Duration(seconds: 1))); } catch (e) { Get.showSnackbar(GetSnackBar( message: 'Error: $e', duration: const Duration(seconds: 2))); } }, icon: const Icon(Icons.add_home), ), PopupMenuButton( tooltip: S.of(context).moreOptions, itemBuilder: (context) { return [ PopupMenuItem( value: 1, onTap: () async { AppUpdateDialog.checkUpdateAndShowDialog(context, (b) { if (!b) { Get.showSnackbar(GetSnackBar( message: S.of(context).currentIsLatestVersion, duration: const Duration(seconds: 2))); } }); }, child: Text(S.of(context).checkForUpdates), ), PopupMenuItem( value: 2, onTap: () { showDialog(context: context, builder: ((context){ return const AppAboutDialog(); })); }, child: Text(S.of(context).about), ), ]; }, icon: const Icon(Icons.more_vert), ) ]), floatingActionButton: Obx( () => SwitchFloatingButton( isSwitch: ui.isSwitch.value, onSwitchChange: (s) async { ui.clearLog(); if (s) { // 启动服务 await ServiceManager.instance.startService(); } else { // 停止服务 await ServiceManager.instance.stopService(); } }), ), body: Obx(() => LogListView(logs: ui.logs, controller: ui.scrollController))); } } class MyEventReceiver extends Event { Function(Log log) logCb; Function(bool isRunning) statusCb; MyEventReceiver(this.statusCb, this.logCb); @override void onServiceStatusChanged(bool isRunning) { statusCb(isRunning); } @override void onServerLog(int level, String time, String log) { logCb(Log(level, time, log)); } } class OpenListController extends GetxController { final ScrollController scrollController = ScrollController(); var isSwitch = false.obs; var openlistVersion = "".obs; var logs = [].obs; void clearLog() { logs.clear(); } void addLog(Log log) { logs.add(log); if (scrollController.hasClients) { scrollController.jumpTo(scrollController.position.maxScrollExtent); } } @override void onInit() { // 设置日志接收器,但状态变化只通过ServiceManager处理 Event.setUp(MyEventReceiver( (isRunning) { // 不在这里更新状态,避免冲突 print('Event receiver status: $isRunning'); }, (log) => addLog(log))); Android().getOpenListVersion().then((value) => openlistVersion.value = value); // 获取初始状态 ServiceManager.instance.checkServiceStatus().then((isRunning) async { isSwitch.value = isRunning; if (Platform.isIOS && !isRunning) { await ServiceManager.instance.startService(); } }); // 只监听ServiceManager的状态变化 ServiceManager.instance.serviceStatusStream.listen((isRunning) { print('ServiceManager status changed: $isRunning'); isSwitch.value = isRunning; }); super.onInit(); } } ================================================ FILE: lib/pages/openlist/pwd_edit_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../generated/l10n.dart'; class PwdEditDialog extends StatefulWidget { final ValueChanged onConfirm; const PwdEditDialog({super.key, required this.onConfirm}); @override State createState() { return _PwdEditDialogState(); } } class _PwdEditDialogState extends State with SingleTickerProviderStateMixin { final TextEditingController pwdController = TextEditingController(); @override void dispose() { pwdController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: Text(S.of(context).modifyAdminPassword), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: pwdController, decoration: const InputDecoration( labelText: "admin密码", ), ), ], ), actions: [ TextButton( onPressed: () {Get.back();}, child: Text(S.of(context).cancel), ), FilledButton( onPressed: () { Get.back(); widget.onConfirm(pwdController.text); }, child: Text(S.of(context).confirm), ), ], ); } } ================================================ FILE: lib/pages/settings/preference_widgets.dart ================================================ import 'package:flutter/material.dart'; class DividerPreference extends StatelessWidget { const DividerPreference({super.key, required this.title}); final String title; @override Widget build(BuildContext context) { return Column(children: [ const Divider( height: 1, ), Container( alignment: Alignment.center, padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), child: Text( title, style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Theme.of(context).primaryColor), ), ), ]); } } class BasicPreference extends StatelessWidget { final String title; final String subtitle; final Widget? leading; final Widget? trailing; final GestureTapCallback? onTap; const BasicPreference({ super.key, required this.title, required this.subtitle, this.onTap, this.leading, this.trailing, }); @override Widget build(BuildContext context) { return ListTile( title: Text(title), subtitle: Text(subtitle), leading: leading, trailing: trailing, onTap: onTap, ); } } class SwitchPreference extends StatelessWidget { const SwitchPreference({ super.key, required this.title, required this.subtitle, this.icon, required this.value, required this.onChanged, }); final String title; final String subtitle; final Widget? icon; final bool value; final ValueChanged onChanged; @override Widget build(BuildContext context) { return BasicPreference( title: title, subtitle: subtitle, leading: icon, trailing: Switch(value: value, onChanged: onChanged), onTap: () { onChanged(!value); }, ); } } ================================================ FILE: lib/pages/settings/settings.dart ================================================ import 'dart:io'; import 'package:openlist_mobile/contant/native_bridge.dart'; import 'package:openlist_mobile/generated_api.dart'; import 'package:openlist_mobile/pages/settings/preference_widgets.dart'; import 'package:openlist_mobile/pages/settings/troubleshooting_page.dart'; import 'package:openlist_mobile/utils/language_controller.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../generated/l10n.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({Key? key}) : super(key: key); @override State createState() { return _SettingsScreenState(); } } class _SettingsScreenState extends State { late AppLifecycleListener _lifecycleListener; @override void initState() { _lifecycleListener = AppLifecycleListener( onResume: () async { final controller = Get.put(_SettingsController()); controller.updateData(); }, ); super.initState(); } @override void dispose() { _lifecycleListener.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final controller = Get.put(_SettingsController()); return Scaffold( body: Obx( () => ListView( children: [ // SizedBox(height: MediaQuery.of(context).padding.top), // Android-specific permission requests if (Platform.isAndroid) ...[ Visibility( visible: !controller._managerStorageGranted.value || !controller._notificationGranted.value || !controller._storageGranted.value, child: DividerPreference(title: S.of(context).importantSettings), ), Visibility( visible: !controller._managerStorageGranted.value, child: BasicPreference( title: S.of(context).grantManagerStoragePermission, subtitle: S.of(context).grantStoragePermissionDesc, onTap: () { Permission.manageExternalStorage.request(); }, ), ), Visibility( visible: !controller._storageGranted.value, child: BasicPreference( title: S.of(context).grantStoragePermission, subtitle: S.of(context).grantStoragePermissionDesc, onTap: () { Permission.storage.request(); }, )), Visibility( visible: !controller._notificationGranted.value, child: BasicPreference( title: S.of(context).grantNotificationPermission, subtitle: S.of(context).grantNotificationPermissionDesc, onTap: () { Permission.notification.request(); }, )), ], // End of Android-specific permissions DividerPreference(title: S.of(context).general), // Language Settings BasicPreference( title: S.of(context).language, subtitle: _getLanguageDisplayName(), leading: const Icon(Icons.language), onTap: () { _showLanguageSelectionDialog(context); }, ), SwitchPreference( title: S.of(context).autoCheckForUpdates, subtitle: S.of(context).autoCheckForUpdatesDesc, icon: const Icon(Icons.system_update), value: controller.autoUpdate, onChanged: (value) { controller.autoUpdate = value; }, ), SwitchPreference( title: S.of(context).wakeLock, subtitle: S.of(context).wakeLockDesc, icon: const Icon(Icons.screen_lock_portrait), value: controller.wakeLock, onChanged: (value) { controller.wakeLock = value; }, ), SwitchPreference( title: S.of(context).bootAutoStartService, subtitle: S.of(context).bootAutoStartServiceDesc, icon: const Icon(Icons.power_settings_new), value: controller.startAtBoot, onChanged: (value) { controller.startAtBoot = value; }, ), // AutoStartWebPage SwitchPreference( title: S.of(context).autoStartWebPage, subtitle: S.of(context).autoStartWebPageDesc, icon: const Icon(Icons.open_in_browser), value: controller._autoStartWebPage.value, onChanged: (value) { controller.autoStartWebPage = value; }, ), // Data directory setting - only for Android if (Platform.isAndroid) BasicPreference( title: S.of(context).dataDirectory, subtitle: controller._dataDir.value, leading: const Icon(Icons.folder), onTap: () async { final path = await FilePicker.platform.getDirectoryPath(); if (path == null) { Get.showSnackbar(GetSnackBar( message: S.current.setDefaultDirectory, duration: const Duration(seconds: 3), mainButton: TextButton( onPressed: () { controller.setDataDir(""); Get.back(); }, child: Text(S.current.confirm), ))); } else { controller.setDataDir(path); } }, ), DividerPreference(title: S.of(context).uiSettings), SwitchPreference( icon: const Icon(Icons.pan_tool_alt_outlined), title: S.of(context).silentJumpApp, subtitle: S.of(context).silentJumpAppDesc, value: controller._silentJumpApp.value, onChanged: (value) { controller.silentJumpApp = value; }), BasicPreference( title: S.of(context).troubleshooting, subtitle: S.of(context).troubleshootingDesc, leading: const Icon(Icons.help_outline), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => const TroubleshootingPage(), ), ); }, ), ], ), )); } String _getLanguageDisplayName() { final languageController = Get.find(); final currentOption = languageController.currentLanguageOption; switch (currentOption.name) { case 'followSystem': return S.of(context).followSystem; case 'simplifiedChinese': return S.of(context).simplifiedChinese; case 'english': return S.of(context).english; default: return currentOption.name; } } void _showLanguageSelectionDialog(BuildContext context) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(S.of(context).languageSettings), content: SingleChildScrollView( child: LanguageSelector( onLanguageChanged: () { Navigator.of(context).pop(); setState(() {}); // 刷新界面以显示新的语言设置 }, ), ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text(S.of(context).cancel), ), ], ); }, ); } } class _SettingsController extends GetxController { final _dataDir = "".obs; final _autoUpdate = true.obs; final _managerStorageGranted = true.obs; final _notificationGranted = true.obs; final _storageGranted = true.obs; setDataDir(String value) async { NativeBridge.appConfig.setDataDir(value); _dataDir.value = await NativeBridge.appConfig.getDataDir(); } get dataDir => _dataDir.value; set autoUpdate(value) => { _autoUpdate.value = value, NativeBridge.appConfig.setAutoCheckUpdateEnabled(value) }; get autoUpdate => _autoUpdate.value; final _wakeLock = true.obs; set wakeLock(value) => { _wakeLock.value = value, NativeBridge.appConfig.setWakeLockEnabled(value) }; get wakeLock => _wakeLock.value; final _autoStart = true.obs; set startAtBoot(value) => { _autoStart.value = value, NativeBridge.appConfig.setStartAtBootEnabled(value) }; get startAtBoot => _autoStart.value; final _autoStartWebPage = false.obs; set autoStartWebPage(value) => { _autoStartWebPage.value = value, NativeBridge.appConfig.setAutoOpenWebPageEnabled(value) }; get autoStartWebPage => _autoStartWebPage.value; final _silentJumpApp = false.obs; get silentJumpApp => _silentJumpApp.value; set silentJumpApp(value) => { _silentJumpApp.value = value, NativeBridge.appConfig.setSilentJumpAppEnabled(value) }; @override void onInit() async { updateData(); super.onInit(); } void updateData() async { final cfg = AppConfig(); cfg.isAutoCheckUpdateEnabled().then((value) => autoUpdate = value); cfg.isWakeLockEnabled().then((value) => wakeLock = value); cfg.isStartAtBootEnabled().then((value) => startAtBoot = value); cfg.isAutoOpenWebPageEnabled().then((value) => autoStartWebPage = value); cfg.isSilentJumpAppEnabled().then((value) => silentJumpApp = value); _dataDir.value = await cfg.getDataDir(); final sdk = await NativeBridge.common.getDeviceSdkInt(); // A11 if (sdk >= 30) { _managerStorageGranted.value = await Permission.manageExternalStorage.isGranted; } else { _managerStorageGranted.value = true; _storageGranted.value = await Permission.storage.isGranted; } // A12 if (sdk >= 32) { _notificationGranted.value = await Permission.notification.isGranted; } else { _notificationGranted.value = true; } } } ================================================ FILE: lib/pages/settings/troubleshooting_page.dart ================================================ import 'package:flutter/material.dart'; import '../../generated/l10n.dart'; class TroubleshootingPage extends StatelessWidget { const TroubleshootingPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(S.of(context).troubleshooting), ), body: ListView( padding: const EdgeInsets.all(16.0), children: [ _buildIssueCard( context, icon: Icons.power_settings_new, title: S.of(context).autoStartIssue, description: S.of(context).autoStartIssueDesc, ), _buildIssueCard( context, icon: Icons.storage, title: S.of(context).databaseNotSavedIssue, description: S.of(context).databaseNotSavedIssueDesc, ), ], ), ); } Widget _buildIssueCard( BuildContext context, { required IconData icon, required String title, required String description, }) { return Card( margin: const EdgeInsets.only(bottom: 16.0), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( icon, size: 28, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 12), Expanded( child: Text( title, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 12), Text( description, style: Theme.of(context).textTheme.bodyMedium, ), ], ), ), ); } } ================================================ FILE: lib/pages/web/web.dart ================================================ import 'dart:developer'; import 'dart:io' show Platform; import 'package:openlist_mobile/contant/native_bridge.dart'; import 'package:openlist_mobile/generated_api.dart'; import 'package:openlist_mobile/utils/intent_utils.dart'; import 'package:openlist_mobile/utils/download_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:get/get.dart'; import '../../generated/l10n.dart'; GlobalKey webGlobalKey = GlobalKey(); class WebScreen extends StatefulWidget { const WebScreen({Key? key}) : super(key: key); @override State createState() { return WebScreenState(); } } class WebScreenState extends State with WidgetsBindingObserver { InAppWebViewController? _webViewController; InAppWebViewSettings settings = InAppWebViewSettings( allowsInlineMediaPlayback: true, allowBackgroundAudioPlaying: true, iframeAllowFullscreen: true, javaScriptEnabled: true, mediaPlaybackRequiresUserGesture: false, useShouldOverrideUrlLoading: true, // iOS specific: Enable page caching and state preservation cacheEnabled: true, sharedCookiesEnabled: true, limitsNavigationsToAppBoundDomains: Platform.isIOS, // Enable disk and memory cache for better state preservation cacheMode: CacheMode.LOAD_DEFAULT, // Prevent WebView from being suspended in background allowsBackForwardNavigationGestures: true, // iOS: Suppress rendering until content is loaded suppressesIncrementalRendering: false, ); double _progress = 0; String _url = "http://localhost:5244"; bool _canGoBack = false; bool _isLoading = false; static const Set _inAppSafeSchemes = { "about", "data", "file", }; bool _isLoopbackHost(String host) { final normalized = host.toLowerCase(); return normalized == "localhost" || normalized == "127.0.0.1" || normalized == "::1"; } bool _isAllowedInAppNavigation(Uri uri) { final scheme = uri.scheme.toLowerCase(); if (_inAppSafeSchemes.contains(scheme)) { return true; } if (scheme == "http" || scheme == "https") { if (!Platform.isIOS) { return true; } return _isLoopbackHost(uri.host); } return false; } Future _openExternalUri(String uriString) async { final silentMode = await NativeBridge.appConfig.isSilentJumpAppEnabled(); if (silentMode) { NativeCommon().startActivityFromUri(uriString); return; } if (!mounted) return; Get.showSnackbar(GetSnackBar( message: S.current.jumpToOtherApp, duration: const Duration(seconds: 5), mainButton: TextButton( onPressed: () { NativeCommon().startActivityFromUri(uriString); }, child: Text(S.current.goTo), ))); } onClickNavigationBar() { log("onClickNavigationBar"); _webViewController?.reload(); } @override void initState() { super.initState(); // Register lifecycle observer to handle app state changes WidgetsBinding.instance.addObserver(this); // Get OpenList HTTP port Android() .getOpenListHttpPort() .then((port) { setState(() { _url = "http://localhost:$port"; }); log("OpenList URL set to: $_url"); }) .catchError((error) { log("Failed to get OpenList port: $error"); }); // Wait a bit for service to be ready before loading Future.delayed(const Duration(milliseconds: 500), () { if (mounted && _webViewController == null) { // Will be initialized when WebView is created log("WebView will initialize with URL: $_url"); } }); } @override void dispose() { // Remove lifecycle observer when widget is disposed WidgetsBinding.instance.removeObserver(this); _webViewController?.dispose(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); log("App lifecycle state changed: $state"); switch (state) { case AppLifecycleState.resumed: // App returned to foreground, WebView should be active log("App resumed, WebView is active"); _webViewController?.resume(); break; case AppLifecycleState.paused: // App entered background, ensure WebView state is preserved log("App paused, WebView entering background"); // Note: Do not pause WebView to keep background tasks running // The UIBackgroundModes in Info.plist allows WebKit processes to continue break; case AppLifecycleState.inactive: // App transitioning states (e.g., incoming call, app switcher) log("App inactive"); break; case AppLifecycleState.detached: // App is detached from UI log("App detached"); break; case AppLifecycleState.hidden: // App is hidden log("App hidden"); break; } } @override Widget build(BuildContext context) { return PopScope( canPop: !_canGoBack, onPopInvoked: (didPop) async { log("onPopInvoked $didPop"); if (didPop) return; _webViewController?.goBack(); }, child: Scaffold( body: Column(children: [ SizedBox(height: MediaQuery.of(context).padding.top), LinearProgressIndicator( value: _progress, backgroundColor: Colors.grey[200], valueColor: const AlwaysStoppedAnimation(Colors.blue), ), Expanded( child: InAppWebView( initialSettings: settings, initialUrlRequest: URLRequest(url: WebUri(_url)), onWebViewCreated: (InAppWebViewController controller) { _webViewController = controller; log("WebView created, loading URL: $_url"); }, onLoadStart: (InAppWebViewController controller, Uri? url) { log("onLoadStart $url"); setState(() { _progress = 0; _isLoading = true; }); }, shouldOverrideUrlLoading: (controller, navigationAction) async { log("shouldOverrideUrlLoading ${navigationAction.request.url}"); final uri = navigationAction.request.url; if (uri == null) { return NavigationActionPolicy.CANCEL; } if (_isAllowedInAppNavigation(uri)) { return NavigationActionPolicy.ALLOW; } final scheme = uri.scheme.toLowerCase(); if (Platform.isIOS && scheme == "javascript") { log("Blocked javascript navigation on iOS: ${uri.toString()}"); return NavigationActionPolicy.CANCEL; } await _openExternalUri(uri.toString()); return NavigationActionPolicy.CANCEL; }, onReceivedError: (controller, request, error) async { log("WebView error: ${error.description}"); // Check if OpenList service is running try { if (!await Android().isRunning()) { log("Service not running, attempting to start..."); await Android().startService(); // Wait for service to start and retry for (int i = 0; i < 3; i++) { await Future.delayed(const Duration(milliseconds: 500)); if (await Android().isRunning()) { log("Service started, reloading WebView"); _webViewController?.reload(); break; } } } } catch (e) { log("Failed to handle WebView error: $e"); } }, onDownloadStartRequest: (controller, url) async { Get.showSnackbar(GetSnackBar( title: S.of(context).downloadThisFile, message: url.suggestedFilename ?? url.contentDisposition ?? url.toString(), duration: const Duration(seconds: 5), mainButton: Column(children: [ TextButton( onPressed: () async { Get.closeCurrentSnackbar(); // 使用内置下载管理器后台下载 DownloadManager.downloadFileInBackground( url: url.url.toString(), filename: url.suggestedFilename, ); }, child: Text(S.of(context).directDownload), ), TextButton( onPressed: () { IntentUtils.getUrlIntent(url.url.toString()) .launchChooser(S.of(context).selectAppToOpen); }, child: Text(S.of(context).selectAppToOpen), ), TextButton( onPressed: () { IntentUtils.getUrlIntent(url.url.toString()).launch(); }, child: Text(S.of(context).browserDownload), ), ]), onTap: (_) { Clipboard.setData( ClipboardData(text: url.url.toString())); Get.closeCurrentSnackbar(); Get.showSnackbar(GetSnackBar( message: S.of(context).copiedToClipboard, duration: const Duration(seconds: 1), )); }, )); }, onLoadStop: (InAppWebViewController controller, Uri? url) async { log("onLoadStop $url"); setState(() { _progress = 0; _isLoading = false; }); }, onProgressChanged: (InAppWebViewController controller, int progress) { setState(() { _progress = progress / 100; if (_progress == 1) _progress = 0; }); controller.canGoBack().then((value) => setState(() { _canGoBack = value; })); }, onUpdateVisitedHistory: (InAppWebViewController controller, WebUri? url, bool? isReload) { _url = url.toString(); }, ), ), ]), )); } } ================================================ FILE: lib/utils/app_store_update.dart ================================================ import 'dart:io'; import 'package:flutter/services.dart'; class AppStoreUpdate { static const MethodChannel _channel = MethodChannel('openlist/app_store_update'); static Future checkAndShowUpdate() async { if (!Platform.isIOS) { return false; } final result = await _channel.invokeMethod('checkAndShowUpdate'); return result ?? false; } } ================================================ FILE: lib/utils/download_examples.dart ================================================ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'download_manager.dart'; import '../generated/l10n.dart'; /// 下载功能使用示例 class DownloadExamples { /// 示例1: 简单文件下载 static Future downloadSimpleFile() async { await DownloadManager.downloadFileInBackground( url: 'https://example.com/document.pdf', filename: 'my_document.pdf', ); } /// 示例2: 带进度的下载 static Future downloadWithProgress() async { await DownloadManager.downloadFileWithProgress( url: 'https://example.com/large_file.zip', filename: 'large_file.zip', ); } /// 示例3: 自定义进度回调 static Future downloadWithCustomProgress() async { await DownloadManager.downloadFileWithProgress( url: 'https://example.com/video.mp4', filename: 'video.mp4', ); } /// 示例4: 批量下载 static Future downloadMultipleFiles(List urls) async { for (int i = 0; i < urls.length; i++) { String url = urls[i]; String filename = 'file_${i + 1}_${DateTime.now().millisecondsSinceEpoch}'; Get.showSnackbar(GetSnackBar( message: S.current.downloadingFileProgress(i + 1, urls.length), duration: Duration(seconds: 2), )); bool success = await DownloadManager.downloadFileInBackground( url: url, filename: filename, ); if (!success) { Get.showSnackbar(GetSnackBar( message: S.current.fileDownloadFailed(i + 1), duration: Duration(seconds: 3), )); break; } } Get.showSnackbar(GetSnackBar( message: S.current.batchDownloadComplete, duration: Duration(seconds: 3), )); } /// 示例5: 下载并显示自定义对话框 static Future downloadWithCustomDialog(BuildContext context) async { // 显示确认对话框 bool? shouldDownload = await showDialog( context: context, builder: (context) => AlertDialog( title: Text(S.current.confirmDownload), content: Text(S.current.confirmDownloadMessage), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: Text(S.current.cancel), ), TextButton( onPressed: () => Navigator.of(context).pop(true), child: Text(S.current.download), ), ], ), ); if (shouldDownload == true) { await DownloadManager.downloadFileWithProgress( url: 'https://example.com/important_file.pdf', filename: 'important_file.pdf', ); } } /// 示例6: 下载图片并显示预览 static Future downloadImageWithPreview(String imageUrl) async { // 先显示加载提示 Get.dialog( AlertDialog( content: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text(S.current.downloadingImage), ], ), ), barrierDismissible: false, ); bool success = await DownloadManager.downloadFileInBackground( url: imageUrl, filename: 'image_${DateTime.now().millisecondsSinceEpoch}.jpg', ); Get.back(); // 关闭加载对话框 if (success) { Get.showSnackbar(GetSnackBar( message: S.current.imageDownloadSuccess, duration: Duration(seconds: 3), mainButton: TextButton( onPressed: () { // 可以在这里添加打开图片的逻辑 Get.showSnackbar(GetSnackBar( message: S.current.checkImageInDownloadFolder, duration: Duration(seconds: 2), )); }, child: Text(S.current.view), ), )); } } /// 示例7: 下载APK并提示安装 static Future downloadApkAndInstall(String apkUrl, String version) async { bool success = await DownloadManager.downloadFileWithProgress( url: apkUrl, filename: 'app_update_v$version.apk', ); if (success) { Get.dialog( AlertDialog( title: Text(S.current.downloadCompleteTitle), content: Text(S.current.apkDownloadCompleteMessage), actions: [ TextButton( onPressed: () => Get.back(), child: Text(S.current.laterInstall), ), TextButton( onPressed: () { Get.back(); // 这里可以添加安装APK的逻辑 Get.showSnackbar(GetSnackBar( message: S.current.findApkInDownloadFolder, duration: Duration(seconds: 5), )); }, child: Text(S.current.installNow), ), ], ), ); } } } /// 下载工具类 - 提供一些便捷方法 class DownloadUtils { /// 检查URL是否为下载链接 static bool isDownloadUrl(String url) { final downloadExtensions = [ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.zip', '.rar', '.7z', '.tar', '.gz', '.mp3', '.mp4', '.avi', '.mkv', '.mov', '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.apk', '.exe', '.dmg', '.deb', '.rpm' ]; String lowerUrl = url.toLowerCase(); return downloadExtensions.any((ext) => lowerUrl.contains(ext)); } /// 从URL获取文件扩展名 static String getFileExtension(String url) { try { Uri uri = Uri.parse(url); String path = uri.path; if (path.contains('.')) { return path.split('.').last.toLowerCase(); } } catch (e) { print('获取文件扩展名失败: $e'); } return ''; } /// 格式化文件大小 static String formatFileSize(int bytes) { if (bytes < 1024) return '${bytes}B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}KB'; if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}MB'; return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)}GB'; } /// 显示下载选择对话框 static void showDownloadOptions(BuildContext context, String url, {String? filename}) { showModalBottomSheet( context: context, builder: (context) => Container( padding: EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( S.current.selectDownloadMethod, style: Theme.of(context).textTheme.titleLarge, ), SizedBox(height: 16), ListTile( leading: Icon(Icons.download), title: Text(S.current.directDownloadMethod), subtitle: Text(S.current.directDownloadMethodDesc), onTap: () { Navigator.pop(context); DownloadManager.downloadFileWithProgress( url: url, filename: filename, ); }, ), ListTile( leading: Icon(Icons.open_in_browser), title: Text(S.current.browserDownloadMethod), subtitle: Text(S.current.browserDownloadMethodDesc), onTap: () { Navigator.pop(context); // 这里可以调用原有的Intent方式 }, ), ListTile( leading: Icon(Icons.share), title: Text(S.current.shareLink), subtitle: Text(S.current.shareLinkDesc), onTap: () { Navigator.pop(context); // 这里可以添加分享功能 }, ), ], ), ), ); } } ================================================ FILE: lib/utils/download_manager.dart ================================================ import 'dart:io'; import 'dart:developer'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:get/get.dart' as getx; import 'package:flutter/material.dart'; import 'package:open_filex/open_filex.dart'; import 'notification_manager.dart'; import '../generated/l10n.dart'; /// 下载任务状态 enum DownloadStatus { pending, // 等待中 downloading, // 下载中 completed, // 已完成 failed, // 失败 cancelled, // 已取消 } /// 下载任务 class DownloadTask { final String id; final String url; final String filename; final String filePath; DownloadStatus status; double progress; int receivedBytes; int totalBytes; String? errorMessage; DateTime startTime; DateTime? endTime; CancelToken? cancelToken; DownloadTask({ required this.id, required this.url, required this.filename, required this.filePath, this.status = DownloadStatus.pending, this.progress = 0.0, this.receivedBytes = 0, this.totalBytes = 0, this.errorMessage, DateTime? startTime, this.endTime, this.cancelToken, }) : startTime = startTime ?? DateTime.now(); String get statusText { switch (status) { case DownloadStatus.pending: return S.current.pending; case DownloadStatus.downloading: return S.current.downloading; case DownloadStatus.completed: return S.current.completed; case DownloadStatus.failed: return S.current.failed; case DownloadStatus.cancelled: return S.current.cancelled; } } String get progressText { if (totalBytes > 0) { return '${_formatBytes(receivedBytes)} / ${_formatBytes(totalBytes)}'; } return _formatBytes(receivedBytes); } String _formatBytes(int bytes) { if (bytes < 1024) return '${bytes}B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}KB'; if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}MB'; return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)}GB'; } } class DownloadManager { static final Dio _dio = Dio(); static final Map _activeTasks = {}; static final List _completedTasks = []; /// 获取所有活跃的下载任务 static List get activeTasks => _activeTasks.values.toList(); /// 获取所有已完成的下载任务 static List get completedTasks => _completedTasks; /// 获取所有下载任务 static List get allTasks => [..._activeTasks.values, ..._completedTasks]; /// 带进度条的下载(后台下载,不阻塞UI) static Future downloadFileWithProgress({ required String url, String? filename, }) async { // 初始化通知管理器 await NotificationManager.initialize(); // 生成任务ID String taskId = DateTime.now().millisecondsSinceEpoch.toString(); // 获取下载目录 Directory? downloadDir = await _getOpenListDownloadDirectory(); if (downloadDir == null) { getx.Get.showSnackbar(getx.GetSnackBar( message: S.current.cannotGetDownloadDirectory, duration: const Duration(seconds: 3), )); return false; } // 确定文件名和路径 String finalFilename = filename ?? _getFilenameFromUrl(url); String filePath = '${downloadDir.path}/$finalFilename'; filePath = _getUniqueFilePath(filePath); finalFilename = filePath.split('/').last; // 创建下载任务 CancelToken cancelToken = CancelToken(); DownloadTask task = DownloadTask( id: taskId, url: url, filename: finalFilename, filePath: filePath, status: DownloadStatus.pending, cancelToken: cancelToken, ); // 添加到活跃任务列表 _activeTasks[taskId] = task; // 显示开始下载提示(只显示一次) getx.Get.showSnackbar(getx.GetSnackBar( message: S.current.startDownloadFile(finalFilename), duration: const Duration(seconds: 2), backgroundColor: Colors.green, )); try { // 更新任务状态 task.status = DownloadStatus.downloading; // 显示初始通知 await NotificationManager.showDownloadProgressNotification(); // 执行下载 await _dio.download( url, filePath, cancelToken: cancelToken, onReceiveProgress: (received, total) { if (task.status == DownloadStatus.cancelled) return; task.receivedBytes = received; task.totalBytes = total; if (total > 0) { task.progress = received / total; } // 更新通知进度 NotificationManager.showDownloadProgressNotification(); log('下载进度: ${(task.progress * 100).toStringAsFixed(1)}%'); }, ); // 下载完成 task.status = DownloadStatus.completed; task.endTime = DateTime.now(); task.progress = 1.0; // 移动到已完成列表 _activeTasks.remove(taskId); _completedTasks.insert(0, task); // 插入到开头,最新的在前面 // 显示单个文件完成通知 await NotificationManager.showSingleFileCompleteNotification(task); // 显示完成提示 getx.Get.showSnackbar(getx.GetSnackBar( message: S.current.downloadCompleteFile(finalFilename), duration: const Duration(seconds: 3), backgroundColor: Colors.blue, mainButton: TextButton( onPressed: () { _openFile(filePath); }, child: Text(S.current.open), ), )); log('文件下载完成: $filePath'); return true; } catch (e) { if (e is DioException && e.type == DioExceptionType.cancel) { // 用户取消下载 task.status = DownloadStatus.cancelled; task.endTime = DateTime.now(); log('下载已取消: $url'); } else { // 下载失败 task.status = DownloadStatus.failed; task.errorMessage = e.toString(); task.endTime = DateTime.now(); log('下载失败: $e'); getx.Get.showSnackbar(getx.GetSnackBar( message: S.current.downloadFailedFile(finalFilename), duration: const Duration(seconds: 3), backgroundColor: Colors.red, )); } // 移动到已完成列表 _activeTasks.remove(taskId); _completedTasks.insert(0, task); // 更新通知状态 if (_activeTasks.isEmpty) { await NotificationManager.cancelDownloadNotification(); } else { await NotificationManager.showDownloadProgressNotification(); } return false; } } /// 简单的后台下载(推荐使用) static Future downloadFileInBackground({ required String url, String? filename, }) async { return await downloadFileWithProgress( url: url, filename: filename, ); } /// 取消下载任务 static void cancelDownload(String taskId) { DownloadTask? task = _activeTasks[taskId]; if (task != null && task.cancelToken != null) { task.cancelToken!.cancel(S.current.userCancelledDownloadError); } } /// 清除已完成的下载记录 static void clearCompletedTasks() { _completedTasks.clear(); } /// 删除下载任务记录 static void removeTask(String taskId) { _activeTasks.remove(taskId); _completedTasks.removeWhere((task) => task.id == taskId); } /// 获取OpenList专用下载目录 static Future _getOpenListDownloadDirectory() async { try { Directory? baseDir; if (Platform.isAndroid) { // Android: 优先使用公共下载目录 baseDir = Directory('/storage/emulated/0/Download'); if (!await baseDir.exists()) { // 如果公共下载目录不存在,使用外部存储目录 baseDir = await getExternalStorageDirectory(); if (baseDir != null) { baseDir = Directory('${baseDir.path}/Download'); } } } else if (Platform.isIOS) { // iOS: 使用应用文档目录下的Downloads文件夹 baseDir = await getApplicationDocumentsDirectory(); baseDir = Directory('${baseDir.path}/Downloads'); } else { // 其他平台(如Windows、macOS、Linux) baseDir = await getDownloadsDirectory(); } if (baseDir == null) { log('无法获取基础下载目录'); return null; } // 创建OpenList专用文件夹 Directory openListDir = Directory('${baseDir.path}/OpenList'); if (!await openListDir.exists()) { try { await openListDir.create(recursive: true); log('创建OpenList下载目录: ${openListDir.path}'); } catch (e) { log('创建OpenList目录失败: $e'); // 如果创建失败,返回基础目录 return baseDir; } } log('OpenList下载目录: ${openListDir.path}'); return openListDir; } catch (e) { log('获取下载目录失败: $e'); return null; } } /// 从URL中提取文件名 static String _getFilenameFromUrl(String url) { try { Uri uri = Uri.parse(url); String path = uri.path; if (path.isNotEmpty && path.contains('/')) { String filename = path.split('/').last; if (filename.isNotEmpty) { return filename; } } } catch (e) { log('解析文件名失败: $e'); } // 如果无法从URL提取文件名,使用时间戳 return 'download_${DateTime.now().millisecondsSinceEpoch}'; } /// 获取唯一的文件路径(避免重名) static String _getUniqueFilePath(String originalPath) { File file = File(originalPath); if (!file.existsSync()) { return originalPath; } String directory = file.parent.path; String nameWithoutExtension = file.path.split('/').last.split('.').first; String extension = file.path.contains('.') ? '.${file.path.split('.').last}' : ''; int counter = 1; String newPath; do { newPath = '$directory/${nameWithoutExtension}_$counter$extension'; counter++; } while (File(newPath).existsSync()); return newPath; } /// 检查是否为 APK 文件 static bool _isApkFile(String filePath) { return filePath.toLowerCase().endsWith('.apk'); } /// 检查和请求安装权限 static Future _checkInstallPermission() async { if (!Platform.isAndroid) return true; try { // 检查是否有安装权限 bool hasPermission = await Permission.requestInstallPackages.isGranted; if (!hasPermission) { // 请求安装权限 PermissionStatus status = await Permission.requestInstallPackages.request(); if (status.isGranted) { return true; } else if (status.isPermanentlyDenied) { // 权限被永久拒绝,引导用户到设置页面 getx.Get.dialog( AlertDialog( title: Text(S.current.needInstallPermission), content: Text(S.current.needInstallPermissionDesc), actions: [ TextButton( onPressed: () => getx.Get.back(), child: Text(S.current.cancel), ), TextButton( onPressed: () { getx.Get.back(); openAppSettings(); }, child: Text(S.current.goToSettings), ), ], ), ); return false; } else { getx.Get.showSnackbar(getx.GetSnackBar( message: S.current.needInstallPermissionToInstallApk, duration: const Duration(seconds: 3), )); return false; } } return true; } catch (e) { log('检查安装权限失败: $e'); return true; // 如果检查失败,继续尝试打开 } } /// 尝试打开文件 static Future _openFile(String filePath) async { try { log('尝试打开文件: $filePath'); // 如果是 APK 文件,先检查安装权限 if (_isApkFile(filePath)) { bool hasPermission = await _checkInstallPermission(); if (!hasPermission) { return; // 没有权限,不继续打开 } } // 使用 open_filex 插件打开文件 final result = await OpenFilex.open(filePath); log('打开文件结果: ${result.type} - ${result.message}'); // 根据结果显示相应的提示 switch (result.type) { case ResultType.done: // 文件成功打开,不需要额外提示 break; case ResultType.noAppToOpen: if (_isApkFile(filePath)) { getx.Get.showSnackbar(getx.GetSnackBar( message: S.current.cannotInstallApkNeedPermission, duration: const Duration(seconds: 5), mainButton: TextButton( onPressed: () { openAppSettings(); }, child: Text(S.current.goToSettings), ), )); } else { getx.Get.showSnackbar(getx.GetSnackBar( message: S.current.noAppToOpenFile, duration: const Duration(seconds: 3), mainButton: TextButton( onPressed: () { _showFileLocation(filePath); }, child: Text(S.current.viewLocation), ), )); } break; case ResultType.fileNotFound: getx.Get.showSnackbar(getx.GetSnackBar( message: S.current.fileNotFound, duration: const Duration(seconds: 3), )); break; case ResultType.permissionDenied: if (_isApkFile(filePath)) { getx.Get.showSnackbar(getx.GetSnackBar( message: S.current.noPermissionToInstallApkFile, duration: const Duration(seconds: 5), mainButton: TextButton( onPressed: () { openAppSettings(); }, child: Text(S.current.goToSettings), ), )); } else { getx.Get.showSnackbar(getx.GetSnackBar( message: S.current.noPermissionToOpenFile, duration: const Duration(seconds: 3), )); } break; case ResultType.error: getx.Get.showSnackbar(getx.GetSnackBar( message: S.current.openFileFailed(result.message), duration: const Duration(seconds: 3), mainButton: TextButton( onPressed: () { _showFileLocation(filePath); }, child: Text(S.current.viewLocation), ), )); break; } } catch (e) { log('打开文件异常: $e'); getx.Get.showSnackbar(getx.GetSnackBar( message: S.current.openFileException(e.toString()), duration: const Duration(seconds: 3), mainButton: TextButton( onPressed: () { _showFileLocation(filePath); }, child: Text(S.current.viewLocation), ), )); } } /// 显示文件位置信息 static void _showFileLocation(String filePath) { getx.Get.dialog( AlertDialog( title: Text(S.current.fileLocation), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(S.current.fileSavedTo), const SizedBox(height: 8), SelectableText( filePath, style: const TextStyle(fontSize: 12), ), const SizedBox(height: 16), Text( S.current.fileLocationTip, style: const TextStyle(fontSize: 14, color: Colors.grey), ), ], ), actions: [ TextButton( onPressed: () => getx.Get.back(), child: Text(S.current.ok), ), ], ), ); } /// 获取OpenList下载目录路径(公共方法) static Future getDownloadDirectoryPath() async { Directory? dir = await _getOpenListDownloadDirectory(); return dir?.path; } /// 列出已下载的文件 static Future> getDownloadedFiles() async { try { Directory? downloadDir = await _getOpenListDownloadDirectory(); if (downloadDir != null && await downloadDir.exists()) { return downloadDir.listSync(); } } catch (e) { log('获取下载文件列表失败: $e'); } return []; } /// 清理下载目录 static Future clearDownloadDirectory() async { try { Directory? downloadDir = await _getOpenListDownloadDirectory(); if (downloadDir != null && await downloadDir.exists()) { await downloadDir.delete(recursive: true); log('已清理下载目录'); return true; } } catch (e) { log('清理下载目录失败: $e'); } return false; } /// 删除指定文件 static Future deleteFile(String filename) async { try { Directory? downloadDir = await _getOpenListDownloadDirectory(); if (downloadDir != null) { File file = File('${downloadDir.path}/$filename'); if (await file.exists()) { await file.delete(); log('已删除文件: $filename'); return true; } } } catch (e) { log('删除文件失败: $e'); } return false; } } /// 下载控制器(保持向后兼容) class DownloadController extends getx.GetxController { double _progress = 0.0; String _statusText = S.current.preparingDownloadStatus; bool _isCancelled = false; double get progress => _progress; String get statusText => _statusText; bool get isCancelled => _isCancelled; void updateProgress(double progress, int received, int total) { if (_isCancelled) return; _progress = progress; _statusText = '${_formatBytes(received)} / ${_formatBytes(total)}'; update(); } void cancelDownload() { _isCancelled = true; _statusText = S.current.downloadCancelledText; update(); } String _formatBytes(int bytes) { if (bytes < 1024) return '${bytes}B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}KB'; if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}MB'; return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)}GB'; } } ================================================ FILE: lib/utils/download_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'download_manager.dart'; import '../generated/l10n.dart'; /// 下载功能测试页面 class DownloadTestPage extends StatelessWidget { const DownloadTestPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(S.current.downloadFunctionTest), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( S.current.testDirectDownloadFunction, style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 20), ElevatedButton( onPressed: () async { // 测试下载一个小文件 await DownloadManager.downloadFileWithProgress( url: 'https://httpbin.org/json', filename: 'test.json', ); }, child: Text(S.current.testDownloadJsonFile), ), const SizedBox(height: 10), ElevatedButton( onPressed: () async { // 测试下载图片 await DownloadManager.downloadFileWithProgress( url: 'https://httpbin.org/image/png', filename: 'test_image.png', ); }, child: Text(S.current.testDownloadPngImage), ), const SizedBox(height: 10), ElevatedButton( onPressed: () async { // 测试下载较大文件 await DownloadManager.downloadFileWithProgress( url: 'https://httpbin.org/drip?duration=5&numbytes=1024000', filename: 'large_test.bin', ); }, child: Text(S.current.testDownloadLargeFile), ), const SizedBox(height: 10), ElevatedButton( onPressed: () { // 提示用户通过底部导航栏查看下载文件 Get.showSnackbar(GetSnackBar( message: S.current.checkDownloadManagerForFiles, duration: const Duration(seconds: 2), )); }, child: Text(S.current.viewDownloadFiles), ), const SizedBox(height: 10), ElevatedButton( onPressed: () async { // 显示下载目录路径 String? path = await DownloadManager.getDownloadDirectoryPath(); Get.dialog( AlertDialog( title: Text(S.current.downloadDirectory), content: SelectableText(path ?? S.current.getDownloadPathFailed), actions: [ TextButton( onPressed: () => Get.back(), child: Text(S.current.ok), ), ], ), ); }, child: Text(S.current.viewDownloadDirectory), ), const SizedBox(height: 20), Text( S.current.description, style: Theme.of(context).textTheme.titleMedium, ), Text( S.current.downloadInstructions, style: Theme.of(context).textTheme.bodyMedium, ), ], ), ), ); } } /// 在主应用中添加测试入口的辅助方法 class DownloadTestHelper { static void showTestDialog(BuildContext context) { Get.dialog( AlertDialog( title: Text(S.current.downloadFunctionTest), content: Text(S.current.openDownloadTestPage), actions: [ TextButton( onPressed: () => Get.back(), child: Text(S.current.cancel), ), TextButton( onPressed: () { Get.back(); Get.to(() => const DownloadTestPage()); }, child: Text(S.current.ok), ), ], ), ); } } ================================================ FILE: lib/utils/intent_utils.dart ================================================ import 'package:android_intent_plus/android_intent.dart'; class IntentUtils { static AndroidIntent getUrlIntent(String url) { return AndroidIntent(action: "action_view", data: url); } } ================================================ FILE: lib/utils/language_controller.dart ================================================ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:openlist_mobile/utils/language_manager.dart'; import 'package:openlist_mobile/generated/l10n.dart'; class LanguageController extends GetxController { static LanguageController get to => Get.find(); final _currentLanguageOption = LanguageManager.supportedLanguages.first.obs; final _locale = Rxn(); LanguageOption get currentLanguageOption => _currentLanguageOption.value; Locale? get locale => _locale.value; @override void onInit() { super.onInit(); _loadSavedLanguage(); } // 加载保存的语言设置 Future _loadSavedLanguage() async { try { final savedOption = await LanguageManager.instance.getSavedLanguageOption(); _currentLanguageOption.value = savedOption; final savedLocale = await LanguageManager.instance.getCurrentLocale(); _locale.value = savedLocale; } catch (e) { debugPrint('Failed to load saved language: $e'); } } // 切换语言 Future changeLanguage(LanguageOption option) async { try { _currentLanguageOption.value = option; // 保存语言设置 await LanguageManager.instance.saveLanguageCode(option.code); // 更新locale if (option.locale != null) { _locale.value = option.locale; Get.updateLocale(option.locale!); } else { // 跟随系统语言 final systemLocale = Get.deviceLocale ?? const Locale('en'); // 检查系统语言是否被支持 final supportedSystemLocale = _getSupportedLocale(systemLocale); _locale.value = null; // 保持null表示跟随系统 Get.updateLocale(supportedSystemLocale); } } catch (e) { debugPrint('Failed to change language: $e'); } } // 获取支持的语言环境 Locale _getSupportedLocale(Locale deviceLocale) { // 检查是否直接支持 for (final option in LanguageManager.supportedLanguages) { if (option.locale?.languageCode == deviceLocale.languageCode) { return option.locale!; } } // 默认返回英语 return const Locale('en'); } // 获取当前应该使用的locale(考虑跟随系统的情况) Locale getEffectiveLocale() { if (_locale.value != null) { return _locale.value!; } // 如果是跟随系统,返回系统语言或默认语言 final systemLocale = Get.deviceLocale ?? const Locale('en'); return _getSupportedLocale(systemLocale); } } // 语言选择器组件 class LanguageSelector extends StatelessWidget { final VoidCallback? onLanguageChanged; const LanguageSelector({ Key? key, this.onLanguageChanged, }) : super(key: key); @override Widget build(BuildContext context) { return GetBuilder( builder: (controller) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ...LanguageManager.supportedLanguages.map( (option) => RadioListTile( title: Text(_getLocalizedLanguageName(option)), value: option, groupValue: controller.currentLanguageOption, onChanged: (LanguageOption? value) async { if (value != null) { await controller.changeLanguage(value); onLanguageChanged?.call(); } }, ), ), ], ); }, ); } String _getLocalizedLanguageName(LanguageOption option) { switch (option.name) { case 'followSystem': return S.current.followSystem; case 'simplifiedChinese': return S.current.simplifiedChinese; case 'english': return S.current.english; default: return option.name; } } } ================================================ FILE: lib/utils/language_manager.dart ================================================ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; class LanguageManager { static const String _languageKey = 'app_language'; static const String _systemLanguageValue = 'system'; static LanguageManager? _instance; static LanguageManager get instance => _instance ??= LanguageManager._(); LanguageManager._(); // 支持的语言 static const List supportedLanguages = [ LanguageOption( code: _systemLanguageValue, name: 'followSystem', locale: null, ), LanguageOption( code: 'zh', name: 'simplifiedChinese', locale: Locale('zh'), ), LanguageOption( code: 'en', name: 'english', locale: Locale('en'), ), ]; // 获取保存的语言代码 Future getSavedLanguageCode() async { final prefs = await SharedPreferences.getInstance(); return prefs.getString(_languageKey) ?? _systemLanguageValue; } // 保存语言代码 Future saveLanguageCode(String languageCode) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_languageKey, languageCode); } // 获取保存的语言选项 Future getSavedLanguageOption() async { final languageCode = await getSavedLanguageCode(); return supportedLanguages.firstWhere( (option) => option.code == languageCode, orElse: () => supportedLanguages.first, ); } // 根据语言代码获取Locale Future getLocaleFromCode(String languageCode) async { if (languageCode == _systemLanguageValue) { return null; // 跟随系统 } final option = supportedLanguages.firstWhere( (option) => option.code == languageCode, orElse: () => supportedLanguages.first, ); return option.locale; } // 获取当前应该使用的Locale Future getCurrentLocale() async { final languageCode = await getSavedLanguageCode(); return await getLocaleFromCode(languageCode); } // 清除语言设置(恢复为跟随系统) Future clearLanguageSetting() async { final prefs = await SharedPreferences.getInstance(); await prefs.remove(_languageKey); } } class LanguageOption { final String code; final String name; // 对应本地化键名 final Locale? locale; const LanguageOption({ required this.code, required this.name, required this.locale, }); @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is LanguageOption && other.code == code; } @override int get hashCode => code.hashCode; } ================================================ FILE: lib/utils/notification_manager.dart ================================================ import 'dart:io'; import 'dart:developer'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart' as getx; import 'download_manager.dart'; import '../pages/download_manager_page.dart'; import '../generated/l10n.dart'; class NotificationManager { static final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin(); static bool _isInitialized = false; static const int _downloadNotificationId = 1000; /// 初始化通知 static Future initialize() async { if (_isInitialized) return; try { // Android 初始化设置 const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); // iOS 初始化设置 const DarwinInitializationSettings initializationSettingsIOS = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, ); const InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsIOS, ); await _notifications.initialize( initializationSettings, onDidReceiveNotificationResponse: _onNotificationTapped, ); // 请求通知权限 if (Platform.isAndroid) { await _notifications .resolvePlatformSpecificImplementation() ?.requestNotificationsPermission(); } else if (Platform.isIOS) { await _notifications .resolvePlatformSpecificImplementation() ?.requestPermissions( alert: true, badge: true, sound: true, ); } _isInitialized = true; log('通知管理器初始化成功'); } catch (e) { log('通知管理器初始化失败: $e'); } } /// 处理通知点击事件 static void _onNotificationTapped(NotificationResponse response) { log('通知被点击: ${response.payload}'); // 跳转到下载管理页面 if (getx.Get.context != null) { getx.Get.to(() => const DownloadManagerPage()); } } /// 显示或更新下载进度通知 static Future showDownloadProgressNotification() async { if (!_isInitialized) { await initialize(); } try { List activeTasks = DownloadManager.activeTasks .where((task) => task.status == DownloadStatus.downloading) .toList(); if (activeTasks.isEmpty) { // 没有活跃下载任务,取消通知 await _notifications.cancel(_downloadNotificationId); return; } String title; String body; int progress = 0; int maxProgress = 100; if (activeTasks.length == 1) { // 单个文件下载 DownloadTask task = activeTasks.first; title = S.current.currentlyDownloading; body = task.filename; progress = (task.progress * 100).round(); } else { // 多个文件下载 title = S.current.currentlyDownloading; body = S.current.currentDownloadingFiles(activeTasks.length); // 计算总进度 - 所有文件下载进度的总和 double totalProgress = 0; for (DownloadTask task in activeTasks) { totalProgress += task.progress; } // 总进度条为所有文件进度的平均值 double avgProgress = totalProgress / activeTasks.length; progress = (avgProgress * 100).round(); } // Android 通知详情 AndroidNotificationDetails androidDetails = AndroidNotificationDetails( 'download_channel', S.current.downloadProgressChannel, channelDescription: S.current.downloadProgressDesc, importance: Importance.low, priority: Priority.low, showProgress: true, maxProgress: maxProgress, progress: progress, ongoing: true, // 常驻通知 autoCancel: false, playSound: false, enableVibration: false, icon: '@mipmap/ic_launcher', actions: [ AndroidNotificationAction( 'view_downloads', S.current.viewDownloads, showsUserInterface: true, ), ], ); // iOS 通知详情 const DarwinNotificationDetails iosDetails = DarwinNotificationDetails( presentAlert: false, presentBadge: false, presentSound: false, ); NotificationDetails notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); await _notifications.show( _downloadNotificationId, title, body, notificationDetails, payload: 'download_progress', ); } catch (e) { log('显示下载进度通知失败: $e'); } } /// 显示下载完成通知 static Future showDownloadCompleteNotification() async { if (!_isInitialized) { await initialize(); } try { // 检查是否还有其他下载任务在进行 bool hasActiveDownloads = DownloadManager.activeTasks.isNotEmpty; if (hasActiveDownloads) { // 还有其他下载在进行,不显示完成通知,继续显示进度通知 await showDownloadProgressNotification(); return; } // 先取消进度通知 await _notifications.cancel(_downloadNotificationId); // 获取最近完成的任务数量 List completedTasks = DownloadManager.completedTasks .where((task) => task.status == DownloadStatus.completed) .toList(); if (completedTasks.isEmpty) return; String title; String body; if (completedTasks.length == 1) { // 单个文件完成 DownloadTask task = completedTasks.first; title = S.current.downloadCompleteNotificationTitle(task.filename); body = S.current.clickToJumpToDownloadManager; } else { // 多个文件完成 title = S.current.downloadCompleteTitle; body = S.current.multipleFilesCompleted(completedTasks.length); } // Android 通知详情 AndroidNotificationDetails androidDetails = AndroidNotificationDetails( 'download_complete_channel', S.current.downloadCompleteChannel, channelDescription: S.current.downloadCompleteChannelDesc, importance: Importance.high, priority: Priority.high, autoCancel: true, playSound: true, enableVibration: true, icon: '@mipmap/ic_launcher', actions: [ AndroidNotificationAction( 'open_downloads', S.current.openDownloadManager, showsUserInterface: true, ), ], ); // iOS 通知详情 const DarwinNotificationDetails iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ); NotificationDetails notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); await _notifications.show( _downloadNotificationId + 1, title, body, notificationDetails, payload: 'download_complete', ); } catch (e) { log('显示下载完成通知失败: $e'); } } /// 显示单个文件下载完成通知 static Future showSingleFileCompleteNotification(DownloadTask task) async { if (!_isInitialized) { await initialize(); } try { // 检查是否还有其他下载任务在进行 bool hasActiveDownloads = DownloadManager.activeTasks.isNotEmpty; if (hasActiveDownloads) { // 还有其他下载在进行,更新进度通知 await showDownloadProgressNotification(); return; } // 先取消进度通知 await _notifications.cancel(_downloadNotificationId); String title = S.current.downloadCompleteNotificationTitle(task.filename); String body = S.current.clickToJumpToDownloadManager; // Android 通知详情 AndroidNotificationDetails androidDetails = AndroidNotificationDetails( 'download_complete_channel', S.current.downloadCompleteChannel, channelDescription: S.current.downloadCompleteChannelDesc, importance: Importance.high, priority: Priority.high, autoCancel: true, playSound: true, enableVibration: true, icon: '@mipmap/ic_launcher', actions: [ AndroidNotificationAction( 'open_downloads', S.current.openDownloadManager, showsUserInterface: true, ), ], ); // iOS 通知详情 const DarwinNotificationDetails iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ); NotificationDetails notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); await _notifications.show( _downloadNotificationId + 1, title, body, notificationDetails, payload: 'download_complete', ); } catch (e) { log('显示单个文件下载完成通知失败: $e'); } } /// 取消下载通知 static Future cancelDownloadNotification() async { try { await _notifications.cancel(_downloadNotificationId); } catch (e) { log('取消下载通知失败: $e'); } } /// 取消所有通知 static Future cancelAllNotifications() async { try { await _notifications.cancelAll(); } catch (e) { log('取消所有通知失败: $e'); } } /// 格式化字节大小 static String _formatBytes(int bytes) { if (bytes < 1024) return '${bytes}B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}KB'; if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}MB'; return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)}GB'; } } ================================================ FILE: lib/utils/service_manager.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart'; import 'package:openlist_mobile/generated_api.dart'; /// Service Manager - Manages OpenList backend service lifecycle class ServiceManager { static const String _channelName = 'com.openlist.mobile/service'; static const MethodChannel _channel = MethodChannel(_channelName); static ServiceManager? _instance; static ServiceManager get instance => _instance ??= ServiceManager._(); ServiceManager._(); // Service status stream controller final StreamController _serviceStatusController = StreamController.broadcast(); /// Service status stream Stream get serviceStatusStream => _serviceStatusController.stream; bool _isServiceRunning = false; Timer? _statusCheckTimer; /// Current service running status bool get isServiceRunning => _isServiceRunning; /// Initialize service manager Future initialize() async { try { // 设置方法调用处理器 _channel.setMethodCallHandler(_handleMethodCall); // 开始定期检查服务状态 _startStatusCheck(); // 初始检查服务状态 await checkServiceStatus(); debugPrint('ServiceManager initialized'); } catch (e) { debugPrint('Failed to initialize ServiceManager: $e'); } } /// 处理来自原生端的方法调用 Future _handleMethodCall(MethodCall call) async { debugPrint('ServiceManager received method call: ${call.method}'); switch (call.method) { case 'onServiceStatusChanged': final bool isRunning = call.arguments['isRunning'] ?? false; debugPrint('ServiceManager status change notification: $isRunning'); _updateServiceStatus(isRunning); break; default: debugPrint('Unknown method call: ${call.method}'); } } /// Start OpenList service Future startService() async { try { if (Platform.isAndroid) { final bool result = await _channel.invokeMethod('startService'); debugPrint('Start service result (Android): $result'); // Delay status check to give service startup time Timer(const Duration(seconds: 2), () => checkServiceStatus()); return result; } else if (Platform.isIOS) { // Use Pigeon API for iOS await Android().startService(); debugPrint('Start service called (iOS)'); // Delay status check Timer(const Duration(seconds: 2), () => checkServiceStatus()); return true; } return false; } catch (e) { debugPrint('Failed to start service: $e'); return false; } } /// Stop OpenList service Future stopService() async { try { if (Platform.isAndroid) { final bool result = await _channel.invokeMethod('stopService'); debugPrint('Stop service result (Android): $result'); // Update status immediately if (result) { _updateServiceStatus(false); } // Delay status check to confirm service stopped Timer(const Duration(seconds: 1), () => checkServiceStatus()); return result; } else if (Platform.isIOS) { // iOS does not need explicit stop - managed by OpenListManager debugPrint('Stop service called (iOS) - managed by system'); _updateServiceStatus(false); return true; } return false; } catch (e) { debugPrint('Failed to stop service: $e'); return false; } } /// Check service status Future checkServiceStatus() async { try { bool isRunning = false; if (Platform.isAndroid) { isRunning = await _channel.invokeMethod('isServiceRunning'); } else if (Platform.isIOS) { // Use Pigeon API for iOS isRunning = await Android().isRunning(); } _updateServiceStatus(isRunning); return isRunning; } catch (e) { debugPrint('Failed to check service status: $e'); return false; } } /// Restart service Future restartService() async { try { await stopService(); await Future.delayed(const Duration(seconds: 2)); return await startService(); } catch (e) { debugPrint('Failed to restart service: $e'); return false; } } /// 检查是否在电池优化白名单中 Future isBatteryOptimizationIgnored() async { if (!Platform.isAndroid) return true; try { final bool result = await _channel.invokeMethod('isBatteryOptimizationIgnored'); return result; } catch (e) { debugPrint('Failed to check battery optimization status: $e'); return false; } } /// 请求忽略电池优化 Future requestIgnoreBatteryOptimization() async { if (!Platform.isAndroid) return true; try { final bool result = await _channel.invokeMethod('requestIgnoreBatteryOptimization'); return result; } catch (e) { debugPrint('Failed to request battery optimization exemption: $e'); return false; } } /// 打开电池优化设置 Future openBatteryOptimizationSettings() async { if (!Platform.isAndroid) return false; try { final bool result = await _channel.invokeMethod('openBatteryOptimizationSettings'); return result; } catch (e) { debugPrint('Failed to open battery optimization settings: $e'); return false; } } /// 打开自启动设置 Future openAutoStartSettings() async { if (!Platform.isAndroid) return false; try { final bool result = await _channel.invokeMethod('openAutoStartSettings'); return result; } catch (e) { debugPrint('Failed to open auto start settings: $e'); return false; } } /// 获取服务地址 Future getServiceAddress() async { if (!Platform.isAndroid) return ''; try { final String address = await _channel.invokeMethod('getServiceAddress'); return address; } catch (e) { debugPrint('Failed to get service address: $e'); return ''; } } /// 开始定期检查服务状态 void _startStatusCheck() { _statusCheckTimer?.cancel(); _statusCheckTimer = Timer.periodic(const Duration(seconds: 30), (timer) { checkServiceStatus(); }); } /// 停止状态检查 void _stopStatusCheck() { _statusCheckTimer?.cancel(); _statusCheckTimer = null; } /// 更新服务状态 void _updateServiceStatus(bool isRunning) { if (_isServiceRunning != isRunning) { _isServiceRunning = isRunning; _serviceStatusController.add(isRunning); debugPrint('Service status changed: $isRunning'); } } /// 释放资源 void dispose() { _stopStatusCheck(); _serviceStatusController.close(); } } ================================================ FILE: lib/utils/update_checker.dart ================================================ import 'dart:convert'; import 'dart:core'; import 'dart:developer'; import 'dart:io'; import 'package:openlist_mobile/contant/native_bridge.dart'; class UpdateChecker { String owner; String repo; Map? _data; UpdateChecker({required this.owner, required this.repo}); String _versionName = ""; String _systemABI = ""; downloadData() async { _data = await _getLatestRelease(owner, repo); _versionName = await NativeBridge.common.getVersionName(); _systemABI = await NativeBridge.common.getDeviceCPUABI(); } Map get data { if (_data == null) { throw Exception('Data not downloaded'); } return _data!; } static Future> _getLatestRelease( String owner, String repo) async { HttpClient client = HttpClient(); final req = await client.getUrl( Uri.parse('https://api.github.com/repos/$owner/$repo/releases/latest')); final response = await req.close(); if (response.statusCode == HttpStatus.ok) { final body = await response.transform(utf8.decoder).join(); return json.decode(body); } else { throw Exception( 'Failed to get latest release, status code: ${response.statusCode}'); } } String getTag() { return data['tag_name']; } Future hasNewVersion() async { final latestVersion = getTag(); final currentVersion = _versionName; log('latestVersion: $latestVersion, currentVersion: $currentVersion'); // return true; return _extractNumbers(latestVersion) > _extractNumbers(currentVersion); } String getApkDownloadUrl() { final assets = data['assets']; for (var asset in assets) { if (asset['name'].contains(_systemABI)) { return asset['browser_download_url']; } } throw Exception('Failed to get apk download url'); } String getUpdateContent() { return data['body']; } String getHtmlUrl() { return data['html_url']; } // 1.24.011609 to Int static int _extractNumbers(String input) { final s = input.replaceAll(RegExp(r'[^0-9]'), ''); return int.parse(s); } } ================================================ FILE: lib/widgets/switch_floating_action_button.dart ================================================ import 'package:flutter/material.dart'; class SwitchFloatingButton extends StatefulWidget { final bool isSwitch; final ValueChanged onSwitchChange; const SwitchFloatingButton( {super.key, required this.isSwitch, required this.onSwitchChange}); @override State createState() => _SwitchFloatingButtonState(); } class _SwitchFloatingButtonState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; late final Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 200), ); _animation = Tween(begin: 0.0, end: 1.0).animate(_controller); } @override Widget build(BuildContext context) { final icon = widget.isSwitch ? const Icon(Icons.stop, size: 48) : const Icon(Icons.send, size: 32); return FloatingActionButton( onPressed: () { if (widget.isSwitch && _controller.isCompleted) { _controller.reverse(from: 0.5); } else { _controller.forward(from: 0.5); } widget.onSwitchChange(!widget.isSwitch); }, backgroundColor: widget.isSwitch ? Theme.of(context).colorScheme.inversePrimary : Theme.of(context).colorScheme.primaryContainer, elevation: 8.0, shape: const CircleBorder(), child: RotationTransition( turns: _animation, child: icon, ), ); } @override void dispose() { super.dispose(); _controller.dispose(); } } ================================================ FILE: openlist-lib/openlistlib/common.go ================================================ package openlistlib import "net" func GetOutboundIP() (net.IP, error) { conn, err := net.Dial("udp", "8.8.8.8:80") if err != nil { return nil, err } defer conn.Close() localAddr := conn.LocalAddr().(*net.UDPAddr) return localAddr.IP, nil } func GetOutboundIPString() string { netIp, err := GetOutboundIP() if err != nil { return "localhost" } return netIp.String() } ================================================ FILE: openlist-lib/openlistlib/internal/log.go ================================================ package internal import log "github.com/sirupsen/logrus" type MyFormatter struct { log.Formatter OnLog func(entry *log.Entry) } func (f *MyFormatter) Format(entry *log.Entry) ([]byte, error) { f.OnLog(entry) return nil, nil } ================================================ FILE: openlist-lib/openlistlib/server.go ================================================ package openlistlib import ( "errors" "time" "github.com/OpenListTeam/OpenList/v4/internal/bootstrap" "github.com/OpenListTeam/OpenList/v4/internal/db" "github.com/OpenListTeam/OpenList/v4/openlistlib/internal" "github.com/OpenListTeam/OpenList/v4/pkg/utils" log "github.com/sirupsen/logrus" ) type LogCallback interface { OnLog(level int16, time int64, message string) } type Event interface { OnStartError(t string, err string) OnShutdown(t string) OnProcessExit(code int) } var startFailedHookUuid = "" var shutdownHookUuid = "" var logFormatter *internal.MyFormatter func Init(event Event, cb LogCallback) error { if startFailedHookUuid != "" { bootstrap.RemoveEndpointStartFailedHook(startFailedHookUuid) startFailedHookUuid = "" } if shutdownHookUuid != "" { bootstrap.RemoveEndpointShutdownHook(shutdownHookUuid) shutdownHookUuid = "" } bootstrap.Init() startFailedHookUuid = bootstrap.RegisterEndpointStartFailedHook(event.OnStartError) shutdownHookUuid = bootstrap.RegisterEndpointShutdownHook(event.OnShutdown) logFormatter = &internal.MyFormatter{ OnLog: func(entry *log.Entry) { cb.OnLog(int16(entry.Level), entry.Time.UnixMilli(), entry.Message) }, } if utils.Log == nil { return errors.New("utils.log is nil") } else { utils.Log.SetFormatter(logFormatter) utils.Log.ExitFunc = event.OnProcessExit } return nil } func IsRunning(t string) bool { return bootstrap.IsRunning(t) } // Start starts the server func Start() { bootstrap.Start() } // Shutdown timeout 毫秒 func Shutdown(timeout int64) (err error) { timeoutDuration := time.Duration(timeout) * time.Millisecond bootstrap.Shutdown(timeoutDuration) // Force database sync before shutdown ForceDBSync() //bootstrap.Release() return nil } // ForceDBSync forces SQLite WAL checkpoint to sync data to main database file func ForceDBSync() error { log.Info("Forcing database sync (WAL checkpoint)...") // Get the database instance and execute WAL checkpoint gormDB := db.GetDb() if gormDB != nil { sqlDB, err := gormDB.DB() if err != nil { log.Errorf("Failed to get database connection: %v", err) return err } // Execute WAL checkpoint with TRUNCATE mode to force sync and remove WAL files _, err = sqlDB.Exec("PRAGMA wal_checkpoint(TRUNCATE)") if err != nil { log.Errorf("Failed to execute WAL checkpoint: %v", err) return err } // Also execute synchronous commit to ensure data is written to disk _, err = sqlDB.Exec("PRAGMA synchronous=FULL") if err != nil { log.Warnf("Failed to set synchronous mode: %v", err) } log.Info("Database sync completed successfully") } else { log.Warn("Database instance is nil, skipping sync") } return nil } ================================================ FILE: openlist-lib/openlistlib/settings.go ================================================ package openlistlib import ( "github.com/OpenListTeam/OpenList/v4/cmd" "github.com/OpenListTeam/OpenList/v4/cmd/flags" "github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/pkg/utils" ) func SetConfigData(path string) { flags.DataDir = path } func SetConfigLogStd(b bool) { flags.LogStd = b } func SetConfigDebug(b bool) { flags.Debug = b } func SetConfigNoPrefix(b bool) { flags.NoPrefix = b } func SetAdminPassword(pwd string) { admin, err := op.GetAdmin() if err != nil { utils.Log.Errorf("failed get admin user: %+v", err) return } admin.SetPassword(pwd) if err := op.UpdateUser(admin); err != nil { utils.Log.Errorf("failed update admin user: %+v", err) return } utils.Log.Infof("admin user has been updated:") utils.Log.Infof("username: %s", admin.Username) utils.Log.Infof("password: %s", pwd) cmd.DelAdminCacheOnline() } ================================================ FILE: openlist-lib/scripts/clear.sh ================================================ #!/bin/bash mkdir /tmp/openlist rm -rf /tmp/openlist/* cp -r ../scripts /tmp/openlist cp -r ../openlistlib /tmp/openlist rm -rf ../* cp -r /tmp/openlist/* ../ ================================================ FILE: openlist-lib/scripts/fix_ios_dependencies.sh ================================================ #!/bin/bash echo "Fixing iOS incompatible dependencies by patching rclone source..." # Find the module root if [ -f go.mod ]; then MODULE_ROOT="." elif [ -f ../go.mod ]; then MODULE_ROOT=".." cd .. else echo "Error: Cannot find go.mod" exit 1 fi echo "Working in module root: $(pwd)" # Check for rclone dependency echo "Checking for rclone dependency..." RCLONE_VERSION=$(go list -m all | grep "github.com/rclone/rclone" | awk '{print $2}' || echo "") if [ -z "$RCLONE_VERSION" ]; then echo "No rclone dependency found, skipping patch" exit 0 fi echo "Found rclone version: $RCLONE_VERSION" # Function to create minimal local rclone (backup method) create_minimal_rclone() { echo "Using backup method: creating minimal local rclone module..." LOCAL_RCLONE_DIR="./local_rclone" rm -rf "$LOCAL_RCLONE_DIR" 2>/dev/null || true # Create the directory structure mkdir -p "$LOCAL_RCLONE_DIR/lib/buildinfo" # Create a minimal go.mod for the local rclone cat > "$LOCAL_RCLONE_DIR/go.mod" << 'GOMOD_EOF' module github.com/rclone/rclone go 1.19 GOMOD_EOF # Create the patched osversion.go file cat > "$LOCAL_RCLONE_DIR/lib/buildinfo/osversion.go" << 'OSVERSION_EOF' //go:build !windows package buildinfo // GetOSVersion returns OS version, kernel and bitness func GetOSVersion() (osVersion, osKernel string) { return } OSVERSION_EOF # Add replace directive to use local copy echo "Adding replace directive to use local rclone copy..." go mod edit -replace github.com/rclone/rclone="./local_rclone" return 0 } # Try primary method first echo "Attempting primary method: patching existing rclone..." # Download dependencies to get the source echo "Downloading dependencies..." go mod download # Find the rclone module path RCLONE_PATH=$(go list -m -f '{{.Dir}}' github.com/rclone/rclone 2>/dev/null) if [ -z "$RCLONE_PATH" ]; then echo "Could not find rclone module path, trying alternative method..." # Try to find it in GOPATH/pkg/mod GOPATH_MOD=$(go env GOPATH)/pkg/mod RCLONE_PATH=$(find "$GOPATH_MOD" -name "rclone@$RCLONE_VERSION" -type d 2>/dev/null | head -1) fi if [ -z "$RCLONE_PATH" ]; then echo "Could not locate rclone source directory, using backup method..." create_minimal_rclone else echo "Found rclone source at: $RCLONE_PATH" # Path to the problematic file OSVERSION_FILE="$RCLONE_PATH/lib/buildinfo/osversion.go" if [ ! -f "$OSVERSION_FILE" ]; then echo "osversion.go file not found, using backup method..." create_minimal_rclone else echo "Found osversion.go at: $OSVERSION_FILE" # Try to patch the existing file LOCAL_RCLONE_DIR="./local_rclone" rm -rf "$LOCAL_RCLONE_DIR" 2>/dev/null || true mkdir -p "$LOCAL_RCLONE_DIR" echo "Copying rclone source to local directory..." if cp -r "$RCLONE_PATH"/* "$LOCAL_RCLONE_DIR/" 2>/dev/null; then # Fix permissions on the copied files echo "Fixing permissions on copied files..." find "$LOCAL_RCLONE_DIR" -type f -exec chmod u+w {} \; 2>/dev/null || true find "$LOCAL_RCLONE_DIR" -type d -exec chmod u+w {} \; 2>/dev/null || true # Update the file path to local copy OSVERSION_FILE="$LOCAL_RCLONE_DIR/lib/buildinfo/osversion.go" # Try to patch the file if [ -f "$OSVERSION_FILE" ] && [ -w "$OSVERSION_FILE" ]; then echo "Patching osversion.go for iOS compatibility..." # Create the patch content cat > "$OSVERSION_FILE" << 'PATCH_EOF' //go:build !windows package buildinfo // GetOSVersion returns OS version, kernel and bitness func GetOSVersion() (osVersion, osKernel string) { return } PATCH_EOF # Add replace directive to use local copy echo "Adding replace directive to use local rclone copy..." go mod edit -replace github.com/rclone/rclone="./local_rclone" else echo "Cannot write to copied file, using backup method..." create_minimal_rclone fi else echo "Failed to copy rclone source, using backup method..." create_minimal_rclone fi fi fi # Verify the patch echo "Verifying patch..." FINAL_OSVERSION_FILE="./local_rclone/lib/buildinfo/osversion.go" if [ -f "$FINAL_OSVERSION_FILE" ]; then echo "✅ Patched osversion.go found" echo "Content:" cat "$FINAL_OSVERSION_FILE" echo "" # Check if the patch was applied correctly if grep -q "host.PlatformInformation\|host.KernelVersion\|host.KernelArch" "$FINAL_OSVERSION_FILE"; then echo "❌ WARNING: File still contains problematic host function calls!" echo "Patch may not have been applied correctly." exit 1 else echo "✅ Patch applied successfully - no problematic host function calls found" fi else echo "❌ Failed to create patched osversion.go" exit 1 fi # Clean and rebuild to apply changes echo "Cleaning and rebuilding module with patched rclone..." go mod tidy go mod download echo "✅ iOS dependency fix completed by patching rclone source" echo "The problematic gopsutil calls in rclone have been disabled for iOS builds" ================================================ FILE: openlist-lib/scripts/gobind.sh ================================================ #!/bin/bash # Build version information builtAt="${OPENLIST_BUILT_AT:-$(date +'%F %T %z')}" gitAuthor="${OPENLIST_GIT_AUTHOR:-The OpenList Projects Contributors }" gitCommit="${OPENLIST_GIT_COMMIT:-$(git log --pretty=format:'%h' -1 2>/dev/null || echo 'unknown')}" version="${OPENLIST_VERSION:-dev}" webVersion="${OPENLIST_WEB_VERSION:-rolling}" echo "Building with version info:" echo " Version: $version" echo " WebVersion: $webVersion" echo " GitCommit: $gitCommit" echo " BuiltAt: $builtAt" # Construct ldflags ldflags="-s -w" ldflags="$ldflags -X 'github.com/OpenListTeam/OpenList/v4/internal/conf.BuiltAt=$builtAt'" ldflags="$ldflags -X 'github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=$gitAuthor'" ldflags="$ldflags -X 'github.com/OpenListTeam/OpenList/v4/internal/conf.GitCommit=$gitCommit'" ldflags="$ldflags -X 'github.com/OpenListTeam/OpenList/v4/internal/conf.Version=$version'" ldflags="$ldflags -X 'github.com/OpenListTeam/OpenList/v4/internal/conf.WebVersion=$webVersion'" # First check if we're in the right place echo "Starting Android build from: $(pwd)" # For Android, we need to find the bindable package directory, not just go.mod # The original approach was correct - look for openlistlib directory if [ -d ../openlistlib ]; then echo "Found openlistlib directory, using that for Android build" cd ../openlistlib || exit else echo "Searching for bindable package directory..." cd ../ || exit # Look for directories that might contain bindable packages if [ -d openlistlib ]; then echo "Found openlistlib in current directory" cd openlistlib || exit elif [ -d cmd/openlistlib ]; then echo "Found openlistlib in cmd directory" cd cmd/openlistlib || exit else echo "Error: Cannot find openlistlib directory for Android binding" echo "Current directory: $(pwd)" echo "Directory contents:" ls -la echo "Looking for Go files that might be bindable..." find . -name "*.go" -type f | head -10 exit 1 fi fi echo "Current directory: $(pwd)" echo "Building OpenList for Android..." # Check if this directory has Go files suitable for binding if ! ls *.go >/dev/null 2>&1; then echo "Warning: No Go files found in current directory" echo "Directory contents:" ls -la fi if [ "$1" == "debug" ]; then gomobile bind -ldflags "$ldflags" -v -androidapi 19 -target="android/arm64" else gomobile bind -ldflags "$ldflags" -v -androidapi 19 fi echo "Moving aar and jar files to android/app/libs" mkdir -p ../../android/app/libs mv -f ./*.aar ../../android/app/libs mv -f ./*.jar ../../android/app/libs ================================================ FILE: openlist-lib/scripts/gobind_ios.sh ================================================ #!/bin/bash # Build version information builtAt="${OPENLIST_BUILT_AT:-$(date +'%F %T %z')}" gitAuthor="${OPENLIST_GIT_AUTHOR:-The OpenList Projects Contributors }" gitCommit="${OPENLIST_GIT_COMMIT:-$(git log --pretty=format:'%h' -1 2>/dev/null || echo 'unknown')}" version="${OPENLIST_VERSION:-dev}" webVersion="${OPENLIST_WEB_VERSION:-rolling}" echo "Building with version info:" echo " Version: $version" echo " WebVersion: $webVersion" echo " GitCommit: $gitCommit" echo " BuiltAt: $builtAt" # Construct ldflags ldflags="-s -w" ldflags="$ldflags -X 'github.com/OpenListTeam/OpenList/v4/internal/conf.BuiltAt=$builtAt'" ldflags="$ldflags -X 'github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=$gitAuthor'" ldflags="$ldflags -X 'github.com/OpenListTeam/OpenList/v4/internal/conf.GitCommit=$gitCommit'" ldflags="$ldflags -X 'github.com/OpenListTeam/OpenList/v4/internal/conf.Version=$version'" ldflags="$ldflags -X 'github.com/OpenListTeam/OpenList/v4/internal/conf.WebVersion=$webVersion'" echo "Starting iOS build from: $(pwd)" # Find openlistlib directory if [ -d ../openlistlib ]; then cd ../openlistlib || exit elif [ -d openlistlib ]; then cd openlistlib || exit else echo "Error: Cannot find openlistlib directory" exit 1 fi echo "Current directory: $(pwd)" # Check if gomobile is available if ! command -v gomobile &> /dev/null; then echo "Error: gomobile not found. Please run init_gomobile.sh first." exit 1 fi echo "Go version: $(go version)" # Work from module root if go.mod exists in parent if [ -f ../go.mod ]; then echo "Found go.mod in parent directory" cd .. # Fix iOS incompatible dependencies echo "Fixing iOS incompatible dependencies..." chmod +x scripts/fix_ios_dependencies.sh ./scripts/fix_ios_dependencies.sh # Update mobile packages echo "Updating mobile packages..." go get -u golang.org/x/mobile/... go install golang.org/x/mobile/cmd/gobind@latest go install golang.org/x/mobile/cmd/gomobile@latest # Reinitialize gomobile echo "Reinitializing gomobile..." gomobile clean || true gomobile init # Set CGO environment for iOS export CGO_ENABLED=1 # Build for iOS with iOS-specific build tags to exclude incompatible packages echo "Starting iOS build from module root..." echo "CGO_ENABLED: $CGO_ENABLED" # Use build tags to exclude problematic packages on iOS echo "Attempting gomobile bind with iOS tags..." gomobile bind -ldflags "$ldflags" -v -target="ios" -tags="ios,mobile" ./openlistlib 2>&1 | tee ios_build.log # Check the exit status if [ $? -ne 0 ]; then echo "Error: gomobile bind failed" echo "=== Build log ===" cat ios_build.log 2>/dev/null || echo "No build log available" echo "=== End build log ===" # Try to get more specific error information echo "Checking for specific issues..." # Check if it's a dependency issue if grep -q "cannot find package\|no Go files\|build constraints exclude all Go files" ios_build.log; then echo "Detected dependency or build constraint issues" # Try with minimal tags echo "Retrying with minimal build tags..." gomobile bind -ldflags "$ldflags" -v -target="ios" ./openlistlib 2>&1 | tee ios_build_minimal.log if [ $? -ne 0 ]; then echo "Minimal build also failed:" cat ios_build_minimal.log 2>/dev/null || echo "No minimal build log available" exit 1 fi else echo "Unknown build error, exiting" exit 1 fi fi echo "Listing generated files in current directory:" ls -la *.xcframework 2>/dev/null || echo "No .xcframework files found in current directory" # Also check if any frameworks were generated with different patterns ls -la Openlistlib.xcframework 2>/dev/null || echo "Openlistlib.xcframework not found" ls -la openlistlib.xcframework 2>/dev/null || echo "openlistlib.xcframework not found" # Find the Flutter project root by looking for pubspec.yaml echo "Locating Flutter project root..." FLUTTER_ROOT="" CURRENT_DIR=$(pwd) # Check various possible locations for pubspec.yaml if [ -f "pubspec.yaml" ]; then FLUTTER_ROOT="." echo "Found Flutter project root at current directory" elif [ -f "../pubspec.yaml" ]; then FLUTTER_ROOT="../" echo "Found Flutter project root at ../" elif [ -f "../../pubspec.yaml" ]; then FLUTTER_ROOT="../../" echo "Found Flutter project root at ../../" elif [ -f "../../../pubspec.yaml" ]; then FLUTTER_ROOT="../../../" echo "Found Flutter project root at ../../../" else # Try to find it by searching upwards SEARCH_DIR="$CURRENT_DIR" for i in {1..5}; do SEARCH_DIR="$(dirname "$SEARCH_DIR")" if [ -f "$SEARCH_DIR/pubspec.yaml" ]; then FLUTTER_ROOT=$(realpath --relative-to="$CURRENT_DIR" "$SEARCH_DIR") echo "Found Flutter project root at: $FLUTTER_ROOT" break fi done if [ -z "$FLUTTER_ROOT" ]; then echo "Warning: Cannot find Flutter project root (pubspec.yaml), using default relative path" FLUTTER_ROOT="../../" fi fi echo "Using Flutter project root: $FLUTTER_ROOT" echo "Creating iOS Frameworks directory at: ${FLUTTER_ROOT}ios/Frameworks" mkdir -p "${FLUTTER_ROOT}ios/Frameworks" # Check if xcframework files exist before moving if ls *.xcframework 1> /dev/null 2>&1; then echo "Moving xcframework files to Flutter iOS Frameworks directory..." # Ensure the target directory exists mkdir -p "${FLUTTER_ROOT}ios/Frameworks" # Copy files to Flutter project for framework in *.xcframework; do echo "Copying $framework to ${FLUTTER_ROOT}ios/Frameworks/" cp -rf "$framework" "${FLUTTER_ROOT}ios/Frameworks/" done echo "✅ iOS framework build completed successfully" echo "Files in Flutter iOS Frameworks directory:" ls -lah "${FLUTTER_ROOT}ios/Frameworks/" # Verify the files are in the expected location EXPECTED_PATH="${FLUTTER_ROOT}ios/Frameworks" ABSOLUTE_EXPECTED_PATH=$(cd "$EXPECTED_PATH" && pwd) if [ -d "$EXPECTED_PATH" ] && [ "$(ls -A "$EXPECTED_PATH")" ]; then echo "✅ Framework files successfully placed in: $ABSOLUTE_EXPECTED_PATH" # List all frameworks found echo "" echo "=== Frameworks Ready ===" find "$EXPECTED_PATH" -name "*.xcframework" -type d -exec basename {} \; else echo "❌ Warning: Framework files may not be in the expected location" fi else echo "Warning: No .xcframework files were generated" echo "Checking if files were generated with different names..." ls -la *.framework 2>/dev/null || echo "No .framework files found either" ls -la openlistlib* 2>/dev/null || echo "No openlistlib files found" exit 1 fi else echo "Error: No go.mod found in parent directory" exit 1 fi ================================================ FILE: openlist-lib/scripts/init_gomobile.sh ================================================ #!/bin/bash echo "Installing gomobile and dependencies..." # Install gomobile command echo "Installing gomobile command..." go install golang.org/x/mobile/cmd/gomobile@latest || { echo "Failed to install gomobile" exit 1 } # Install gobind command (needed for iOS) echo "Installing gobind command..." go install golang.org/x/mobile/cmd/gobind@latest || { echo "Failed to install gobind" exit 1 } # Install bind packages echo "Installing bind packages..." go get golang.org/x/mobile/bind@latest || { echo "Failed to install golang.org/x/mobile/bind" exit 1 } go get golang.org/x/mobile/bind/objc@latest || { echo "Failed to install golang.org/x/mobile/bind/objc" exit 1 } # Initialize gomobile echo "Initializing gomobile..." gomobile init || { echo "Failed to initialize gomobile" exit 1 } echo "Gomobile initialization completed successfully" # Verify installation echo "Verifying installation..." echo "gomobile version: $(gomobile version 2>/dev/null || echo 'version command failed')" echo "gobind available: $(command -v gobind >/dev/null && echo 'yes' || echo 'no')" ================================================ FILE: openlist-lib/scripts/init_openlist.sh ================================================ #!/bin/bash GIT_REPO="https://github.com/OpenListTeam/OpenList.git" TAG_NAME=$(git -c 'versionsort.suffix=-' ls-remote --exit-code --refs --sort='version:refname' --tags $GIT_REPO | tail -n 1 | cut -d'/' -f3) echo "OpenList - ${TAG_NAME}" rm -rf ./src unset GIT_WORK_TREE git clone --branch "$TAG_NAME" https://github.com/OpenListTeam/OpenList.git ./src rm -rf ./src/.git echo "Checking cloned source structure:" ls -la ./src/ # Move the entire OpenList source to the parent directory echo "Moving OpenList source files..." mv -f ./src/* ../ rm -rf ./src echo "Checking moved files in parent directory:" ls -la ../ # Check if we have the main go.mod in the right place if [ -f ../go.mod ]; then echo "Found go.mod in parent directory" cd ../ go mod edit -replace github.com/djherbis/times@v1.6.0=github.com/jing332/times@latest echo "OpenList source initialization completed" else echo "Error: go.mod not found after moving files" echo "Contents of parent directory:" ls -la ../ exit 1 fi ================================================ FILE: openlist-lib/scripts/init_web.sh ================================================ #!/bin/bash echo "Initializing Web assets..." mkdir -p dist # Function to fetch release info with retries and proxy fallback fetch_release_info() { local attempt=1 local max_attempts=3 local api_url="https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/latest" local proxy_url="https://ghproxy.lvedong.eu.org/https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/latest" # First try direct API echo "Trying direct GitHub API..." while [ $attempt -le $max_attempts ]; do echo "Direct API attempt $attempt/$max_attempts..." RELEASE_INFO=$(curl -fsSL --max-time 10 \ -H "Accept: application/vnd.github.v3+json" \ "$api_url" 2>/dev/null) local curl_exit_code=$? if [ $curl_exit_code -eq 0 ] && [ -n "$RELEASE_INFO" ]; then echo "Successfully fetched release info via direct API on attempt $attempt" return 0 else echo "Direct API attempt $attempt failed (exit code: $curl_exit_code)" if [ $attempt -lt $max_attempts ]; then echo "Waiting 3 seconds before retry..." sleep 3 fi fi attempt=$((attempt + 1)) done echo "Direct API failed after $max_attempts attempts, trying proxy..." # Try proxy API attempt=1 while [ $attempt -le $max_attempts ]; do echo "Proxy API attempt $attempt/$max_attempts..." RELEASE_INFO=$(curl -fsSL --max-time 15 \ -H "Accept: application/vnd.github.v3+json" \ "$proxy_url" 2>/dev/null) local curl_exit_code=$? if [ $curl_exit_code -eq 0 ] && [ -n "$RELEASE_INFO" ]; then echo "Successfully fetched release info via proxy on attempt $attempt" return 0 else echo "Proxy attempt $attempt failed (exit code: $curl_exit_code)" if [ $attempt -lt $max_attempts ]; then echo "Waiting 5 seconds before retry..." sleep 5 fi fi attempt=$((attempt + 1)) done echo "Failed to fetch release info via both direct API and proxy" return 1 } # Get release info from GitHub API with retries echo "Fetching latest release information..." if ! fetch_release_info; then echo "Error: Failed to fetch release info from GitHub API" exit 1 fi # Parse download URL with better pattern matching echo "Parsing download URL..." if command -v jq >/dev/null 2>&1; then DOWNLOAD_URL=$(echo "$RELEASE_INFO" | jq -r '.assets[] | select(.browser_download_url | test("openlist-frontend-dist.*\\.tar\\.gz$") and (test("openlist-frontend-dist-lite") | not)) | .browser_download_url') else # Fallback without jq - look for versioned filename DOWNLOAD_URL=$(echo "$RELEASE_INFO" | grep -o '"browser_download_url":"[^"]*openlist-frontend-dist-v[^"]*\.tar\.gz"' | grep -v 'lite' | head -1 | sed 's/.*"browser_download_url":"\([^"]*\)".*/\1/') # If versioned pattern doesn't work, try general pattern if [ -z "$DOWNLOAD_URL" ]; then DOWNLOAD_URL=$(echo "$RELEASE_INFO" | grep -o '"browser_download_url":"[^"]*openlist-frontend-dist[^"]*\.tar\.gz"' | grep -v 'lite' | head -1 | sed 's/.*"browser_download_url":"\([^"]*\)".*/\1/') fi fi if [ -z "$DOWNLOAD_URL" ] || [ "$DOWNLOAD_URL" = "null" ]; then echo "Error: Could not determine download URL" exit 1 fi echo "Download URL: $DOWNLOAD_URL" # Function to download file with retries and proxy fallback download_file() { local url="$1" local output="$2" local attempt=1 local max_attempts=3 # First try direct download echo "Trying direct download..." while [ $attempt -le $max_attempts ]; do echo "Direct download attempt $attempt/$max_attempts..." # Try download if curl -fsSL --max-time 30 -o "$output" "$url"; then echo "Direct download successful on attempt $attempt" return 0 else local curl_exit_code=$? echo "Direct download attempt $attempt failed (exit code: $curl_exit_code)" # Remove partial file if it exists rm -f "$output" if [ $attempt -lt $max_attempts ]; then echo "Waiting 3 seconds before retry..." sleep 3 fi fi attempt=$((attempt + 1)) done # Try proxy download if direct failed echo "Direct download failed, trying proxy download..." local proxy_url="https://ghproxy.lvedong.eu.org/$url" attempt=1 while [ $attempt -le $max_attempts ]; do echo "Proxy download attempt $attempt/$max_attempts..." # Try proxy download if curl -fsSL --max-time 45 -o "$output" "$proxy_url"; then echo "Proxy download successful on attempt $attempt" return 0 else local curl_exit_code=$? echo "Proxy download attempt $attempt failed (exit code: $curl_exit_code)" # Remove partial file if it exists rm -f "$output" if [ $attempt -lt $max_attempts ]; then echo "Waiting 5 seconds before retry..." sleep 5 fi fi attempt=$((attempt + 1)) done echo "Failed to download via both direct and proxy methods" return 1 } # Download the file with retries echo "Downloading web assets..." if ! download_file "$DOWNLOAD_URL" "dist.tar.gz"; then echo "Error: Failed to download web assets after multiple attempts" exit 1 fi # Extract and install echo "Extracting web assets..." tar -zxf dist.tar.gz -C dist || { echo "Error: Failed to extract archive" exit 1 } echo "Installing web assets..." rm -rf ../public/dist mv -f dist ../public rm -f dist.tar.gz echo "Web assets initialization completed" ================================================ FILE: openlist-lib/scripts/init_web_ios.sh ================================================ #!/bin/bash echo "Initializing Web assets for iOS build..." # Create dist directory mkdir -p dist # Use a more reliable approach for iOS with retry mechanism # First, try to get the latest release info with better error handling echo "Fetching latest release information..." # Function to fetch release info with retries and proxy fallback fetch_release_info() { local attempt=1 local max_attempts=3 local api_url="https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/latest" local proxy_url="https://ghproxy.lvedong.eu.org/https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/latest" # First try direct API echo "Trying direct GitHub API..." while [ $attempt -le $max_attempts ]; do echo "Direct API attempt $attempt/$max_attempts..." RELEASE_INFO=$(curl -fsSL --max-time 10 \ -H "Accept: application/vnd.github.v3+json" \ -H "User-Agent: OpenList-iOS-Builder" \ "$api_url" 2>/dev/null) local curl_exit_code=$? if [ $curl_exit_code -eq 0 ] && [ -n "$RELEASE_INFO" ]; then echo "Successfully fetched release info via direct API on attempt $attempt" return 0 else echo "Direct API attempt $attempt failed (exit code: $curl_exit_code)" if [ $attempt -lt $max_attempts ]; then echo "Waiting 3 seconds before retry..." sleep 3 fi fi attempt=$((attempt + 1)) done echo "Direct API failed after $max_attempts attempts, trying proxy..." # Try proxy API attempt=1 while [ $attempt -le $max_attempts ]; do echo "Proxy API attempt $attempt/$max_attempts..." RELEASE_INFO=$(curl -fsSL --max-time 15 \ -H "Accept: application/vnd.github.v3+json" \ -H "User-Agent: OpenList-iOS-Builder" \ "$proxy_url" 2>/dev/null) local curl_exit_code=$? if [ $curl_exit_code -eq 0 ] && [ -n "$RELEASE_INFO" ]; then echo "Successfully fetched release info via proxy on attempt $attempt" return 0 else echo "Proxy attempt $attempt failed (exit code: $curl_exit_code)" if [ $attempt -lt $max_attempts ]; then echo "Waiting 5 seconds before retry..." sleep 5 fi fi attempt=$((attempt + 1)) done echo "Failed to fetch release info via both direct API and proxy" return 1 } # Try to fetch release info with retries if ! fetch_release_info; then echo "Cannot proceed without API access" exit 1 else echo "Successfully fetched release info, parsing download URL..." # Check if jq is available if command -v jq >/dev/null 2>&1; then echo "Using jq to parse JSON..." DOWNLOAD_URL=$(echo "$RELEASE_INFO" | jq -r '.assets[] | select(.browser_download_url | test("openlist-frontend-dist.*\\.tar\\.gz$") and (test("openlist-frontend-dist-lite") | not)) | .browser_download_url') echo "jq found URL: $DOWNLOAD_URL" else echo "jq not available, using grep/sed to parse JSON..." # More robust fallback parsing without jq # Look for openlist-frontend-dist-v*.tar.gz but not lite version DOWNLOAD_URL=$(echo "$RELEASE_INFO" | grep -o '"browser_download_url":"[^"]*openlist-frontend-dist-v[^"]*\.tar\.gz"' | grep -v 'lite' | head -1 | sed 's/.*"browser_download_url":"\([^"]*\)".*/\1/') echo "grep/sed found URL: $DOWNLOAD_URL" # If the above doesn't work, try a more general pattern if [ -z "$DOWNLOAD_URL" ] || [ "$DOWNLOAD_URL" = "null" ]; then echo "Trying more general pattern..." DOWNLOAD_URL=$(echo "$RELEASE_INFO" | grep -o '"browser_download_url":"[^"]*openlist-frontend-dist[^"]*\.tar\.gz"' | grep -v 'lite' | head -1 | sed 's/.*"browser_download_url":"\([^"]*\)".*/\1/') echo "General pattern found URL: $DOWNLOAD_URL" fi fi fi # Validate download URL if [ -z "$DOWNLOAD_URL" ] || [ "$DOWNLOAD_URL" = "null" ]; then echo "Error: Could not determine download URL from API response" echo "API response preview:" echo "$RELEASE_INFO" | head -20 exit 1 fi echo "Download URL: $DOWNLOAD_URL" # Function to download file with retries and proxy fallback download_file() { local url="$1" local output="$2" local attempt=1 local max_attempts=3 # First try direct download echo "Trying direct download..." while [ $attempt -le $max_attempts ]; do echo "Direct download attempt $attempt/$max_attempts..." # Try download if curl -fsSL --max-time 30 -o "$output" "$url"; then echo "Direct download successful on attempt $attempt" return 0 else local curl_exit_code=$? echo "Direct download attempt $attempt failed (exit code: $curl_exit_code)" # Remove partial file if it exists rm -f "$output" if [ $attempt -lt $max_attempts ]; then echo "Waiting 3 seconds before retry..." sleep 3 fi fi attempt=$((attempt + 1)) done # Try proxy download if direct failed echo "Direct download failed, trying proxy download..." local proxy_url="https://ghproxy.lvedong.eu.org/$url" attempt=1 while [ $attempt -le $max_attempts ]; do echo "Proxy download attempt $attempt/$max_attempts..." # Try proxy download if curl -fsSL --max-time 45 -o "$output" "$proxy_url"; then echo "Proxy download successful on attempt $attempt" return 0 else local curl_exit_code=$? echo "Proxy download attempt $attempt failed (exit code: $curl_exit_code)" # Remove partial file if it exists rm -f "$output" if [ $attempt -lt $max_attempts ]; then echo "Waiting 5 seconds before retry..." sleep 5 fi fi attempt=$((attempt + 1)) done echo "Failed to download via both direct and proxy methods" return 1 } # Download the file with retries echo "Downloading web assets..." if ! download_file "$DOWNLOAD_URL" "dist.tar.gz"; then echo "Error: Failed to download web assets after multiple attempts" exit 1 fi # Verify the downloaded file if [ ! -f dist.tar.gz ] || [ ! -s dist.tar.gz ]; then echo "Error: Downloaded file is empty or missing" exit 1 fi echo "Downloaded file size: $(ls -lh dist.tar.gz | awk '{print $5}')" # Extract the archive echo "Extracting web assets..." if tar -zxf dist.tar.gz -C dist 2>/dev/null; then echo "Extraction successful" else echo "Error: Failed to extract archive" echo "File info:" file dist.tar.gz 2>/dev/null || echo "file command not available" ls -la dist.tar.gz exit 1 fi # Move to final location echo "Installing web assets..." rm -rf ../public/dist if [ -d dist ]; then mv dist ../public/ echo "Web assets installed successfully" else echo "Error: Extracted directory not found" exit 1 fi # Cleanup rm -f dist.tar.gz echo "Web assets initialization completed" ================================================ FILE: openlist_version ================================================ v4.2.1 ================================================ FILE: pigeon_config.yaml ================================================ # Pigeon configuration for OpenList Mobile # Generate platform-specific API code from Dart definitions input: pigeons/pigeon.dart dart_out: lib/generated_api.dart java_out: android/app/src/main/java/com/openlist/pigeon/GeneratedApi.java java_package: com.openlist.pigeon swift_out: ios/Runner/GeneratedPluginRegistrant.swift ================================================ FILE: pigeons/pigeon.dart ================================================ import 'package:pigeon/pigeon.dart'; @HostApi() abstract class AppConfig { bool isWakeLockEnabled(); void setWakeLockEnabled(bool enabled); bool isStartAtBootEnabled(); void setStartAtBootEnabled(bool enabled); bool isAutoCheckUpdateEnabled(); void setAutoCheckUpdateEnabled(bool enabled); bool isAutoOpenWebPageEnabled(); void setAutoOpenWebPageEnabled(bool enabled); String getDataDir(); void setDataDir(String dir); bool isSilentJumpAppEnabled(); void setSilentJumpAppEnabled(bool enabled); } @HostApi() abstract class NativeCommon { bool startActivityFromUri(String intentUri); int getDeviceSdkInt(); String getDeviceCPUABI(); String getVersionName(); int getVersionCode(); void toast(String msg); void longToast(String msg); } @HostApi() abstract class Android { void addShortcut(); void startService(); void setAdminPwd(String pwd); int getOpenListHttpPort(); bool isRunning(); String getOpenListVersion(); } @FlutterApi() abstract class Event { void onServiceStatusChanged(bool isRunning); void onServerLog( int level, String time, String log, ); } ================================================ FILE: pigeons/run.cmd ================================================ flutter pub run pigeon --input pigeons/pigeon.dart^ --dart_out lib/generated_api.dart^ --java_out android/app/src/main/java/com/openlist/pigeon/GeneratedApi.java^ --java_package "com.openlist.pigeon" ` ================================================ FILE: pubspec.yaml ================================================ name: openlist_mobile description: "OpenList for Android and iOS" # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. version: 4.2.1+1 environment: sdk: '>=3.2.4 <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter intl: ^0.20.2 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 get: ^4.6.6 pigeon: ^26.0.0 flutter_inappwebview: ^6.0.0 android_intent_plus: ^5.3.0 flutter_svg: ^2.0.9 permission_handler: ^12.0.1 lazy_load_indexed_stack: ^1.1.0 animations: ^2.0.11 file_picker: ^10.2.0 fade_indexed_stack: ^0.2.2 dio: ^5.4.0 path_provider: ^2.1.2 open_filex: ^4.3.4 flutter_local_notifications: ^19.3.1 open_file_manager: ^2.0.1 shared_preferences: ^2.3.2 flutter_highlight: ^0.7.0 flutter_smooth_markdown: ^0.1.5 dev_dependencies: flutter_test: sdk: flutter # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^6.0.0 intl_utils: ^2.8.7 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: assets: - assets/openlist.svg # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages flutter_intl: enabled: true main_locale: zh ================================================ FILE: test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility in the flutter_test package. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:openlist_mobile/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(const MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Tap the '+' icon and trigger a frame. await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify that our counter has incremented. expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); }