Full Code of nini22P/Iris for AI

main 30f54182ae8a cached
202 files
1.2 MB
294.6k tokens
387 symbols
1 requests
Download .txt
Showing preview only (1,349K chars total). Download the full file or copy to clipboard to get everything.
Repository: nini22P/Iris
Branch: main
Commit: 30f54182ae8a
Files: 202
Total size: 1.2 MB

Directory structure:
gitextract_lt5ecz80/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .metadata
├── .vscode/
│   ├── launch.json
│   └── settings.json
├── CHANGELOG.md
├── LICENSE
├── PRIVACY.md
├── README.md
├── README_CN.md
├── analysis_options.yaml
├── android/
│   ├── .gitignore
│   ├── app/
│   │   ├── build.gradle
│   │   ├── proguard-rules.pro
│   │   └── src/
│   │       ├── debug/
│   │       │   └── AndroidManifest.xml
│   │       ├── main/
│   │       │   ├── AndroidManifest.xml
│   │       │   ├── kotlin/
│   │       │   │   └── nini22p/
│   │       │   │       └── iris/
│   │       │   │           └── MainActivity.kt
│   │       │   └── res/
│   │       │       ├── drawable/
│   │       │       │   └── launch_background.xml
│   │       │       ├── drawable-v21/
│   │       │       │   └── launch_background.xml
│   │       │       ├── mipmap-anydpi-v26/
│   │       │       │   ├── ic_launcher.xml
│   │       │       │   └── ic_launcher_round.xml
│   │       │       ├── values/
│   │       │       │   ├── ic_launcher_background.xml
│   │       │       │   └── styles.xml
│   │       │       └── values-night/
│   │       │           └── styles.xml
│   │       └── profile/
│   │           └── AndroidManifest.xml
│   ├── build.gradle
│   ├── gradle/
│   │   └── wrapper/
│   │       └── gradle-wrapper.properties
│   ├── gradle.properties
│   └── settings.gradle
├── devtools_options.yaml
├── extract_log.py
├── inno.iss
├── ios/
│   ├── .gitignore
│   ├── Flutter/
│   │   ├── AppFrameworkInfo.plist
│   │   ├── Debug.xcconfig
│   │   └── Release.xcconfig
│   ├── Runner/
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets/
│   │   │   ├── AppIcon.appiconset/
│   │   │   │   └── Contents.json
│   │   │   └── LaunchImage.imageset/
│   │   │       ├── Contents.json
│   │   │       └── README.md
│   │   ├── Base.lproj/
│   │   │   ├── LaunchScreen.storyboard
│   │   │   └── Main.storyboard
│   │   ├── Info.plist
│   │   └── 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
├── l10n.yaml
├── lib/
│   ├── globals.dart
│   ├── hooks/
│   │   ├── player/
│   │   │   ├── use_fvp_player.dart
│   │   │   └── use_media_kit_player.dart
│   │   ├── ui/
│   │   │   ├── use_full_screen.dart
│   │   │   ├── use_orientation.dart
│   │   │   └── use_resize_window.dart
│   │   ├── use_app_lifecycle.dart
│   │   ├── use_brightness.dart
│   │   ├── use_cover.dart
│   │   ├── use_gesture.dart
│   │   ├── use_keyboard.dart
│   │   └── use_volume.dart
│   ├── info.dart
│   ├── l10n/
│   │   ├── app_en.arb
│   │   ├── app_zh.arb
│   │   ├── iso_639.dart
│   │   └── languages.dart
│   ├── main.dart
│   ├── models/
│   │   ├── file.dart
│   │   ├── player.dart
│   │   ├── progress.dart
│   │   ├── storages/
│   │   │   ├── ftp.dart
│   │   │   ├── local.dart
│   │   │   ├── storage.dart
│   │   │   └── webdav.dart
│   │   └── store/
│   │       ├── app_state.dart
│   │       ├── history_state.dart
│   │       ├── play_queue_state.dart
│   │       ├── player_ui_state.dart
│   │       └── storage_state.dart
│   ├── oss_licenses.dart
│   ├── pages/
│   │   ├── home/
│   │   │   └── home.dart
│   │   └── player/
│   │       ├── audio.dart
│   │       ├── control_bar/
│   │       │   ├── control_bar.dart
│   │       │   ├── control_bar_slider.dart
│   │       │   ├── volume_control.dart
│   │       │   └── volume_slider.dart
│   │       ├── overlays/
│   │       │   ├── controls_overlay.dart
│   │       │   ├── gesture_overlay.dart
│   │       │   ├── minimal_progress_overlay.dart
│   │       │   └── speed_selector.dart
│   │       ├── player.dart
│   │       ├── player_view.dart
│   │       ├── title_bar.dart
│   │       └── video_view.dart
│   ├── store/
│   │   ├── persistent_store.dart
│   │   ├── use_app_store.dart
│   │   ├── use_history_store.dart
│   │   ├── use_play_queue_store.dart
│   │   ├── use_player_ui_store.dart
│   │   └── use_storage_store.dart
│   ├── theme.dart
│   ├── utils/
│   │   ├── check_content_type.dart
│   │   ├── check_data_source_type.dart
│   │   ├── data_migration.dart
│   │   ├── file_size_convert.dart
│   │   ├── files_sort.dart
│   │   ├── format_duration_to_minutes.dart
│   │   ├── get_latest_release.dart
│   │   ├── get_localizations.dart
│   │   ├── get_shuffle_play_queue.dart
│   │   ├── get_subtitle_map.dart
│   │   ├── logger.dart
│   │   ├── path.dart
│   │   ├── path_conv.dart
│   │   ├── platform.dart
│   │   ├── request_storage_permission.dart
│   │   └── url.dart
│   └── widgets/
│       ├── bottom_sheets/
│       │   └── show_open_link_bottom_sheet.dart
│       ├── card.dart
│       ├── chip.dart
│       ├── dialogs/
│       │   ├── show_folder_dialog.dart
│       │   ├── show_ftp_dialog.dart
│       │   ├── show_language_dialog.dart
│       │   ├── show_open_link_dialog.dart
│       │   ├── show_orientation_dialog.dart
│       │   ├── show_rate_dialog.dart
│       │   ├── show_release_dialog.dart
│       │   ├── show_theme_mode_dialog.dart
│       │   └── show_webdav_dialog.dart
│       ├── drag_area.dart
│       ├── popup.dart
│       └── popups/
│           ├── history.dart
│           ├── play_queue.dart
│           ├── settings/
│           │   ├── about.dart
│           │   ├── dependencies.dart
│           │   ├── general.dart
│           │   ├── play.dart
│           │   └── settings.dart
│           ├── storages/
│           │   ├── favorites.dart
│           │   ├── files.dart
│           │   ├── storages.dart
│           │   └── storages_list.dart
│           └── track/
│               ├── audio_track_list.dart
│               ├── subtitle_and_audio_track.dart
│               └── subtitle_list.dart
├── linux/
│   ├── .gitignore
│   ├── CMakeLists.txt
│   ├── flutter/
│   │   ├── CMakeLists.txt
│   │   ├── generated_plugin_registrant.cc
│   │   ├── generated_plugin_registrant.h
│   │   └── generated_plugins.cmake
│   ├── main.cc
│   ├── my_application.cc
│   └── my_application.h
├── macos/
│   ├── .gitignore
│   ├── Flutter/
│   │   ├── Flutter-Debug.xcconfig
│   │   ├── Flutter-Release.xcconfig
│   │   └── GeneratedPluginRegistrant.swift
│   ├── Runner/
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets/
│   │   │   └── AppIcon.appiconset/
│   │   │       └── Contents.json
│   │   ├── Base.lproj/
│   │   │   └── MainMenu.xib
│   │   ├── Configs/
│   │   │   ├── AppInfo.xcconfig
│   │   │   ├── Debug.xcconfig
│   │   │   ├── Release.xcconfig
│   │   │   └── Warnings.xcconfig
│   │   ├── DebugProfile.entitlements
│   │   ├── Info.plist
│   │   ├── MainFlutterWindow.swift
│   │   └── Release.entitlements
│   ├── Runner.xcodeproj/
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace/
│   │   │   └── xcshareddata/
│   │   │       └── IDEWorkspaceChecks.plist
│   │   └── xcshareddata/
│   │       └── xcschemes/
│   │           └── Runner.xcscheme
│   ├── Runner.xcworkspace/
│   │   ├── contents.xcworkspacedata
│   │   └── xcshareddata/
│   │       └── IDEWorkspaceChecks.plist
│   └── RunnerTests/
│       └── RunnerTests.swift
├── pubspec.yaml
├── test/
│   └── widget_test.dart
└── windows/
    ├── .gitignore
    ├── CMakeLists.txt
    ├── flutter/
    │   ├── CMakeLists.txt
    │   ├── generated_plugin_registrant.cc
    │   ├── generated_plugin_registrant.h
    │   └── generated_plugins.cmake
    ├── inno-languages/
    │   ├── ChineseSimplified.isl
    │   └── English.isl
    ├── iris-updater.bat
    └── runner/
        ├── CMakeLists.txt
        ├── Runner.rc
        ├── flutter_window.cpp
        ├── flutter_window.h
        ├── main.cpp
        ├── resource.h
        ├── runner.exe.manifest
        ├── utils.cpp
        ├── utils.h
        ├── win32_window.cpp
        └── win32_window.h

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/ci.yml
================================================
name: ci
on:
  push:
    branches: 
      - main
      - dev
    paths-ignore:
      - README.md
      - README_CN.md
      - LICENSE
  pull_request:
    paths-ignore:
      - README.md
      - README_CN.md
      - LICENSE

jobs:
  build-windows:
    runs-on: windows-2022
    steps:
      - name: Clone repository
        uses: actions/checkout@v4
      - name: Set up Flutter
        uses: subosito/flutter-action@v2
        with:
          channel: stable
      - name: Generate code
        run: flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs
      - name: Build Flutter application for Windows
        run: flutter build windows
      - name: Create ZIP archive
        run: |
          # Create a directory to hold the files
          mkdir IRIS
          # Copy the build output to the IRIS directory
          Copy-Item -Path "build\windows\x64\runner\Release\*" -Destination "IRIS" -Recurse -Force
          # Create a ZIP file
          Compress-Archive -Path "IRIS" -DestinationPath "IRIS-windows.zip"
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: IRIS-windows.zip
          path: IRIS-windows.zip
      - name: Remove updater
        run: Remove-Item -Path build\windows\x64\runner\Release\iris-updater.bat -Force
      - name: Create installer
        run: iscc inno.iss
      - name: Upload installer
        uses: actions/upload-artifact@v4
        with:
          name: IRIS-windows-installer.exe
          path: build\windows\x64\runner\Release\IRIS-windows-installer.exe
      - name: Clean up
        run: Remove-Item -Path build\windows\x64\runner\Release\ -Recurse -Force
      - name: Build store msix
        run: dart run msix:build --store true
      - name: Remove updater
        run: Remove-Item -Path build\windows\x64\runner\Release\iris-updater.bat -Force
      - name: Pack store msix
        run: dart run msix:pack --store true --output-name IRIS-windows-store
      - name: Upload msix
        uses: actions/upload-artifact@v4
        with:
          name: IRIS-windows-store.msix
          path: build\windows\x64\runner\Release\IRIS-windows-store.msix

  build-android:
    runs-on: ubuntu-latest
    steps:
      - name: Clone repository
        uses: actions/checkout@v4
      - name: Set up Flutter
        uses: subosito/flutter-action@v2
        with:
          channel: stable
      - name: Generate code
        run: flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs
      - name: Set up Java
        uses: actions/setup-java@v4
        with:
          distribution: zulu
          java-version: 21
      - name: Decode and save keystore
        run: |
          echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks
      - name: Save key.properties
        run: |
          echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/key.properties
          echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties
          echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties
          echo "storeFile=keystore.jks" >> android/key.properties
      - name: Build Flutter application for Android
        run: flutter build apk --split-per-abi
      - name: Rename armeabi-v7a APK
        run: mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk IRIS-android-armeabi-v7a.apk
      - name: Rename arm64-v8a APK
        run: mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk IRIS-android-arm64-v8a.apk
      - name: Rename x86_64 APK
        run: mv build/app/outputs/flutter-apk/app-x86_64-release.apk IRIS-android-x86_64.apk
      - name: Upload armeabi-v7a APK
        uses: actions/upload-artifact@v4
        with:
          name: IRIS-android-armeabi-v7a.apk
          path: IRIS-android-armeabi-v7a.apk
      - name: Upload arm64-v8a APK
        uses: actions/upload-artifact@v4
        with:
          name: IRIS-android-arm64-v8a.apk
          path: IRIS-android-arm64-v8a.apk
      - name: Upload x86_64 APK
        uses: actions/upload-artifact@v4
        with:
          name: IRIS-android-x86_64.apk
          path: IRIS-android-x86_64.apk

  release:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    needs:
      - build-windows
      - build-android
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Get version
        id: yq
        uses: mikefarah/yq@master
        with:
          cmd: yq '.version' 'pubspec.yaml'
      - name: Print version
        run: echo ${{ steps.yq.outputs.result }}
      - name: Prepare tag name
        id: tag_name
        run: |
          VERSION="${{ steps.yq.outputs.result }}"
          TAG_NAME="v${VERSION%%+*}"
          echo "TAG_NAME=$TAG_NAME" >> "$GITHUB_OUTPUT"
      - name: Check tag
        uses: mukunku/tag-exists-action@v1.6.0
        id: check-tag
        with:
          tag: ${{ steps.tag_name.outputs.TAG_NAME }}
      - name: Eextract log
        if: steps.check-tag.outputs.exists == 'false'
        run: python extract_log.py ${{ steps.tag_name.outputs.TAG_NAME }}
      - name: Download artifact
        if: steps.check-tag.outputs.exists == 'false'
        uses: actions/download-artifact@v4
        with:
          path: artifacts
          merge-multiple: true
      - name: Release
        if: steps.check-tag.outputs.exists == 'false'
        uses: softprops/action-gh-release@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ steps.tag_name.outputs.TAG_NAME }}
          body_path: CHANGELOG_${{ steps.tag_name.outputs.TAG_NAME }}.md
          draft: false
          prerelease: false
          files: |
            artifacts/IRIS-windows.zip
            artifacts/IRIS-windows-installer.exe
            artifacts/IRIS-android-armeabi-v7a.apk
            artifacts/IRIS-android-arm64-v8a.apk
            artifacts/IRIS-android-x86_64.apk


================================================
FILE: .gitignore
================================================
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/

# Symbolication related
app.*.symbols

# Obfuscation related
app.*.map.json

# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

# freezed & json_serializable generated files
**/*.freezed.dart
**/*.g.dart

# l10n
lib/l10n/app_localizations*.dart


================================================
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: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668"
  channel: "stable"

project_type: app

# Tracks metadata for the flutter migrate command
migration:
  platforms:
    - platform: root
      create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
      base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
    - platform: android
      create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
      base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
    - platform: ios
      create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
      base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
    - platform: linux
      create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
      base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
    - platform: macos
      create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
      base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
    - platform: web
      create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
      base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
    - platform: windows
      create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
      base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668

  # 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: .vscode/launch.json
================================================
{
  // 使用 IntelliSense 了解相关属性。 
  // 悬停以查看现有属性的描述。
  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    
    {
      "name": "iris",
      "request": "launch",
      "type": "dart"
    },
    {
      "name": "iris (profile mode)",
      "request": "launch",
      "type": "dart",
      "flutterMode": "profile"
    },
    {
      "name": "iris (release mode)",
      "request": "launch",
      "type": "dart",
      "flutterMode": "release"
    }
  ]
}

================================================
FILE: .vscode/settings.json
================================================
{
  "arb-editor.suppressedWarnings": [
    "missing_metadata_for_key"
  ],
}

================================================
FILE: CHANGELOG.md
================================================
## v1.5.2

### Changelog

* Migrate to MPL-2.0 license
* Add more media type support
* Fix key open subtitle and audio track issue
* Fix popup layer cannot be closed with `Esc` key issue

### 更新日志

* 迁移到 MPL-2.0 许可证
* 添加更多媒体类型支持
* 修复按键打开字幕和音频轨道的问题
* 修复弹出层无法使用 `Esc` 键关闭的问题

## v1.5.1

### Changelog

* Adjusted the playback control bar style in compact layout
* Clicking “Check update” in the Microsoft Store version redirects to the Store page
* Fixed forward and backward issues

### 更新日志

* 调整了紧凑布局下的播放控制栏样式
* 微软商店版本中点击检查更新将跳转至商店页面
* 修复了快退快进的问题

## v1.5.0

### Changelog

* Updated app icon
* Added a stop and an exit button
* Added a playback-speed selector that activates with a touch-and-hold gesture
* Fixed URI handling
* Improved stability and performance

### 更新日志

* 更换应用图标
* 添加停止按钮和退出按钮
* 添加触控长按激活的播放速度选择器
* 修复 uri 处理
* 优化了稳定性和性能

## v1.4.2

### Changelog

* Fix audio cover issue
* Improve uri handling

### 更新日志

* 修复音频封面问题
* 改进 uri 处理

## v1.4.1

### Changelog

* Dynamic FTP streaming url

### 更新日志

* FTP 串流使用动态 url

## v1.4.0

### Changelog

* Supports FTP storage
* Supports adding local folders to the storage list
* Storage list support remote disk and network shortcuts on Windows
* Add Windows installer

### 更新日志

* 支持 FTP 存储
* 支持添加本地文件夹到存储列表
* Windows 版本存储列表支持远程磁盘和网络快捷方式
* 添加 Windows 版本安装器

## v1.3.4

### Changelog

* The Android version allows you to set the screen orientation.
* Add playback speed button.
* Add hotkeys: Step forward `+`, Step backward `-`.

### 更新日志

* 安卓版本可以设置屏幕方向。
* 添加播放速度按钮。
* 添加快捷键:帧进 `+`,帧退 `-`。

## v1.3.3

### Changelog

* Fix issue of not being able to continue playback after startup

### 更新日志

* 修复启动后无法继续播放的问题

## v1.3.2

### Changelog

* Support for custom https ports when adding WebDAV storage

### 更新日志

* 添加 WebDAV 存储时支持自定义 https 端口

## v1.3.1

### Changelog

* The data save location for the Windows version has been changed to `C:\Users\<user>\AppData\Roaming\nini22P\iris`
* Updated upstream dependencies and fixed the issue with switching subtitles in the FVP player backend

### 更新日志

* Windows 版本数据保存位置已修改为 `C:\Users\<user>\AppData\Roaming\nini22P\iris`
* 更新上游依赖,修复 FVP 播放器后端切换字幕的问题

## v1.3.0

### Changelog

* Add [FVP](https://github.com/wang-bin/fvp) player backend (Experimental, with unknown bugs)
* Adding volume adjust
* Add file sort
* Add hotkeys: Volume up ( `Arrow Up` ), Volume down ( `Arrow Down` ), Volume mute ( `Ctrl + M` ), Toggle always on top ( `F10` ), Close currently media file ( `Ctrl + C` ), Exit application ( `Alt + X` )
* Improved some visual effects

### 更新日志

* 添加 [FVP](https://github.com/wang-bin/fvp) 播放器后端(实验性,有未知bug)
* 添加音量调整
* 添加文件排序
* 添加快捷键:提升音量( `Arrow Up` )、降低音量( `Arrow Down` )、静音(`Ctrl + M`)、切换窗口置顶( `F10` )、关闭当前媒体文件( `Ctrl + C` )、退出应用( `Alt + X` )
* 改进了部分视觉效果

## v1.2.1

### Changelog

* Split APKs by architecture to reduce installation size.

### 更新日志

* 拆分不同架构的 APK 以减小安装包大小

## v1.2.0

### Changelog

* Support jumping to video playback from external clicks (Windows version can play by command line or dragging files to the window)
* Support adjusting brightness and volume gestures (Brightness gestures are not available on Windows version)
* Support playing online links
* Add an option to always start playback from the beginning
* On Android 11 and above, file reading is changed to using the "Manage All Files" permission
* Improved WebDAV connection test function
* Improved some visual effects

### 更新日志

* 支持从外部点击视频跳转播放(Windows 版本可以通过命令行或者拖拽文件到窗口播放)
* 支持调整亮度和音量手势(Windows 版本调整亮度手势不可用)
* 支持播放在线链接
* 添加总是从头开始播放的选项
* Android 11 以上读取文件时改为使用 `管理所有文件` 权限
* 改进 WebDAV 测试连接功能
* 改进了部分视觉效果

## v1.1.1

### Changelog

* Restore old update method for windows version (Double-click the `iris-updater.bat` in the same directory as the executable file to upgrade if you have problems updating.)

### 更新日志

* windows 版本恢复为旧的更新方式(更新出问题的可双击打开可执行文件同级目录下的 `iris-updater.bat` 升级)

## v1.1.0

### Breaking Changes

* All configurations will be cleared. Please reconfigure

### Changlog

* Display all local storage
* Support playback history
* Support random playback
* Support loop playback
* Support video zoom

### 重大变更

* 所有配置将被清空,请重新配置

### 更新日志

* 显示所有本地存储
* 支持播放历史
* 支持随机播放
* 支持循环播放
* 支持视频缩放

## v1.0.3

### Changelog

* Improve Windows version installation updates
* Fixes an issue where subtitles may not be found

### 更新日志

* 改进 Windows 版本安装更新
* 修复可能无法找到字幕的问题

## v1.0.2

### Changelog

* Support for switching built-in audio tracks
* Reduce package size for Windows version

### 更新日志

* 支持切换内置音轨
* 减小 Windows 版本包体大小

## v1.0.1

### Changelog

* Windows version support auto update

### 更新日志

* Windows 版本支持自动更新

## v1.0.0

### Changelog

* Supports WebDAV and local storage video playback

### 更新日志

* 支持 WebDAV 和本地存储视频播放


================================================
FILE: LICENSE
================================================
Mozilla Public License Version 2.0
==================================

1. Definitions

--------------

1.1. "Contributor"
    means each individual or legal entity that creates, contributes to
    the creation of, or owns Covered Software.

1.2. "Contributor Version"
    means the combination of the Contributions of others (if any) used
    by a Contributor and that particular Contributor's Contribution.

1.3. "Contribution"
    means Covered Software of a particular Contributor.

1.4. "Covered Software"
    means Source Code Form to which the initial Contributor has attached
    the notice in Exhibit A, the Executable Form of such Source Code
    Form, and Modifications of such Source Code Form, in each case
    including portions thereof.

1.5. "Incompatible With Secondary Licenses"
    means

    (a) that the initial Contributor has attached the notice described
        in Exhibit B to the Covered Software; or

    (b) that the Covered Software was made available under the terms of
        version 1.1 or earlier of the License, but not also under the
        terms of a Secondary License.

1.6. "Executable Form"
    means any form of the work other than Source Code Form.

1.7. "Larger Work"
    means a work that combines Covered Software with other material, in
    a separate file or files, that is not Covered Software.

1.8. "License"
    means this document.

1.9. "Licensable"
    means having the right to grant, to the maximum extent possible,
    whether at the time of the initial grant or subsequently, any and
    all of the rights conveyed by this License.

1.10. "Modifications"
    means any of the following:

    (a) any file in Source Code Form that results from an addition to,
        deletion from, or modification of the contents of Covered
        Software; or

    (b) any new file in Source Code Form that contains any Covered
        Software.

1.11. "Patent Claims" of a Contributor
    means any patent claim(s), including without limitation, method,
    process, and apparatus claims, in any patent Licensable by such
    Contributor that would be infringed, but for the grant of the
    License, by the making, using, selling, offering for sale, having
    made, import, or transfer of either its Contributions or its
    Contributor Version.

1.12. "Secondary License"
    means either the GNU General Public License, Version 2.0, the GNU
    Lesser General Public License, Version 2.1, the GNU Affero General
    Public License, Version 3.0, or any later versions of those
    licenses.

1.13. "Source Code Form"
    means the form of the work preferred for making modifications.

1.14. "You" (or "Your")
    means an individual or a legal entity exercising rights under this
    License. For legal entities, "You" includes any entity that
    controls, is controlled by, or is under common control with You. For
    purposes of this definition, "control" means (a) the power, direct
    or indirect, to cause the direction or management of such entity,
    whether by contract or otherwise, or (b) ownership of more than
    fifty percent (50%) of the outstanding shares or beneficial
    ownership of such entity.

2. License Grants and Conditions

--------------------------------

2.1. Grants

Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:

(a) under intellectual property rights (other than patent or trademark)
    Licensable by such Contributor to use, reproduce, make available,
    modify, display, perform, distribute, and otherwise exploit its
    Contributions, either on an unmodified basis, with Modifications, or
    as part of a Larger Work; and

(b) under Patent Claims of such Contributor to make, use, sell, offer
    for sale, have made, import, and otherwise transfer either its
    Contributions or its Contributor Version.

2.2. Effective Date

The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.

2.3. Limitations on Grant Scope

The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:

(a) for any code that a Contributor has removed from Covered Software;
    or

(b) for infringements caused by: (i) Your and any other third party's
    modifications of Covered Software, or (ii) the combination of its
    Contributions with other software (except as part of its Contributor
    Version); or

(c) under Patent Claims infringed by Covered Software in the absence of
    its Contributions.

This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).

2.4. Subsequent Licenses

No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).

2.5. Representation

Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.

2.6. Fair Use

This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.

2.7. Conditions

Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.

3. Responsibilities

-------------------

3.1. Distribution of Source Form

All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.

3.2. Distribution of Executable Form

If You distribute Covered Software in Executable Form then:

(a) such Covered Software must also be made available in Source Code
    Form, as described in Section 3.1, and You must inform recipients of
    the Executable Form how they can obtain a copy of such Source Code
    Form by reasonable means in a timely manner, at a charge no more
    than the cost of distribution to the recipient; and

(b) You may distribute such Executable Form under the terms of this
    License, or sublicense it under different terms, provided that the
    license for the Executable Form does not attempt to limit or alter
    the recipients' rights in the Source Code Form under this License.

3.3. Distribution of a Larger Work

You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).

3.4. Notices

You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.

3.5. Application of Additional Terms

You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.

4. Inability to Comply Due to Statute or Regulation

---------------------------------------------------

If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.

5. Termination

--------------

5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.

5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.

5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.

************************************************************************

*                                                                      *
* 6. Disclaimer of Warranty                                           *
* -------------------------                                           *
*                                                                      *
* Covered Software is provided under this License on an "as is"       *
* basis, without warranty of any kind, either expressed, implied, or  *
* statutory, including, without limitation, warranties that the       *
* Covered Software is free of defects, merchantable, fit for a        *
* particular purpose or non-infringing. The entire risk as to the     *
* quality and performance of the Covered Software is with You.        *
* Should any Covered Software prove defective in any respect, You     *
* (not any Contributor) assume the cost of any necessary servicing,   *
* repair, or correction. This disclaimer of warranty constitutes an   *
* essential part of this License. No use of any Covered Software is   *
* authorized under this License except under this disclaimer.         *
*                                                                      *

************************************************************************

************************************************************************

*                                                                      *
* 7. Limitation of Liability                                          *
* --------------------------                                          *
*                                                                      *
* Under no circumstances and under no legal theory, whether tort      *
* (including negligence), contract, or otherwise, shall any           *
* Contributor, or anyone who distributes Covered Software as          *
* permitted above, be liable to You for any direct, indirect,         *
* special, incidental, or consequential damages of any character      *
* including, without limitation, damages for lost profits, loss of    *
* goodwill, work stoppage, computer failure or malfunction, or any    *
* and all other commercial damages or losses, even if such party      *
* shall have been informed of the possibility of such damages. This   *
* limitation of liability shall not apply to liability for death or   *
* personal injury resulting from such party's negligence to the       *
* extent applicable law prohibits such limitation. Some               *
* jurisdictions do not allow the exclusion or limitation of           *
* incidental or consequential damages, so this exclusion and          *
* limitation may not apply to You.                                    *
*                                                                      *

************************************************************************

8. Litigation

-------------

Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.

9. Miscellaneous

----------------

This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.

10. Versions of the License

---------------------------

10.1. New Versions

Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.

10.2. Effect of New Versions

You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.

10.3. Modified Versions

If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).

10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses

If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.

Exhibit A - Source Code Form License Notice
-------------------------------------------

  This Source Code Form is subject to the terms of the Mozilla Public
  License, v. 2.0. If a copy of the MPL was not distributed with this
  file, You can obtain one at <https://mozilla.org/MPL/2.0/>.

If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.

You may add additional accurate notices of copyright ownership.

Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------

  This Source Code Form is "Incompatible With Secondary Licenses", as
  defined by the Mozilla Public License, v. 2.0.


================================================
FILE: PRIVACY.md
================================================
# IRIS Privacy Policy

**Last Updated:** June 7, 2025

Thank you for using IRIS ("the Application"). We are committed to protecting your privacy. This Privacy Policy explains how we handle information in connection with your use of the Application.

Please read this Privacy Policy carefully before using the Application. By using the Application, you agree to the terms of this policy.

### Information We Collect

To provide and improve our services, the Application collects the following types of information:

1. **Information You Provide**

   * **Network Storage Credentials**: When you configure access to your network storage (like WebDAV), you will be asked to provide connection details such as server address, username, and password.
   * **Online Video Links**: When you use the "Open Link" feature, you provide the URL for the video.
2. **Information We Collect Automatically**

   * **Playback History and Preferences**: To enhance your experience, the Application locally stores your playback history (including playback progress) and personal preferences (such as playback speed, volume, and theme mode) on your device.

### How We Use Your Information

We strictly adhere to the principles of lawfulness, legitimacy, and necessity in collecting and using your information. This information is used solely to implement product features, specifically to:

* Access and play media files from your local device or your configured network storage (like WebDAV).
* Stream and play videos from online URLs you provide.
* Remember your playback progress to allow you to resume watching.
* Save your settings and preferences for a personalized experience.
* Check for application updates to provide you with the latest features and bug fixes.

### Information Storage and Security

We place great importance on the security of your information.

* **Storage Location**: All your personal data, including WebDAV credentials, playback history, and application settings, is **stored only locally on your device**. We do not upload this information to any external servers.
* **Security Measures**: We use industry-standard security technologies to protect sensitive information stored on your device from unauthorized access, use, or disclosure.

### Information Sharing and Disclosure

We do not sell, trade, or otherwise transfer your personally identifiable information to outside parties. All data is stored locally on your device.

### Children's Privacy

The Application is not intended for use by children under the age of 13. We do not knowingly collect personally identifiable information from children under 13.

### Changes to This Privacy Policy

We may update our Privacy Policy from time to time. Any changes will be posted on this page, and we encourage you to review this policy periodically for any updates. Your continued use of the Application after we post any modifications will constitute your acknowledgment of the changes and your consent to abide and be bound by the modified Privacy Policy.

### Contact Us

If you have any questions or suggestions about this Privacy Policy, please feel free to contact us by creating an issue on our GitHub page:

* [https://github.com/nini22P/iris/issues](https://github.com/nini22P/iris/issues)


================================================
FILE: README.md
================================================
<img height="100px" width="100px" alt="logo" src="./assets/images/logo.png"/>

# IRIS - A lightweight video player

[![Build Status](https://github.com/nini22P/iris/actions/workflows/ci.yml/badge.svg)](https://github.com/nini22P/iris/actions/workflows/ci.yml)
<a href="https://apps.microsoft.com/detail/9NML7WNHNRTJ?referrer=appbadge&mode=direct"><img src="https://get.microsoft.com/images/en-us%20dark.svg" height="30"/></a>
<a href="https://afdian.com/a/nini22P"><img alt="Afdian" style="height: 30px;" src="https://pic1.afdiancdn.com/static/img/welcome/button-sponsorme.png"></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/nini22p)

English | [中文](./README_CN.md)

## Features

- [X] Base on [Media Kit](https://github.com/media-kit/media-kit) | [FVP](https://github.com/wang-bin/fvp), supports multiple video formats
- [X] Local storage, WebDAV and FTP support
- [X] Switchable subtitle and audio track
- [X] Playback queue support for random and repeat
- [X] Comprehensive gesture support

## Download

| Platform | Arch/Channel      | Download Link                                                                                                      | Notes                  |
| :------- | :---------------- | :----------------------------------------------------------------------------------------------------------------- | :--------------------- |
| Windows  | **Microsoft Store** | [Microsoft Store](https://apps.microsoft.com/detail/9NML7WNHNRTJ)                                                  | **Recommended**, auto-updates |
|          | Installer         | [IRIS-windows-installer.exe](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows-installer.exe)     |                        |
|          | Portable          | [IRIS-windows.zip](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows.zip)                       | Unzip and run          |
| Android  | arm64-v8a         | [IRIS-android-arm64-v8a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-arm64-v8a.apk)     | For 64-bit devices     |
|          | armeabi-v7a       | [IRIS-android-armeabi-v7a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-armeabi-v7a.apk) | For 32-bit devices     |
|          | x86_64            | [IRIS-android-x86_64.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-x86_64.apk)           | For emulators/x86 devices |

## Keyboard and Gesture Controls

### Keyboard Controls

| Key                    | Description                                        |
| ---------------------- | -------------------------------------------------- |
| `Space`              | Play / Pause / Select file                         |
| `Arrow Left`         | Fast backward                    |
| `Arrow Right`        | Fast forward                    |
| `Arrow Up`           | Volume up                                          |
| `Arrow Down`         | Volume down                                        |
| `Ctrl + Arrow Left`  | Previous                                           |
| `Ctrl + Arrow Right` | Next                                               |
| `Ctrl + X`           | Shuffle                                            |
| `Ctrl + R`           | Repeat                                             |
| `Ctrl + V`           | Video zoom                                         |
| `Ctrl + M`           | Volume mute                                        |
| `S`                  | Subtitles and audio tracks                         |
| `P`                  | Play queue                                         |
| `F`                  | Storages                                           |
| `Ctrl + O`           | Open file                                          |
| `Ctrl + L`           | Open link                                          |
| `Ctrl + C`           | Close currently media file                         |
| `Ctrl + H`           | Play history                                       |
| `Ctrl + P`           | Settings                                           |
| `+`                  | Step forward                                       |
| `-`                  | Step backward                                      |
| `Enter`              | Enter full screen / Exit full screen / Select file |
| `F11`                | Enter full screen / Exit full screen               |
| `Esc`                | Exit current Menu / Go back / Exit full screen     |
| `F10`                | Toggle always on top                               |
| `Alt + X`            | Exit application                                   |

### Gesture Controls

| Gesture                           | Description                   |
| --------------------------------- | ----------------------------- |
| Tap                               | Select an item or open a menu |
| Double tap center                 | Play / Pause                  |
| Double tap left side              | Fast backward                |
| Double tap right side             | Fast forward                  |
| Swipe left / right                | Adjust playback progress      |
| Swipe up / down on left side      | Adjust screen brightness      |
| Swipe up / down on right side     | Adjust device volume          |
| Long press                        | Display Playback Speed Selector |
| Long press and swipe left / right | Adjust speed playback speed   |

## Contribution

Contributions of any kind are welcome! If you have suggestions, bug reports, or want to add new features, please submit an [issue](https://github.com/nini22P/iris/issues) or directly submit a Pull Request.

## Sponsorship

If you like this project and want to support me, you can sponsor me through the following platforms:

- [Afdian](https://afdian.com/a/nini22P)
- [Ko-fi](https://ko-fi.com/nini22p)

Thank you for your support!

## License

This project is licensed under the MPL-2.0 license. For more details, please see the [LICENSE](./LICENSE) file.


================================================
FILE: README_CN.md
================================================
<img height="100px" width="100px" alt="logo" src="./assets/images/logo.png"/>

# IRIS - 轻量级视频播放器

[![Build Status](https://github.com/nini22P/iris/actions/workflows/ci.yml/badge.svg)](https://github.com/nini22P/iris/actions/workflows/ci.yml)
<a href="https://apps.microsoft.com/detail/9NML7WNHNRTJ?referrer=appbadge&mode=direct"><img src="https://get.microsoft.com/images/zh-cn%20dark.svg" height="30"/></a>
<a href="https://afdian.com/a/nini22P"><img alt="爱发电" style="height: 30px;" src="https://pic1.afdiancdn.com/static/img/welcome/button-sponsorme.png"></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/nini22p)

[English](./README.md) | 中文

## 特性

- [X] 基于 [Media Kit](https://github.com/media-kit/media-kit) | [FVP](https://github.com/wang-bin/fvp),可播放多种视频格式
- [X] 支持本地存储、WebDAV 和 FTP
- [X] 可切换字幕和音轨
- [X] 播放队列支持随机和重复
- [X] 完善的手势支持

## 下载

| 平台    | 架构/渠道     | 下载链接                                                                                                           | 备注             |
| :------ | :------------ | :----------------------------------------------------------------------------------------------------------------- | :--------------- |
| Windows | **微软商店**  | [Microsoft Store](https://apps.microsoft.com/detail/9NML7WNHNRTJ)                                                  | **推荐**,自动更新 |
|         | 安装包        | [IRIS-windows-installer.exe](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows-installer.exe)     |                  |
|         | 便携版        | [IRIS-windows.zip](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows.zip)                       | 解压即用         |
| Android | arm64-v8a     | [IRIS-android-arm64-v8a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-arm64-v8a.apk)     | 64位设备         |
|         | armeabi-v7a   | [IRIS-android-armeabi-v7a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-armeabi-v7a.apk) | 32位设备         |
|         | x86_64        | [IRIS-android-x86_64.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-x86_64.apk)           | 模拟器/x86设备   |

## 键位和手势

### 键位操作

| 键位                   | 描述                                 |
| ---------------------- | ------------------------------------ |
| `Space`              | 播放 / 暂停 / 选择文件               |
| `Arrow Left`         | 快退                                 |
| `Arrow Right`        | 快进                                 |
| `Arrow Up`           | 提升音量                             |
| `Arrow Down`         | 降低音量                             |
| `Ctrl + Arrow Left`  | 上一个                               |
| `Ctrl + Arrow Right` | 下一个                               |
| `Ctrl + X`           | 随机                                 |
| `Ctrl + R`           | 重复                                 |
| `Ctrl + V`           | 视频缩放                             |
| `Ctrl + M`           | 静音                                 |
| `S`                  | 字幕和音轨                           |
| `P`                  | 播放队列                             |
| `F`                  | 存储                                 |
| `Ctrl + O`           | 打开文件                             |
| `Ctrl + L`           | 打开链接                             |
| `Ctrl + C`           | 关闭当前媒体文件                     |
| `Ctrl + H`           | 播放历史                             |
| `Ctrl + P`           | 设置                                 |
| `+`                  | 帧进                                 |
| `-`                  | 帧退                                 |
| `Enter`              | 进入全屏 / 退出全屏 / 选择文件       |
| `F11`                | 进入全屏 / 退出全屏                  |
| `Esc`                | 退出当前菜单 / 返回上一级 / 关闭全屏 |
| `F10`                | 切换窗口置顶                         |
| `Alt + X`            | 退出应用                             |

### 手势操作

| 手势             | 描述               |
| ---------------- | ------------------ |
| 单击             | 选择项目或打开菜单 |
| 双击屏幕中心     | 播放 / 暂停        |
| 双击屏幕左侧     | 快退                |
| 双击屏幕右侧     | 快进                |
| 左右滑动         | 调整播放进度       |
| 屏幕左侧上下滑动 | 调整屏幕亮度       |
| 屏幕右侧上下滑动 | 调整设备音量       |
| 长按             | 显示播放速度选择器 |
| 长按后左右滑动   | 调整播放速度       |

## 贡献

欢迎任何形式的贡献!如果您有建议、bug 报告或想要添加新功能,请提交 [issue](https://github.com/nini22P/iris/issues) 或者直接提交 Pull Request。

## 赞助

如果您喜欢这个项目并希望支持我,可以通过以下方式赞助我:

- [爱发电](https://afdian.com/a/nini22P)
- [Ko-fi](https://ko-fi.com/nini22p)

感谢您的支持!

## 许可证

本项目采用 MPL-2.0 许可证,详细信息请查看 [LICENSE](./LICENSE) 文件。


================================================
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
analyzer:
  plugins:
    # - custom_lint
  errors:
    invalid_annotation_target: ignore

================================================
FILE: android/.gitignore
================================================
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java

.cxx

# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

/app/src/main/assets/flutter_assets

.kotlin
build

================================================
FILE: android/app/build.gradle
================================================
import java.nio.file.Files
import java.security.MessageDigest

plugins {
    id "com.android.application"
    id "kotlin-android"
    // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
    id "dev.flutter.flutter-gradle-plugin"
}

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

android {
    namespace = "nini22p.iris"
    compileSdk = flutter.compileSdkVersion
    ndkVersion = "27.0.12077973"

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_17
    }

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId = "nini22p.iris"
        // You can update the following values to match your application needs.
        // For more information, see: https://flutter.dev/to/review-gradle-config.
        minSdk = flutter.minSdkVersion
        targetSdk = flutter.targetSdkVersion
        versionCode = flutter.versionCode
        versionName = flutter.versionName
    }

    signingConfigs {
        release {
            keyAlias = keystoreProperties['keyAlias']
            keyPassword = keystoreProperties['keyPassword']
            storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
            storePassword = keystoreProperties['storePassword']
        }
    }

    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig = signingConfigs.release
        }
    }
}

flutter {
    source = "../.."
}


task downloadFiles(type: Exec) {
    def filesToDownload = [
        [
            "url": "https://github.com/notofonts/noto-cjk/raw/refs/heads/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Medium.otf",
            "md5": "58c83279d990b2cf88d40a0a34832e31",
            "destination": file("./src/main/assets/flutter_assets/assets/fonts/NotoSansCJKsc-Medium.otf")
        ]
    ]

    filesToDownload.each { fileInfo ->
        def destFile = fileInfo.destination

        if (destFile.exists()) {
           def calculatedMD5 = MessageDigest.getInstance("MD5").digest(Files.readAllBytes(destFile.toPath())).encodeHex().toString()

            if (calculatedMD5 != fileInfo.md5) {
                destFile.delete()
                println "MD5 mismatch. File deleted: ${destFile}"
            }
        }

        if (!destFile.exists()) {
             destFile.parentFile.mkdirs()
            println "Downloading file from: ${fileInfo.url}"
            destFile.withOutputStream { os ->
                os << new URL(fileInfo.url).openStream()
            }

             def calculatedMD5 = MessageDigest.getInstance("MD5").digest(Files.readAllBytes(destFile.toPath())).encodeHex().toString()
            if (calculatedMD5 != fileInfo.md5) {
                throw new GradleException("MD5 verification failed for ${destFile}")
            }
        }
    }
}

assemble.dependsOn(downloadFiles)

================================================
FILE: android/app/proguard-rules.pro
================================================
-dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue
-dontwarn com.google.errorprone.annotations.CheckReturnValue
-dontwarn com.google.errorprone.annotations.Immutable
-dontwarn com.google.errorprone.annotations.RestrictedApi
-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.concurrent.GuardedBy

================================================
FILE: android/app/src/debug/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- The INTERNET permission is required for development. Specifically,
         the Flutter tool needs it to communicate with the running application
         to allow setting breakpoints, to provide hot reload, etc.
    -->
    <uses-permission android:name="android.permission.INTERNET"/>
</manifest>


================================================
FILE: android/app/src/main/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
    <application
        android:label="IRIS"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher"
        android:allowBackup="false"
        android:fullBackupContent="false"
        android:enableOnBackInvokedCallback="true"
    >
        <activity android:name=".MainActivity" android:exported="true" android:launchMode="singleInstance" android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme"/>
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:mimeType="video/*" />
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data android:name="flutterEmbedding" android:value="2"/>
    </application>
    <!-- Required to query activities that can process text, see:
         https://developer.android.com/training/package-visibility and
         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
    <queries>
        <intent>
            <action android:name="android.intent.action.PROCESS_TEXT"/>
            <data android:mimeType="text/plain"/>
        </intent>
    </queries>
</manifest>


================================================
FILE: android/app/src/main/kotlin/nini22p/iris/MainActivity.kt
================================================
package nini22p.iris

import io.flutter.embedding.android.FlutterActivity

class MainActivity: FlutterActivity()


================================================
FILE: android/app/src/main/res/drawable/launch_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@android:color/white" />

    <!-- You can insert your own image assets here -->
    <!-- <item>
        <bitmap
            android:gravity="center"
            android:src="@mipmap/launch_image" />
    </item> -->
</layer-list>


================================================
FILE: android/app/src/main/res/drawable-v21/launch_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="?android:colorBackground" />

    <!-- You can insert your own image assets here -->
    <!-- <item>
        <bitmap
            android:gravity="center"
            android:src="@mipmap/launch_image" />
    </item> -->
</layer-list>


================================================
FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@color/ic_launcher_background"/>
    <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

================================================
FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@color/ic_launcher_background"/>
    <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

================================================
FILE: android/app/src/main/res/values/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="ic_launcher_background">#2F2F2F</color>
</resources>

================================================
FILE: android/app/src/main/res/values/styles.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
    <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
        <!-- Show a splash screen on the activity. Automatically removed when
             the Flutter engine draws its first frame -->
        <item name="android:windowBackground">@drawable/launch_background</item>
    </style>
    <!-- Theme applied to the Android Window as soon as the process has started.
         This theme determines the color of the Android Window while your
         Flutter UI initializes, as well as behind your Flutter UI while its
         running.

         This Theme is only used starting with V2 of Flutter's Android embedding. -->
    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
        <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
        <item name="android:windowBackground">?android:colorBackground</item>
    </style>
</resources>


================================================
FILE: android/app/src/main/res/values-night/styles.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
        <!-- Show a splash screen on the activity. Automatically removed when
             the Flutter engine draws its first frame -->
        <item name="android:windowBackground">@drawable/launch_background</item>
    </style>
    <!-- Theme applied to the Android Window as soon as the process has started.
         This theme determines the color of the Android Window while your
         Flutter UI initializes, as well as behind your Flutter UI while its
         running.

         This Theme is only used starting with V2 of Flutter's Android embedding. -->
    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
        <item name="android:windowBackground">?android:colorBackground</item>
    </style>
</resources>


================================================
FILE: android/app/src/profile/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- The INTERNET permission is required for development. Specifically,
         the Flutter tool needs it to communicate with the running application
         to allow setting breakpoints, to provide hot reload, etc.
    -->
    <uses-permission android:name="android.permission.INTERNET"/>
</manifest>


================================================
FILE: android/build.gradle
================================================
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
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip


================================================
FILE: android/gradle.properties
================================================
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
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
    }()

    includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")

    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

plugins {
    id "dev.flutter.flutter-plugin-loader" version "1.0.0"
    id "com.android.application" version "8.12.0" apply false
    id "org.jetbrains.kotlin.android" version "2.2.20" apply false
}

include ":app"


================================================
FILE: devtools_options.yaml
================================================
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:


================================================
FILE: extract_log.py
================================================
import sys

def extract_log(version):
    try:
        with open("CHANGELOG.md", "r", encoding="utf-8") as file:
            lines = file.readlines()
    except FileNotFoundError:
        print("Error: not found CHANGELOG.md")
        return

    found = False
    changelog_lines = []

    for line in lines:
        if line.startswith(f"## {version}"):
            found = True
            continue
        elif line.startswith("## ") and found:
            break
        if found:
            changelog_lines.append(line)

    while changelog_lines and not changelog_lines[-1].strip():
        changelog_lines.pop()

    output = "".join(changelog_lines).strip()

    output_file = f"CHANGELOG_{version}.md"
    with open(output_file, "w", encoding="utf-8") as file:
        file.write(output)

    print(f"Changelog for {version} saved to {output_file}")

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python extract_log.py <version>")
        sys.exit(1)

    version = sys.argv[1]
    extract_log(version)

================================================
FILE: inno.iss
================================================
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!

#define MyAppName "IRIS"
#define MyAppVersion "1.5.2"
#define MyAppPublisher "nini22P"
#define MyAppURL "https://github.com/nini22P/iris"
#define MyAppExeName "iris.exe"
#define MyAppAssocName MyAppPublisher + "." + MyAppName + ".AssocFile"
#define MyAppDesc "IRIS player"
#define MySetupMutex "iris_player"
#define MyProcessName "iris"

[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{E6C83B69-C1A7-4C23-9D74-799BC7A053DB}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName}
; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run
; on anything but x64 and Windows 11 on Arm.
ArchitecturesAllowed=x64compatible
; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the
; install be done in "64-bit mode" on x64 or Windows 11 on Arm,
; meaning it should use the native 64-bit Program Files directory and
; the 64-bit view of the registry.
ArchitecturesInstallIn64BitMode=x64compatible
DisableProgramGroupPage=yes
; Uncomment the following line to run in non administrative install mode (install for current user only).
;PrivilegesRequired=lowest
OutputDir=build\windows\x64\runner\Release
OutputBaseFilename=IRIS-windows-installer
SolidCompression=yes
WizardStyle=modern
CloseApplications=force
SetupMutex={#MySetupMutex}

[Languages]
Name: "english"; MessagesFile: "windows\inno-languages\English.isl"; LicenseFile: "LICENSE"
Name: "chinesesimplified"; MessagesFile: "windows\inno-languages\ChineseSimplified.isl"; LicenseFile: "LICENSE"

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked

[Files]
Source: "build\windows\x64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "build\windows\x64\runner\Release\*.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files

[Registry]
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}"; ValueType: string; ValueName: "FriendlyAppName"; ValueData: "IRIS"

; --- 定义核心的 Programmatic Identifier (ProgID) ---
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocName}"; ValueType: string; ValueName: ""; ValueData: "IRIS Media File"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocName}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocName}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""

; --- ProgID 添加到各文件类型的 "Open With" 列表 ---
; Audio Files
Root: HKA; Subkey: "Software\Classes\.aac\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.aiff\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.alac\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.amr\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.ape\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.caf\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.cda\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.dsd\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.dts\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.flac\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.m4a\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.midi\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.mp3\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.mpc\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.oga\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.ogg\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.opus\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.raw\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.spx\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.tak\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.tta\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.wav\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.wma\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.wv\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
; Video Files
Root: HKA; Subkey: "Software\Classes\.3gp\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.amv\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.asf\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.avi\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.divx\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.dpx\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.drc\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.dv\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.f4v\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.flv\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.h264\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.h265\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.hevc\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.m2ts\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.m4p\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.m4v\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.mkv\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.mng\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.mov\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.mp2\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.mp4\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.mpe\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.mpeg\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.mpg\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.mpv\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.mts\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.mxf\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.nsv\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.ogv\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.qt\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.rm\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.rmvb\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.ts\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.vob\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.webm\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.wmv\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\.yuv\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocName}"; ValueData: ""; Flags: uninsdeletevalue

; --- 注册应用程序的 Capabilities ---
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities"; ValueType: string; ValueName: "ApplicationName"; ValueData: "{#MyAppName}"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities"; ValueType: string; ValueName: "ApplicationDescription"; ValueData: "{#MyAppDesc}"
; Audio Files
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".aac"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".aiff"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".alac"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".cda"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".dsd"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".flac"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".m4a"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".midi"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".mp3"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".ogg"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".opus"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".raw"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".wav"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".wma"; ValueData: "{#MyAppAssocName}"
; Video Files
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".3gp"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".avi"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".dpx"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".dv"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".f4v"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".flv"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".he264"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".hevc"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".h265"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".mkv"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".mp4"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".mpeg"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".mpg"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".mov"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".nsv"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".rm"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".rmvb"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".ts"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".vob"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".webm"; ValueData: "{#MyAppAssocName}"
Root: HKA; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities\FileAssociations"; ValueType: string; ValueName: ".wmv"; ValueData: "{#MyAppAssocName}"

; --- 在 HKLM 中注册应用以支持 Capabilities ---
Root: HKLM; Subkey: "Software\RegisteredApplications"; ValueType: string; ValueName: "{#MyAppName}"; ValueData: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities"; Flags: uninsdeletevalue
Root: HKLM; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities"; ValueType: string; ValueName: "ApplicationName"; ValueData: "{#MyAppName}"; Flags: uninsdeletekey
Root: HKLM; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}\Capabilities"; ValueType: string; ValueName: "ApplicationDescription"; ValueData: "{#MyAppDesc}"

; --- 注册应用程序路径 ---
Root: HKLM; Subkey: "Software\Microsoft\Windows\CurrentVersion\App Paths\{#MyAppExeName}"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName}"; Flags: uninsdeletekey
Root: HKLM; Subkey: "Software\Microsoft\Windows\CurrentVersion\App Paths\{#MyAppExeName}"; ValueType: string; ValueName: "Path"; ValueData: "{app}"

[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon

[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent

[Code]
function ReadFileContent(const FileName: String): String;
var
  StringList: TStringList;
begin
  Result := '';
  StringList := TStringList.Create;
  try
    StringList.LoadFromFile(FileName);
    Result := StringList.Text;
  finally
    StringList.Free;
  end;
end;

function GetProcessPIDs(const ProcessName: string): String;
var
  ResultCode: Integer;
  Cmd: String;
  OutputFile: String;
begin
  OutputFile := ExpandConstant('{tmp}\..\pid_output.txt');
  Cmd := '-Command "Get-Process -Name ''' + ProcessName + ''' | ForEach-Object { if ($_.Path -and  (Test-Path (Join-Path (Split-Path $_.Path) ''libass.dll''))) { $_.Id } } > ''' + OutputFile + '''"';
  if Exec('powershell.exe', Cmd, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
  begin
    Result := ReadFileContent(OutputFile);
  end
  else
  begin
    Result := '';
  end;
  if FileExists(OutputFile) then
      DeleteFile(OutputFile);
end;

function InitializeUninstall(): Boolean;
var
  ErrorCode: Integer;
  UserResponse: Integer;
  PIDs: String;
  PIDList: TStringList;
  i: Integer;
begin
  PIDs := GetProcessPIDs('{#MyProcessName}');

  if PIDs <> '' then
  begin
    UserResponse := MsgBox(FmtMessage(CustomMessage('CloseRunningAppToContinueUninstall'), ['{#MyAppName}']), mbConfirmation, MB_YESNO);

    if UserResponse = IDNO then
    begin
      Result := False;
      Exit;
    end
    else
    begin
      PIDList := TStringList.Create;
      try
        PIDList.CommaText := PIDs;
        for i := 0 to PIDList.Count - 1 do
        begin
          ShellExec('open', 'taskkill.exe', '/f /pid ' + PIDList[i], '', SW_HIDE, ewNoWait, ErrorCode);
        end;
      finally
        PIDList.Free;
      end;
    end;
  end;

  Result := True;
end;


================================================
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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleDevelopmentRegion</key>
  <string>en</string>
  <key>CFBundleExecutable</key>
  <string>App</string>
  <key>CFBundleIdentifier</key>
  <string>io.flutter.flutter.app</string>
  <key>CFBundleInfoDictionaryVersion</key>
  <string>6.0</string>
  <key>CFBundleName</key>
  <string>App</string>
  <key>CFBundlePackageType</key>
  <string>FMWK</string>
  <key>CFBundleShortVersionString</key>
  <string>1.0</string>
  <key>CFBundleSignature</key>
  <string>????</string>
  <key>CFBundleVersion</key>
  <string>1.0</string>
  <key>MinimumOSVersion</key>
  <string>12.0</string>
</dict>
</plist>


================================================
FILE: ios/Flutter/Debug.xcconfig
================================================
#include "Generated.xcconfig"


================================================
FILE: ios/Flutter/Release.xcconfig
================================================
#include "Generated.xcconfig"


================================================
FILE: ios/Runner/AppDelegate.swift
================================================
import Flutter
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}


================================================
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
================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
    </dependencies>
    <scenes>
        <!--View Controller-->
        <scene sceneID="EHf-IW-A2E">
            <objects>
                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
                    <layoutGuides>
                        <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
                        <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
                    </layoutGuides>
                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <subviews>
                            <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
                            </imageView>
                        </subviews>
                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                        <constraints>
                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
                        </constraints>
                    </view>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="53" y="375"/>
        </scene>
    </scenes>
    <resources>
        <image name="LaunchImage" width="168" height="185"/>
    </resources>
</document>


================================================
FILE: ios/Runner/Base.lproj/Main.storyboard
================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
    </dependencies>
    <scenes>
        <!--Flutter View Controller-->
        <scene sceneID="tne-QT-ifu">
            <objects>
                <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
                    <layoutGuides>
                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
                    </layoutGuides>
                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
                    </view>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
            </objects>
        </scene>
    </scenes>
</document>


================================================
FILE: ios/Runner/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>$(DEVELOPMENT_LANGUAGE)</string>
    <key>CFBundleDisplayName</key>
    <string>IRIS</string>
    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>iris</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>$(FLUTTER_BUILD_NAME)</string>
    <key>CFBundleSignature</key>
    <string>????</string>
    <key>CFBundleVersion</key>
    <string>$(FLUTTER_BUILD_NUMBER)</string>
    <key>LSRequiresIPhoneOS</key>
    <true/>
    <key>UILaunchStoryboardName</key>
    <string>LaunchScreen</string>
    <key>UIMainStoryboardFile</key>
    <string>Main</string>
    <key>UISupportedInterfaceOrientations</key>
    <array>
      <string>UIInterfaceOrientationPortrait</string>
      <string>UIInterfaceOrientationLandscapeLeft</string>
      <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    <key>UISupportedInterfaceOrientations~ipad</key>
    <array>
      <string>UIInterfaceOrientationPortrait</string>
      <string>UIInterfaceOrientationPortraitUpsideDown</string>
      <string>UIInterfaceOrientationLandscapeLeft</string>
      <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    <key>CADisableMinimumFrameDurationOnPhone</key>
    <true/>
    <key>UIApplicationSupportsIndirectInputEvents</key>
    <true/>
  </dict>
</plist>

================================================
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 */; };
		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 = "<group>"; };
		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
		331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
		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 = "<group>"; };
		74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
		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 = "<group>"; };
		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* 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 = "<group>";
		};
		9740EEB11CF90186004384FC /* Flutter */ = {
			isa = PBXGroup;
			children = (
				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
				9740EEB21CF90195004384FC /* Debug.xcconfig */,
				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
				9740EEB31CF90195004384FC /* Generated.xcconfig */,
			);
			name = Flutter;
			sourceTree = "<group>";
		};
		97C146E51CF9000F007C117D = {
			isa = PBXGroup;
			children = (
				9740EEB11CF90186004384FC /* Flutter */,
				97C146F01CF9000F007C117D /* Runner */,
				97C146EF1CF9000F007C117D /* Products */,
				331C8082294A63A400263BE5 /* RunnerTests */,
			);
			sourceTree = "<group>";
		};
		97C146EF1CF9000F007C117D /* Products */ = {
			isa = PBXGroup;
			children = (
				97C146EE1CF9000F007C117D /* Runner.app */,
				331C8081294A63A400263BE5 /* RunnerTests.xctest */,
			);
			name = Products;
			sourceTree = "<group>";
		};
		97C146F01CF9000F007C117D /* Runner */ = {
			isa = PBXGroup;
			children = (
				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 */,
			);
			path = Runner;
			sourceTree = "<group>";
		};
/* 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 */,
			);
			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 = "<group>";
		};
		97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
			isa = PBXVariantGroup;
			children = (
				97C147001CF9000F007C117D /* Base */,
			);
			name = LaunchScreen.storyboard;
			sourceTree = "<group>";
		};
/* 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 = nini22p.iris;
				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 = nini22p.iris.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 = nini22p.iris.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 = nini22p.iris.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 = nini22p.iris;
				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 = nini22p.iris;
				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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "self:">
   </FileRef>
</Workspace>


================================================
FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>IDEDidComputeMac32BitWarning</key>
	<true/>
</dict>
</plist>


================================================
FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>PreviewsEnabled</key>
	<false/>
</dict>
</plist>


================================================
FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
   LastUpgradeVersion = "1510"
   version = "1.3">
   <BuildAction
      parallelizeBuildables = "YES"
      buildImplicitDependencies = "YES">
      <BuildActionEntries>
         <BuildActionEntry
            buildForTesting = "YES"
            buildForRunning = "YES"
            buildForProfiling = "YES"
            buildForArchiving = "YES"
            buildForAnalyzing = "YES">
            <BuildableReference
               BuildableIdentifier = "primary"
               BlueprintIdentifier = "97C146ED1CF9000F007C117D"
               BuildableName = "Runner.app"
               BlueprintName = "Runner"
               ReferencedContainer = "container:Runner.xcodeproj">
            </BuildableReference>
         </BuildActionEntry>
      </BuildActionEntries>
   </BuildAction>
   <TestAction
      buildConfiguration = "Debug"
      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
      shouldUseLaunchSchemeArgsEnv = "YES">
      <MacroExpansion>
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
            BuildableName = "Runner.app"
            BlueprintName = "Runner"
            ReferencedContainer = "container:Runner.xcodeproj">
         </BuildableReference>
      </MacroExpansion>
      <Testables>
         <TestableReference
            skipped = "NO"
            parallelizable = "YES">
            <BuildableReference
               BuildableIdentifier = "primary"
               BlueprintIdentifier = "331C8080294A63A400263BE5"
               BuildableName = "RunnerTests.xctest"
               BlueprintName = "RunnerTests"
               ReferencedContainer = "container:Runner.xcodeproj">
            </BuildableReference>
         </TestableReference>
      </Testables>
   </TestAction>
   <LaunchAction
      buildConfiguration = "Debug"
      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
      launchStyle = "0"
      useCustomWorkingDirectory = "NO"
      ignoresPersistentStateOnLaunch = "NO"
      debugDocumentVersioning = "YES"
      debugServiceExtension = "internal"
      allowLocationSimulation = "YES">
      <BuildableProductRunnable
         runnableDebuggingMode = "0">
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
            BuildableName = "Runner.app"
            BlueprintName = "Runner"
            ReferencedContainer = "container:Runner.xcodeproj">
         </BuildableReference>
      </BuildableProductRunnable>
   </LaunchAction>
   <ProfileAction
      buildConfiguration = "Profile"
      shouldUseLaunchSchemeArgsEnv = "YES"
      savedToolIdentifier = ""
      useCustomWorkingDirectory = "NO"
      debugDocumentVersioning = "YES">
      <BuildableProductRunnable
         runnableDebuggingMode = "0">
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
            BuildableName = "Runner.app"
            BlueprintName = "Runner"
            ReferencedContainer = "container:Runner.xcodeproj">
         </BuildableReference>
      </BuildableProductRunnable>
   </ProfileAction>
   <AnalyzeAction
      buildConfiguration = "Debug">
   </AnalyzeAction>
   <ArchiveAction
      buildConfiguration = "Release"
      revealArchiveInOrganizer = "YES">
   </ArchiveAction>
</Scheme>


================================================
FILE: ios/Runner.xcworkspace/contents.xcworkspacedata
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "group:Runner.xcodeproj">
   </FileRef>
</Workspace>


================================================
FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>IDEDidComputeMac32BitWarning</key>
	<true/>
</dict>
</plist>


================================================
FILE: ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>PreviewsEnabled</key>
	<false/>
</dict>
</plist>


================================================
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: l10n.yaml
================================================
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
synthetic-package: false

================================================
FILE: lib/globals.dart
================================================
// ignore: unnecessary_library_name
library my_app.globals;

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';

List<String> arguments = [];
String? initUri;
PermissionStatus? storagePermissionStatus;
final moreMenuKey = GlobalKey<PopupMenuButtonState>();
final rateMenuKey = GlobalKey<PopupMenuButtonState>();
const double speedSelectorItemWidth = 64.0;
const List<double> speedStops = [
  0.25,
  0.5,
  0.75,
  1.0,
  1.25,
  1.5,
  1.75,
  2.0,
  3.0,
  4.0,
  5.0,
  6.0,
  7.0,
  8.0,
  9.0,
  10.0,
];


================================================
FILE: lib/hooks/player/use_fvp_player.dart
================================================
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_zustand/flutter_zustand.dart';
import 'package:fvp/fvp.dart';
import 'package:iris/globals.dart' as globals;
import 'package:iris/models/file.dart';
import 'package:iris/models/player.dart';
import 'package:iris/models/progress.dart';
import 'package:iris/models/storages/storage.dart';
import 'package:iris/models/store/app_state.dart';
import 'package:iris/store/use_app_store.dart';
import 'package:iris/store/use_history_store.dart';
import 'package:iris/store/use_play_queue_store.dart';
import 'package:iris/store/use_player_ui_store.dart';
import 'package:iris/store/use_storage_store.dart';
import 'package:iris/utils/check_data_source_type.dart';
import 'package:iris/utils/logger.dart';
import 'package:iris/utils/platform.dart';
import 'package:media_stream/media_stream.dart';
import 'package:saf_util/saf_util.dart';
import 'package:video_player/video_player.dart';
import 'package:wakelock_plus/wakelock_plus.dart';

FvpPlayer useFvpPlayer(BuildContext context) {
  final autoPlay = useAppStore().select(context, (state) => state.autoPlay);
  final rate = useAppStore().select(context, (state) => state.rate);
  final volume = useAppStore().select(context, (state) => state.volume);
  final isMuted = useAppStore().select(context, (state) => state.isMuted);
  final repeat = useAppStore().select(context, (state) => state.repeat);
  final playQueue =
      usePlayQueueStore().select(context, (state) => state.playQueue);
  final currentIndex =
      usePlayQueueStore().select(context, (state) => state.currentIndex);
  final bool alwaysPlayFromBeginning =
      useAppStore().select(context, (state) => state.alwaysPlayFromBeginning);

  final history = useHistoryStore().select(context, (state) => state.history);

  final looping =
      useMemoized(() => repeat == Repeat.one ? true : false, [repeat]);

  final int currentPlayIndex = useMemoized(
      () => playQueue.indexWhere((element) => element.index == currentIndex),
      [playQueue, currentIndex]);

  final FileItem? file = useMemoized(
      () => playQueue.isEmpty || currentPlayIndex < 0
          ? null
          : playQueue[currentPlayIndex].file,
      [playQueue, currentPlayIndex]);

  final externalSubtitle = useState<int?>(null);

  final List<Subtitle> externalSubtitles =
      useMemoized(() => file?.subtitles ?? [], [file?.subtitles]);

  final isInitializing = useState(false);

  MediaStream mediaStream = useMemoized(() => MediaStream());
  final streamUrl = useMemoized(() => mediaStream.url);

  final controller = useState(VideoPlayerController.networkUrl(Uri.parse('')));

  final isPlaying = useListenableSelector(
      controller.value, () => controller.value.value.isPlaying);
  final position = useListenableSelector(
      controller.value, () => controller.value.value.position);
  final duration = useListenableSelector(
      controller.value, () => controller.value.value.duration);
  final buffered = useListenableSelector(
      controller.value, () => controller.value.value.buffered);
  final width = useListenableSelector(
      controller.value, () => controller.value.value.size.width);
  final height = useListenableSelector(
      controller.value, () => controller.value.value.size.height);

  useEffect(() {
    if (file?.type != ContentType.video) {
      usePlayerUiStore().updateAspectRatio(0);
      return;
    }

    if (width != 0 && height != 0) {
      usePlayerUiStore().updateAspectRatio(width / height);
    } else {
      usePlayerUiStore().updateAspectRatio(0);
    }
    return;
  }, [file?.type, width, height]);

  Future<void> init(FileItem? file) async {
    isInitializing.value = true;

    try {
      if (controller.value.value.isInitialized) {
        logger('Dispose player');
        controller.value.dispose();
      }

      if (file == null || file.uri.isEmpty) {
        controller.value = VideoPlayerController.networkUrl(Uri.parse(''));
      } else {
        final storage = useStorageStore().findById(file.storageId);
        final auth = storage?.getAuth();

        logger('Open file: $file');

        switch (checkDataSourceType(file)) {
          case DataSourceType.file:
            controller.value = VideoPlayerController.file(
              File(file.uri),
              httpHeaders: auth != null ? {'authorization': auth} : {},
            );
          case DataSourceType.contentUri:
            final isExists = await SafUtil().exists(file.uri, false);
            controller.value = VideoPlayerController.contentUri(
              isExists ? Uri.parse(file.uri) : Uri.parse(''),
            );
          default:
            controller.value = VideoPlayerController.networkUrl(
              Uri.parse(file.storageType == StorageType.ftp
                  ? '$streamUrl/${file.uri}'
                  : file.uri),
              httpHeaders: auth != null ? {'authorization': auth} : {},
            );
        }
      }
      await controller.value.initialize();
      await controller.value.setLooping(repeat == Repeat.one ? true : false);
      await controller.value.setPlaybackSpeed(rate);
      await controller.value.setVolume(isMuted ? 0 : volume / 100);
    } catch (e) {
      logger('Error initializing player: $e');
    } finally {
      isInitializing.value = false;
    }
  }

  useEffect(() {
    init(file);
    return;
  }, [file?.uri]);

  useEffect(() {
    return () {
      if (controller.value.value.isInitialized) {
        controller.value.dispose();
      }
    };
  }, []);

  useEffect(() {
    () async {
      final currentExternalSubtitle = externalSubtitle.value;
      if (currentExternalSubtitle == null || externalSubtitles.isEmpty) {
        controller.value.setExternalSubtitle('');
      } else if (externalSubtitle.value! < externalSubtitles.length) {
        bool isExists = true;

        final uri = file?.storageType == StorageType.ftp
            ? '$streamUrl/${externalSubtitles[currentExternalSubtitle].uri}'
            : externalSubtitles[currentExternalSubtitle].uri;

        logger('External subtitle uri: $uri');

        if (Platform.isAndroid &&
            externalSubtitles[currentExternalSubtitle]
                .uri
                .startsWith('content://')) {
          isExists = await SafUtil().exists(uri, false);
        }

        if (isExists) {
          controller.value.setExternalSubtitle(uri);
        } else {
          externalSubtitle.value = null;
        }
      }
    }();

    return;
  }, [externalSubtitles, externalSubtitle.value]);

  useEffect(() {
    () async {
      if (file != null &&
          controller.value.value.isCompleted &&
          controller.value.value.position != Duration.zero &&
          controller.value.value.duration != Duration.zero) {
        logger('Completed: ${file.name}');
        if (repeat == Repeat.one) return;
        if (currentPlayIndex == playQueue.length - 1) {
          if (repeat == Repeat.all) {
            await usePlayQueueStore().updateCurrentIndex(playQueue[0].index);
          }
        } else {
          await usePlayQueueStore()
              .updateCurrentIndex(playQueue[currentPlayIndex + 1].index);
        }
      }
    }();
    return;
  }, [controller.value.value.isCompleted]);

  useEffect(() {
    if (controller.value.value.isInitialized) {
      controller.value.setPlaybackSpeed(rate);
    }
    return;
  }, [rate]);

  useEffect(() {
    if (controller.value.value.isInitialized) {
      controller.value.setVolume(isMuted ? 0 : volume / 100);
    }
    return;
  }, [volume, isMuted]);

  useEffect(() {
    if (controller.value.value.isInitialized) {
      logger('Set looping: $looping');
      controller.value.setLooping(repeat == Repeat.one ? true : false);
    }
    return;
  }, [looping]);

  useEffect(() {
    () async {
      if (controller.value.value.duration != Duration.zero &&
          file != null &&
          file.type == ContentType.video) {
        Progress? progress = history[file.getID()];
        if (progress != null) {
          if (!alwaysPlayFromBeginning &&
              (progress.duration.inMilliseconds -
                      progress.position.inMilliseconds) >
                  5000) {
            logger(
                'Resume progress: ${file.name} position: ${progress.position} duration: ${progress.duration}');
            await controller.value.seekTo(progress.position);
          }
        }
      }

      if (autoPlay) {
        controller.value.play();
      }

      if (externalSubtitles.isNotEmpty) {
        externalSubtitle.value = 0;
      }
    }();
    return;
  }, [controller.value.value.duration]);

  useEffect(() {
    return () {
      if (isAndroid &&
          globals.initUri == file?.uri &&
          globals.initUri != null &&
          globals.initUri!.startsWith('content://')) {
        return;
      }

      if (file != null &&
          controller.value.value.isInitialized &&
          controller.value.value.duration.inSeconds != 0) {
        logger(
            'Save progress: ${file.name}, position: ${controller.value.value.position}, duration: ${controller.value.value.duration}');
        useHistoryStore().add(Progress(
          dateTime: DateTime.now().toUtc(),
          position: controller.value.value.position,
          duration: controller.value.value.duration,
          file: file,
        ));
      }
    };
  }, [file]);

  useEffect(() {
    if (controller.value.value.isPlaying) {
      logger('Enable wakelock');
      WakelockPlus.enable();
    } else {
      logger('Disable wakelock');
      WakelockPlus.disable();
    }
    return;
  }, [controller.value.value.isPlaying]);

  Future<void> play() async {
    if (!controller.value.value.isInitialized &&
        !isInitializing.value &&
        file != null) {
      init(file);
    }
    controller.value.play();
  }

  Future<void> pause() async {
    controller.value.pause();
  }

  Future<void> seek(Duration newPosition) async {
    logger('Seek to: $newPosition');
    if (controller.value.value.duration == Duration.zero) return;
    newPosition.inSeconds < 0
        ? await controller.value.seekTo(Duration.zero)
        : newPosition.inSeconds > controller.value.value.duration.inSeconds
            ? await controller.value.seekTo(controller.value.value.duration)
            : await controller.value.seekTo(newPosition);
  }

  Future<void> backward(int seconds) async => await seek(
      Duration(seconds: controller.value.value.position.inSeconds - seconds));

  Future<void> forward(int seconds) async => await seek(
      Duration(seconds: controller.value.value.position.inSeconds + seconds));

  Future<void> stepBackward() async {
    if (file?.type == ContentType.video) {
      await controller.value.step(frames: -1);
      logger('Step backward');
    }
  }

  Future<void> stepForward() async {
    if (file?.type == ContentType.video) {
      await controller.value.step(frames: 1);
      logger('Step forward');
    }
  }

  Future<void> saveProgress() async {
    if (isAndroid &&
        globals.initUri == file?.uri &&
        globals.initUri != null &&
        globals.initUri!.startsWith('content://')) {
      return;
    }

    if (file != null && controller.value.value.duration != Duration.zero) {
      logger(
          'Save progress: ${file.name}, position: ${controller.value.value.position}, duration: ${controller.value.value.duration}');
      useHistoryStore().add(Progress(
        dateTime: DateTime.now().toUtc(),
        position: controller.value.value.position,
        duration: controller.value.value.duration,
        file: file,
      ));
    }
  }

  useEffect(() => saveProgress, []);

  final fvpPlayer = useMemoized(
    () => FvpPlayer(
      controller: controller.value,
      isInitializing: isInitializing.value,
      isPlaying: isPlaying,
      externalSubtitle: externalSubtitle,
      externalSubtitles: externalSubtitles,
      position:
          !controller.value.value.isInitialized || duration == Duration.zero
              ? Duration.zero
              : position,
      duration: controller.value.value.isInitialized ? duration : Duration.zero,
      buffer: buffered.isEmpty || duration == Duration.zero
          ? Duration.zero
          : buffered.reduce((max, curr) => curr.end > max.end ? curr : max).end,
      width: width,
      height: height,
      play: play,
      pause: pause,
      backward: backward,
      forward: forward,
      stepBackward: stepBackward,
      stepForward: stepForward,
      seek: seek,
      saveProgress: saveProgress,
    ),
    [
      controller.value,
      controller.value.value.isInitialized,
      isInitializing.value,
      isPlaying,
      externalSubtitle.value,
      externalSubtitles,
      position,
      duration,
      buffered,
      width,
      height,
      play,
      pause,
      seek,
      backward,
      forward,
      stepBackward,
      stepForward,
      saveProgress,
    ],
  );

  return fvpPlayer;
}


================================================
FILE: lib/hooks/player/use_media_kit_player.dart
================================================
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_zustand/flutter_zustand.dart';
import 'package:iris/globals.dart' as globals;
import 'package:iris/models/file.dart';
import 'package:iris/models/player.dart';
import 'package:iris/models/progress.dart';
import 'package:iris/models/storages/storage.dart';
import 'package:iris/models/store/app_state.dart';
import 'package:iris/store/use_app_store.dart';
import 'package:iris/store/use_history_store.dart';
import 'package:iris/store/use_play_queue_store.dart';
import 'package:iris/store/use_player_ui_store.dart';
import 'package:iris/store/use_storage_store.dart';
import 'package:iris/utils/logger.dart';
import 'package:iris/utils/platform.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:media_stream/media_stream.dart';
import 'package:path_provider/path_provider.dart';

MediaKitPlayer useMediaKitPlayer(BuildContext context) {
  final player = useMemoized(
    () => Player(
      configuration: const PlayerConfiguration(
        libass: true,
      ),
    ),
  );

  final controller = useMemoized(() => VideoController(player));

  final rate = useAppStore().select(context, (state) => state.rate);
  final volume = useAppStore().select(context, (state) => state.volume);
  final isMuted = useAppStore().select(context, (state) => state.isMuted);

  useEffect(() {
    () async {
      player.setSubtitleTrack(SubtitleTrack.no());
      player.setRate(rate);
      player.setVolume(isMuted ? 0 : volume.toDouble());

      if (Platform.isAndroid) {
        NativePlayer nativePlayer = player.platform as NativePlayer;

        final appSupportDir = await getApplicationSupportDirectory();
        final String fontsDir = "${appSupportDir.path}/fonts";

        final Directory fontsDirectory = Directory(fontsDir);
        if (!await fontsDirectory.exists()) {
          await fontsDirectory.create(recursive: true);
          logger('fonts directory created');
        }

        final File file = File("$fontsDir/NotoSansCJKsc-Medium.otf");
        if (!await file.exists()) {
          final ByteData data =
              await rootBundle.load("assets/fonts/NotoSansCJKsc-Medium.otf");
          final Uint8List buffer = data.buffer.asUint8List();
          await file.create(recursive: true);
          await file.writeAsBytes(buffer);
          logger('NotoSansCJKsc-Medium.otf copied');
        }

        await nativePlayer.setProperty("sub-fonts-dir", fontsDir);
        await nativePlayer.setProperty("sub-font", "NotoSansCJKsc-Medium");
      }
    }();
    return () {
      player.dispose();
    };
  }, []);

  final List<PlayQueueItem> playQueue =
      usePlayQueueStore().select(context, (state) => state.playQueue);
  final int currentIndex =
      usePlayQueueStore().select(context, (state) => state.currentIndex);
  final bool autoPlay =
      useAppStore().select(context, (state) => state.autoPlay);
  final Repeat repeat = useAppStore().select(context, (state) => state.repeat);
  final bool alwaysPlayFromBeginning =
      useAppStore().select(context, (state) => state.alwaysPlayFromBeginning);

  final history = useHistoryStore().select(context, (state) => state.history);

  final int currentPlayIndex = useMemoized(
      () => playQueue.indexWhere((element) => element.index == currentIndex),
      [playQueue, currentIndex]);

  final FileItem? file = useMemoized(
      () => playQueue.isEmpty || currentPlayIndex < 0
          ? null
          : playQueue[currentPlayIndex].file,
      [playQueue, currentPlayIndex]);

  final playingStream = useMemoized(() => player.stream.playing, []);
  bool playing = useStream(playingStream).data ?? false;

  final videoParamsStream = useMemoized(() => player.stream.videoParams, []);
  VideoParams? videoParams = useStream(videoParamsStream).data;

  // AudioParams? audioParams = useStream(player.stream.audioParams).data;

  final positionStream = useMemoized(() => player.stream.position, []);
  final position = useStream(positionStream).data ?? Duration.zero;

  final durationStream = useMemoized(() => player.stream.duration, []);
  Duration duration = useStream(durationStream).data ?? Duration.zero;

  final bufferStream = useMemoized(() => player.stream.buffer, []);
  Duration buffer = useStream(bufferStream).data ?? Duration.zero;

  final completedStream = useMemoized(() => player.stream.completed, []);
  bool completed = useStream(completedStream).data ?? false;

  // double rate = useStream(player.stream.rate).data ?? 1.0;

  final trackStream = useMemoized(() => player.stream.track, []);
  Track? track = useStream(trackStream).data;
  AudioTrack audio =
      useMemoized(() => track?.audio ?? AudioTrack.no(), [track?.audio]);
  SubtitleTrack subtitle = useMemoized(
      () => track?.subtitle ?? SubtitleTrack.no(), [track?.subtitle]);

  final tracksStream = useMemoized(() => player.stream.tracks, []);
  Tracks? tracks = useStream(tracksStream).data;
  List<AudioTrack> audios =
      useMemoized(() => (tracks?.audio ?? []), [tracks?.audio]);
  List<SubtitleTrack> subtitles = useMemoized(
      () => [...(tracks?.subtitle ?? [])]
        ..removeWhere((subtitle) => subtitle == SubtitleTrack.auto()),
      [tracks?.subtitle]);

  final List<Subtitle>? externalSubtitles = useMemoized(
      () => [...file?.subtitles ?? []]..removeWhere(
          (subtitle) => subtitles.any((item) => item.title == subtitle.name)),
      [file?.subtitles, subtitles]);

  final isInitializing = useState(false);

  MediaStream mediaStream = useMemoized(() => MediaStream(), []);

  Future<void> init(FileItem file) async {
    if (file.uri == '') return;
    isInitializing.value = true;

    try {
      final storage = useStorageStore().findById(file.storageId);
      final auth = storage?.getAuth();
      logger('Open file: $file');
      await player.open(
        Media(
          file.storageType == StorageType.ftp
              ? '${mediaStream.url}/${file.uri}'
              : file.uri,
          httpHeaders: auth != null ? {'authorization': auth} : {},
        ),
        play: autoPlay,
      );
    } catch (e) {
      logger('Error initializing player: $e');
    }

    isInitializing.value = false;
  }

  useEffect(() {
    if (file == null || playQueue.isEmpty) {
      player.stop();
    } else {
      init(file);
    }
    return () {
      if (isAndroid &&
          globals.initUri == file?.uri &&
          globals.initUri != null &&
          globals.initUri!.startsWith('content://')) {
        return;
      }

      if (file != null && player.state.duration != Duration.zero) {
        logger(
            'Save progress: ${file.name}, position: ${player.state.position}, duration: ${player.state.duration}');
        useHistoryStore().add(Progress(
          dateTime: DateTime.now().toUtc(),
          position: player.state.position,
          duration: player.state.duration,
          file: file,
        ));
      }
    };
  }, [file]);

  useEffect(() {
    () async {
      if (duration == Duration.zero) {
        await player.setSubtitleTrack(SubtitleTrack.no());
        return;
      }
      // 查询播放进度
      if (file != null && file.type == ContentType.video) {
        Progress? progress = history[file.getID()];
        if (progress != null) {
          if (!alwaysPlayFromBeginning &&
              (progress.duration.inMilliseconds -
                      progress.position.inMilliseconds) >
                  5000) {
            logger(
                'Resume progress: ${file.name} position: ${progress.position} duration: ${progress.duration}');
            await player.seek(progress.position);
          }
        }
      }
      // 设置字幕
      if (externalSubtitles!.isNotEmpty) {
        logger('Set external subtitle: ${externalSubtitles[0]}');
        final uri = file?.storageType == StorageType.ftp
            ? '${mediaStream.url}/${externalSubtitles[0].uri}'
            : externalSubtitles[0].uri;
        logger('External subtitle uri: $uri');
        await player.setSubtitleTrack(
          SubtitleTrack.uri(
            uri,
            title: externalSubtitles[0].name,
          ),
        );
      } else if (subtitles.length > 1) {
        logger(
            'Set subtitle: ${subtitles[1].title ?? subtitles[1].language ?? subtitles[1].id}');
        await player.setSubtitleTrack(subtitles[1]);
      } else {
        await player.setSubtitleTrack(SubtitleTrack.no());
      }
    }();
    return;
  }, [duration]);

  useEffect(() {
    () async {
      if (completed) {
        if (repeat == Repeat.one) return;
        if (currentPlayIndex == playQueue.length - 1) {
          if (repeat == Repeat.all) {
            await usePlayQueueStore().updateCurrentIndex(playQueue[0].index);
          }
        } else {
          await usePlayQueueStore()
              .updateCurrentIndex(playQueue[currentPlayIndex + 1].index);
        }
      }
    }();
    return null;
  }, [completed, repeat]);

  useEffect(() {
    player.setRate(rate);
    return;
  }, [rate]);

  useEffect(() {
    player.setVolume(isMuted ? 0 : volume.toDouble());
    return;
  }, [volume, isMuted]);

  useEffect(() {
    logger('$repeat');
    if (repeat == Repeat.one) {
      player.setPlaylistMode(PlaylistMode.loop);
    } else {
      player.setPlaylistMode(PlaylistMode.none);
    }
    return;
  }, [repeat]);

  useEffect(() {
    if (file?.type != ContentType.video) {
      usePlayerUiStore().updateAspectRatio(0);
      return;
    }

    final width = videoParams?.w ?? 0;
    final height = videoParams?.h ?? 0;
    if (width == 0 || height == 0) {
      usePlayerUiStore().updateAspectRatio(0);
    } else {
      usePlayerUiStore().updateAspectRatio(width / height);
    }
    return;
  }, [file?.type, videoParams?.w, videoParams?.h]);

  Future<void> saveProgress() async {
    if (isAndroid &&
        globals.initUri == file?.uri &&
        globals.initUri != null &&
        globals.initUri!.startsWith('content://')) {
      return;
    }

    if (file != null && player.state.duration != Duration.zero) {
      logger(
          'Save progress: ${file.name}, position: ${player.state.position}, duration: ${player.state.duration}');
      useHistoryStore().add(Progress(
        dateTime: DateTime.now().toUtc(),
        position: player.state.position,
        duration: player.state.duration,
        file: file,
      ));
    }
  }

  useEffect(() => saveProgress, []);

  Future<void> play() async {
    if (duration == Duration.zero && file != null && !isInitializing.value) {
      await init(file);
    }
    await player.play();
  }

  Future<void> pause() async {
    await player.pause();
  }

  Future<void> seek(Duration newPosition) async =>
      newPosition.inMilliseconds < 0
          ? await player.seek(Duration.zero)
          : newPosition.inMilliseconds > duration.inMilliseconds
              ? await player.seek(duration)
              : await player.seek(newPosition);

  Future<void> backward(int seconds) async {
    await seek(Duration(seconds: position.inSeconds - seconds));
  }

  Future<void> forward(int seconds) async {
    await seek(Duration(seconds: position.inSeconds + seconds));
  }

  Future<void> stepBackward() async {
    final nativePlayer = player.platform;
    if (nativePlayer is NativePlayer && file?.type == ContentType.video) {
      await nativePlayer.command(['frame-back-step']);
      logger('Step backward');
    }
  }

  Future<void> stepForward() async {
    final nativePlayer = player.platform;
    if (nativePlayer is NativePlayer && file?.type == ContentType.video) {
      await nativePlayer.command(['frame-step']);
      logger('Step forward');
    }
  }

  final mediaKitPlayer = useMemoized(
    () => MediaKitPlayer(
      player: player,
      controller: controller,
      subtitle: subtitle,
      subtitles: subtitles,
      externalSubtitles: externalSubtitles ?? [],
      audio: audio,
      audios: audios,
      isInitializing: isInitializing.value,
      isPlaying: playing,
      position: duration == Duration.zero ? Duration.zero : position,
      duration: duration,
      buffer: duration == Duration.zero ? Duration.zero : buffer,
      width: videoParams?.w?.toDouble() ?? 0,
      height: videoParams?.h?.toDouble() ?? 0,
      saveProgress: saveProgress,
      play: play,
      pause: pause,
      backward: backward,
      forward: forward,
      stepBackward: stepBackward,
      stepForward: stepForward,
      seek: seek,
    ),
    [
      player,
      controller,
      subtitle,
      subtitles,
      externalSubtitles,
      audio,
      audios,
      isInitializing.value,
      playing,
      position,
      duration,
      buffer,
      videoParams?.w,
      videoParams?.h,
      saveProgress,
      play,
      pause,
      backward,
      forward,
      stepBackward,
      stepForward,
      seek,
    ],
  );

  return mediaKitPlayer;
}


================================================
FILE: lib/hooks/ui/use_full_screen.dart
================================================
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_zustand/flutter_zustand.dart';
import 'package:iris/store/use_player_ui_store.dart';
import 'package:iris/utils/platform.dart';
import 'package:window_manager/window_manager.dart';

void useFullScreen() {
  final context = useContext();

  final isFullScreen =
      usePlayerUiStore().select(context, (state) => state.isFullScreen);

  useEffect(() {
    () async {
      if (isDesktop) {
        await windowManager.setFullScreen(isFullScreen);
      }
    }();
    return;
  }, [isFullScreen]);
}


================================================
FILE: lib/hooks/ui/use_orientation.dart
================================================
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_zustand/flutter_zustand.dart';
import 'package:iris/models/store/app_state.dart';
import 'package:iris/store/use_app_store.dart';
import 'package:iris/store/use_player_ui_store.dart';

void useOrientation() {
  final context = useContext();
  final orientation =
      useAppStore().select(context, (state) => state.orientation);

  final aspectRatio =
      usePlayerUiStore().select(context, (state) => state.aspectRatio);

  setOrientation(ScreenOrientation orientation, double? aspect) {
    if (Platform.isAndroid || Platform.isIOS) {
      switch (orientation) {
        case ScreenOrientation.device:
          SystemChrome.setPreferredOrientations([]);
          break;
        case ScreenOrientation.landscape:
          SystemChrome.setPreferredOrientations([
            DeviceOrientation.landscapeLeft,
            DeviceOrientation.landscapeRight,
          ]);
          break;
        case ScreenOrientation.portrait:
          SystemChrome.setPreferredOrientations([
            DeviceOrientation.portraitUp,
            DeviceOrientation.portraitDown,
          ]);
          break;
      }
    }
  }

  useEffect(() {
    setOrientation(orientation, aspectRatio);
    return () => SystemChrome.setPreferredOrientations([]);
  }, []);

  useEffect(() {
    setOrientation(orientation, aspectRatio);
    return;
  }, [orientation, aspectRatio]);
}


================================================
FILE: lib/hooks/ui/use_resize_window.dart
================================================
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_zustand/flutter_zustand.dart';
import 'package:iris/models/file.dart';
import 'package:iris/store/use_app_store.dart';
import 'package:iris/store/use_play_queue_store.dart';
import 'package:iris/store/use_player_ui_store.dart';
import 'package:iris/utils/logger.dart';
import 'package:iris/utils/platform.dart';
import 'package:window_manager/window_manager.dart';
import 'package:window_size/window_size.dart';

Future<void> _applyResize(Rect newBounds) async {
  if (await windowManager.isFullScreen() || await windowManager.isMaximized()) {
    return;
  }
  await windowManager.setBounds(newBounds, animate: true);
}

void useResizeWindow() {
  final context = useContext();

  final autoResize = useAppStore().select(context, (state) => state.autoResize);
  final isFullScreen =
      usePlayerUiStore().select(context, (state) => state.isFullScreen);
  final aspectRatio =
      usePlayerUiStore().select(context, (state) => state.aspectRatio);

  final currentPlay = usePlayQueueStore().select(context, (state) {
    final index =
        state.playQueue.indexWhere((e) => e.index == state.currentIndex);
    return index != -1 ? state.playQueue[index] : null;
  });
  final contentType = currentPlay?.file.type ?? ContentType.other;

  final prevIsFullScreen = usePrevious(isFullScreen);
  final prevAspectRatio = usePrevious(aspectRatio);

  useEffect(() {
    if (!isDesktop) return;

    Future<void> performResize() async {
      if (isFullScreen) return;

      if (!autoResize) {
        await windowManager.setAspectRatio(0);
        return;
      }

      if (contentType == ContentType.audio) {
        await windowManager.setAspectRatio(0);
        return;
      }

      if (contentType == ContentType.video) {
        if (aspectRatio <= 0) {
          await windowManager.setAspectRatio(0);
          return;
        }

        await windowManager.setAspectRatio(aspectRatio);
        final oldBounds = await windowManager.getBounds();
        final screen = await getCurrentScreen();
        if (screen == null) return;

        if (oldBounds.size.aspectRatio.toStringAsFixed(2) ==
            aspectRatio.toStringAsFixed(2)) {
          return;
        }

        Size newSize;
        final bool isPreviousPortrait = (prevAspectRatio ?? 1.0) < 1.0;
        final bool isCurrentLandscape = aspectRatio >= 1.0;

        if (isPreviousPortrait && isCurrentLandscape) {
          logger('Resize rule: Portrait to Landscape (Height-based)');
          double newHeight = oldBounds.height;
          double newWidth = newHeight * aspectRatio;
          newSize = Size(newWidth, newHeight);
        } else {
          logger('Resize rule: Standard (Normalized Area-based)');
          double currentArea = oldBounds.width * oldBounds.height;
          const double standardAspectRatio = 16.0 / 9.0;
          double normalizedHeight =
              math.sqrt(currentArea / standardAspectRatio);

          double newHeight = normalizedHeight;
          double newWidth = newHeight * aspectRatio;
          newSize = Size(newWidth, newHeight);
        }

        double maxWidth = screen.frame.width / screen.scaleFactor * 0.95;
        double maxHeight = screen.frame.height / screen.scaleFactor * 0.95;

        if (newSize.width > maxWidth) {
          newSize = Size(maxWidth, maxWidth / aspectRatio);
        }
        if (newSize.height > maxHeight) {
          newSize = Size(maxHeight * aspectRatio, maxHeight);
        }

        final newPosition = Offset(
          oldBounds.left + (oldBounds.width - newSize.width) / 2,
          oldBounds.top + (oldBounds.height - newSize.height) / 2,
        );

        await _applyResize(Rect.fromLTWH(
            newPosition.dx, newPosition.dy, newSize.width, newSize.height));
      }
    }

    final wasFullScreen = prevIsFullScreen == true;
    if (wasFullScreen && !isFullScreen) {
      Future.delayed(const Duration(milliseconds: 50), performResize);
    } else {
      performResize();
    }

    return null;
  }, [
    autoResize,
    isFullScreen,
    aspectRatio,
    contentType,
  ]);
}


================================================
FILE: lib/hooks/use_app_lifecycle.dart
================================================
import 'dart:ui';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:iris/models/player.dart';
import 'package:iris/utils/logger.dart';
import 'package:provider/provider.dart';

void useAppLifecycle() {
  final context = useContext();

  AppLifecycleState? appLifecycleState = useAppLifecycleState();

  useEffect(() {
    try {
      if (appLifecycleState == AppLifecycleState.paused) {
        logger('App lifecycle state: paused');
        context.read<MediaPlayer>().saveProgress();
      }
    } catch (e) {
      logger('App lifecycle state error: $e');
    }
    return;
  }, [appLifecycleState]);
}


================================================
FILE: lib/hooks/use_brightness.dart
================================================
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:iris/utils/logger.dart';
import 'package:screen_brightness/screen_brightness.dart';

ValueNotifier<double?> useBrightness(bool isGesture) {
  final brightness = useState<double?>(null);

  useEffect(() {
    try {
      () async {
        if (!isGesture) return;
        brightness.value = await ScreenBrightness.instance.application;
      }();
    } catch (e) {
      logger('Error getting brightness: $e');
    }
    return () => brightness.value = null;
  }, [isGesture]);

  useEffect(() {
    try {
      if (brightness.value != null && isGesture) {
        ScreenBrightness.instance
            .setApplicationScreenBrightness(brightness.value!);
      }
    } catch (e) {
      logger('Error setting brightness: $e');
    }
    return;
  }, [brightness.value]);

  // 退出时重置亮度
  useEffect(
    () => () {
      try {
        ScreenBrightness.instance.resetApplicationScreenBrightness();
      } catch (e) {
        logger('Error resetting brightness: $e');
      }
    },
    [],
  );

  return brightness;
}


================================================
FILE: lib/hooks/use_cover.dart
================================================
import 'package:collection/collection.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_zustand/flutter_zustand.dart';
import 'package:iris/models/file.dart';
import 'package:iris/models/storages/local.dart';
import 'package:iris/models/storages/storage.dart';
import 'package:iris/store/use_play_queue_store.dart';
import 'package:iris/store/use_storage_store.dart';

FileItem? useCover() {
  final context = useContext();

  final playQueue =
      usePlayQueueStore().select(context, (state) => state.playQueue);
  final currentIndex =
      usePlayQueueStore().select(context, (state) => state.currentIndex);

  final FileItem? file = useMemoized(() {
    final index =
        playQueue.indexWhere((element) => element.index == currentIndex);
    return playQueue.isEmpty || index < 0 ? null : playQueue[index].file;
  }, [playQueue, currentIndex]);

  final localStoragesFuture =
      useMemoized(() async => await getLocalStorages(context), []);
  final localStorages = useFuture(localStoragesFuture).data ?? [];

  final storages = useStorageStore().select(context, (state) => state.storages);

  final Storage? storage = useMemoized(
      () => file == null
          ? null
          : [...localStorages, ...storages]
              .firstWhereOrNull((storage) => storage.id == file.storageId),
      [file, localStorages, storages]);

  final cover = useState<FileItem?>(null);

  useEffect(() {
    () async {
      if (storage == null || file == null || file.type != ContentType.audio) {
        cover.value = null;
        return;
      }

      final dir =
          file.path.isEmpty ? <String>[] : ([...file.path]..removeLast());

      final files = await storage.getFiles(dir);

      final images =
          files.where((file) => file.type == ContentType.image).toList();

      cover.value = images.firstWhereOrNull((image) =>
              image.name.split('.').first.toLowerCase() == 'cover') ??
          images.firstWhereOrNull((image) =>
              image.name.toLowerCase().startsWith('cover') ||
              image.name.toLowerCase().startsWith('folder')) ??
          images.firstOrNull;
    }();
    return null;
  }, [storage, file]);

  return cover.value;
}


================================================
FILE: lib/hooks/use_gesture.dart
================================================
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
import 'package:iris/globals.dart' show speedStops, speedSelectorItemWidth;
import 'package:iris/hooks/use_brightness.dart';
import 'package:iris/hooks/use_volume.dart';
import 'package:iris/models/player.dart';
import 'package:iris/store/use_app_store.dart';
import 'package:iris/store/use_player_ui_store.dart';
import 'package:iris/utils/logger.dart';
import 'package:iris/utils/platform.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';

class Gesture {
  final void Function(TapDownDetails) onTapDown;
  final void Function() onTap;
  final void Function(TapDownDetails) onDoubleTapDown;
  final void Function(LongPressStartDetails) onLongPressStart;
  final void Function(LongPressMoveUpdateDetails) onLongPressMoveUpdate;
  final void Function(LongPressEndDetails) onLongPressEnd;
  final void Function() onLongPressCancel;
  final void Function(DragStartDetails) onPanStart;
  final void Function(DragUpdateDetails) onPanUpdate;
  final void Function(DragEndDetails) onPanEnd;
  final void Function() onPanCancel;
  final void Function(PointerHoverEvent) onHover;

  final bool isLongPress;
  final bool isLeftGesture;
  final bool isRightGesture;
  final double? brightness;
  final double? volume;

  Gesture({
    required this.onTapDown,
    required this.onTap,
    required this.onDoubleTapDown,
    required this.onLongPressStart,
    required this.onLongPressMoveUpdate,
    required this.onLongPressEnd,
    required this.onLongPressCancel,
    required this.onPanStart,
    required this.onPanUpdate,
    required this.onPanEnd,
    required this.onPanCancel,
    required this.onHover,
    required this.isLongPress,
    required this.isLeftGesture,
    required this.isRightGesture,
    required this.brightness,
    required this.volume,
  });
}

Gesture useGesture({
  required void Function() showControl,
  required void Function() hideControl,
  required void Function() showProgress,
  required void Function(Offset position) showSpeedSelector,
  required void Function(double finalSpeed) hideSpeedSelector,
  required void Function(double speed, double visualOffset) updateSelectedSpeed,
}) {
  final context = useContext();

  final gestureState = useRef({
    'isTouch': false,
    'isLongPress': false,
    'isDragging': false,
    'startPanOffset': Offset.zero,
    'startSeekPosition': Duration.zero,
    'panDirection': null, // null: 未确定, Axis.horizontal, Axis.vertical
  });

  final isLeftGesture = useState(false);
  final isRightGesture = useState(false);

  final brightness = useBrightness(isLeftGesture.value);
  final volume = useVolume(isRightGesture.value);

  void onTapDown(TapDownDetails details) {
    if (details.kind == PointerDeviceKind.touch) {
      gestureState.value['isTouch'] = true;
    }
  }

  void onTap() {
    if (usePlayerUiStore().state.isShowControl) {
      hideControl();
    } else {
      showControl();
    }
  }

  void onDoubleTapDown(TapDownDetails details) {
    final player = context.read<MediaPlayer>();

    if (details.kind == PointerDeviceKind.touch) {
      final screenWidth = MediaQuery.sizeOf(context).width;
      final tapDx = details.globalPosition.dx;

      if (tapDx > screenWidth * 0.75) {
        // 右侧 25%
        showProgress();
        player.forward(10);
      } else if (tapDx < screenWidth * 0.25) {
        // 左侧 25%
        showProgress();
        player.backward(10);
      } else {
        // 中间 50%
        if (player.isPlaying) {
          useAppStore().updateAutoPlay(false);
          player.pause();
          showControl();
        } else {
          useAppStore().updateAutoPlay(true);
          player.play();
        }
      }
    } else if (isDesktop) {
      // 桌面端双击切换全屏
      usePlayerUiStore()
          .updateFullScreen(!usePlayerUiStore().state.isFullScreen);
    }
  }

  void onLongPressStart(LongPressStartDetails details) {
    if (gestureState.value['isTouch'] as bool &&
        context.read<MediaPlayer>().isPlaying) {
      gestureState.value['isLongPress'] = true;
      gestureState.value['startPanOffset'] = details.globalPosition;

      final currentRate = useAppStore().state.rate;
      final closestSpeed = speedStops.reduce(
          (a, b) => (a - currentRate).abs() < (b - currentRate).abs() ? a : b);
      gestureState.value['initialSpeedIndex'] =
          speedStops.indexOf(closestSpeed);

      showSpeedSelector(details.globalPosition);
      updateSelectedSpeed(closestSpeed, 0.0);
    }
  }

  void onLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
    if (!(gestureState.value['isLongPress'] as bool)) return;

    final startDx = (gestureState.value['startPanOffset'] as Offset).dx;
    final currentDx = details.globalPosition.dx;
    final deltaDx = currentDx - startDx;

    const double sensitivity = speedSelectorItemWidth;
    final double visualOffset = deltaDx;

    int steps = (-visualOffset / sensitivity).round();

    int initialIndex = gestureState.value['initialSpeedIndex'] as int? ??
        speedStops.indexOf(1.0);
    int finalIndex = (initialIndex + steps).clamp(0, speedStops.length - 1);

    double selectedSpeed = speedStops[finalIndex];

    updateSelectedSpeed(selectedSpeed, visualOffset);
    if (useAppStore().state.rate != selectedSpeed) {
      useAppStore().updateRate(selectedSpeed);
    }
  }

  void onLongPressEnd(LongPressEndDetails details) {
    if (gestureState.value['isLongPress'] as bool) {
      hideSpeedSelector(useAppStore().state.rate);
    }
    gestureState.value['isLongPress'] = false;
    gestureState.value['isTouch'] = false;
  }

  void onLongPressCancel() {
    if (gestureState.value['isLongPress'] as bool) {
      hideSpeedSelector(useAppStore().state.rate);
    }
    gestureState.value['isLongPress'] = false;
    gestureState.value['isTouch'] = false;
  }

  void onPanStart(DragStartDetails details) {
    if (isDesktop && details.kind != PointerDeviceKind.touch) {
      windowManager.startDragging();
      return;
    }

    if (gestureState.value['isLongPress'] as bool) {
      return;
    }

    if (details.kind == PointerDeviceKind.touch) {
      const double edgeDeadZone = 48.0;
      final screenSize = MediaQuery.sizeOf(context);
      final startDx = details.globalPosition.dx;

      if (startDx < edgeDeadZone || startDx > screenSize.width - edgeDeadZone) {
        logger("Edge swipe detected. Ignoring for system navigation.");
        return;
      }

      gestureState.value['isTouch'] = true;
      gestureState.value['isDragging'] = true;
      gestureState.value['startPanOffset'] = details.globalPosition;
      gestureState.value['startSeekPosition'] =
          context.read<MediaPlayer>().position;
      gestureState.value['panDirection'] = null;
      isLeftGesture.value = false;
      isRightGesture.value = false;
    }
  }

  void onPanUpdate(DragUpdateDetails details) {
    if (!(gestureState.value['isDragging'] as bool)) return;

    final startOffset = gestureState.value['startPanOffset'] as Offset;
    final totalDx = details.globalPosition.dx - startOffset.dx;
    final totalDy = details.globalPosition.dy - startOffset.dy;

    // 增加手势“死区”,防止误触
    const double panDeadzone = 8.0;
    if (gestureState.value['panDirection'] == null) {
      if (totalDx.abs() > panDeadzone || totalDy.abs() > panDeadzone) {
        gestureState.value['panDirection'] =
            totalDx.abs() > totalDy.abs() ? Axis.horizontal : Axis.vertical;
      }
    }

    final direction = gestureState.value['panDirection'];
    if (direction == null) return;

    // 水平滑动 (Seek)
    if (direction == Axis.horizontal) {
      if (!usePlayerUiStore().state.isSeeking) {
        usePlayerUiStore().updateIsSeeking(true);
      }

      const double sensitivity = 3.0; // 每滑动3像素代表1秒
      final double seekSecondsOffset = totalDx / sensitivity;
      final startSeconds =
          (gestureState.value['startSeekPosition'] as Duration).inSeconds;

      int targetSeconds = (startSeconds + seekSecondsOffset).round();

      // 边界检查
      targetSeconds = targetSeconds.clamp(
          0, context.read<MediaPlayer>().duration.inSeconds);

      context.read<MediaPlayer>().seek(Duration(seconds: targetSeconds));
      showProgress();
    }

    // 垂直滑动 (亮度和音量)
    if (direction == Axis.vertical) {
      // 仅在垂直滑动开始时判断一次左右区域
      if (!isLeftGesture.value && !isRightGesture.value) {
        isLeftGesture.value =
            startOffset.dx < MediaQuery.sizeOf(context).width / 2;
        isRightGesture.value = !isLeftGesture.value;

        if (isRightGesture.value) {
          FlutterVolumeController.updateShowSystemUI(false);
        }
      }

      final double dy = details.delta.dy;

      if (isLeftGesture.value && brightness.value != null) {
        final newBrightness = brightness.value! - dy / 200;
        brightness.value = newBrightness.clamp(0.0, 1.0);
      }

      if (isRightGesture.value && volume.value != null) {
        final newVolume = volume.value! - dy / 200;
        volume.value = newVolume.clamp(0.0, 1.0);
      }
    }
  }

  // ignore: no_leading_underscores_for_local_identifiers
  void _resetPanState() {
    if (usePlayerUiStore().state.isSeeking) {
      usePlayerUiStore().updateIsSeeking(false);
    }
    gestureState.value = {
      ...gestureState.value,
      'isDragging': false,
      'panDirection': null,
    };
    isLeftGesture.value = false;
    isRightGesture.value = false;

    FlutterVolumeController.updateShowSystemUI(true);
  }

  void onPanEnd(DragEndDetails details) => _resetPanState();
  void onPanCancel() => _resetPanState();

  void onHover(PointerHoverEvent event) {
    if (event.kind != PointerDeviceKind.touch) {
      usePlayerUiStore().updateIsHovering(true);
      showControl();
    }
  }

  return Gesture(
    onTapDown: onTapDown,
    onTap: onTap,
    onDoubleTapDown: onDoubleTapDown,
    onLongPressStart: onLongPressStart,
    onLongPressMoveUpdate: onLongPressMoveUpdate,
    onLongPressEnd: onLongPressEnd,
    onLongPressCancel: onLongPressCancel,
    onPanStart: onPanStart,
    onPanUpdate: onPanUpdate,
    onPanEnd: onPanEnd,
    onPanCancel: onPanCancel,
    onHover: onHover,
    isLongPress: gestureState.value['isLongPress'] as bool,
    isLeftGesture: isLeftGesture.value,
    isRightGesture: isRightGesture.value,
    brightness: brightness.value,
    volume: volume.value,
  );
}


================================================
FILE: lib/hooks/use_keyboard.dart
================================================
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:iris/globals.dart';
import 'package:iris/models/player.dart';
import 'package:iris/models/storages/local.dart';
import 'package:iris/widgets/popups/history.dart';
import 'package:iris/widgets/popups/play_queue.dart';
import 'package:iris/widgets/popups/track/subtitle_and_audio_track.dart';
import 'package:iris/widgets/popups/settings/settings.dart';
import 'package:iris/widgets/popups/storages/storages.dart';
import 'package:iris/store/use_app_store.dart';
import 'package:iris/store/use_play_queue_store.dart';
import 'package:iris/store/use_player_ui_store.dart';
import 'package:iris/utils/platform.dart';
import 'package:iris/widgets/bottom_sheets/show_open_link_bottom_sheet.dart';
import 'package:iris/widgets/dialogs/show_open_link_dialog.dart';
import 'package:iris/widgets/popup.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';

typedef KeyboardEvent = void Function(KeyEvent event);

KeyboardEvent useKeyboard({
  required void Function() showControl,
  required Future<void> Function(Future<void>) showControlForHover,
  required void Function() showProgress,
}) {
  final context = useContext();

  void onKeyEvent(KeyEvent event) async {
    final player = context.read<MediaPlayer>();

    if (event.runtimeType == KeyDownEvent) {
      if (HardwareKeyboard.instance.isAltPressed) {
        switch (event.logicalKey) {
          // 退出
          case LogicalKeyboardKey.keyX:
            showControl();
            await player.saveProgress();
            if (isDesktop) {
              windowManager.close();
            } else {
              SystemNavigator.pop();
              exit(0);
            }
        }
        return;
      }

      if (HardwareKeyboard.instance.isControlPressed) {
        final appState = useAppStore().state;
        switch (event.logicalKey) {
          // 上一个
          case LogicalKeyboardKey.arrowLeft:
            showControl();
            usePlayQueueStore().previous();
            break;
          // 下一个
          case LogicalKeyboardKey.arrowRight:
            showControl();
            usePlayQueueStore().next();
            break;
          // 设置
          case LogicalKeyboardKey.keyP:
            showControlForHover(
              showPopup(
                context: context,
                child: const Settings(),
                direction: PopupDirection.right,
              ),
            );
            break;
          // 打开文件
          case LogicalKeyboardKey.keyO:
            showControl();
            await pickLocalFile();
            showControl();
            break;
          // 随机
          case LogicalKeyboardKey.keyX:
            showControl();
            if (appState.shuffle) {
              usePlayQueueStore().sort();
            } else {
              usePlayQueueStore().shuffle();
            }
            useAppStore().updateShuffle(!appState.shuffle);
            break;
          // 循环
          case LogicalKeyboardKey.keyR:
            showControl();
            useAppStore().toggleRepeat();
            break;
          // 视频缩放
          case LogicalKeyboardKey.keyV:
            showControl();
            useAppStore().toggleFit();
            break;
          // 历史
          case LogicalKeyboardKey.keyH:
            showControlForHover(
              showPopup(
                context: context,
                child: const History(),
                direction: PopupDirection.right,
              ),
            );
            break;
          // 打开链接
          case LogicalKeyboardKey.keyL:
            showControl();
            isDesktop
                ? await showOpenLinkDialog(context)
                : await showOpenLinkBottomSheet(context);
            showControl();
            break;
          // 关闭当前播放媒体文件
          case LogicalKeyboardKey.keyC:
            showControl();
            player.pause();
            usePlayQueueStore().updateCurrentIndex(-1);
            break;
          // 静音
          case LogicalKeyboardKey.keyM:
            showControl();
            useAppStore().toggleMute();
            break;
          default:
            break;
        }
        return;
      }

      final playerUiState = usePlayerUiStore().state;
      switch (event.logicalKey) {
        // 播放 | 暂停
        case LogicalKeyboardKey.space:
        case LogicalKeyboardKey.mediaPlayPause:
          showControl();
          if (player.isPlaying) {
            useAppStore().updateAutoPlay(false);
            player.pause();
          } else {
            useAppStore().updateAutoPlay(true);
            player.play();
          }
          break;
        // 上一个
        case LogicalKeyboardKey.mediaTrackPrevious:
          usePlayQueueStore().previous();
          showControl();
          break;
        // 下一个
        case LogicalKeyboardKey.mediaTrackNext:
          showControl();
          usePlayQueueStore().next();
          break;
        // 存储
        case LogicalKeyboardKey.keyF:
          showControlForHover(
            showPopup(
              context: context,
              child: const Storages(),
              direction: PopupDirection.right,
            ),
          );
          break;
        // 播放队列
        case LogicalKeyboardKey.keyP:
          showControlForHover(
            showPopup(
              context: context,
              child: const PlayQueue(),
              direction: PopupDirection.right,
            ),
          );
          break;
        // 字幕和音轨
        case LogicalKeyboardKey.keyS:
          showControlForHover(
            showPopup(
              context: context,
              child: Provider<MediaPlayer>.value(
                value: context.read<MediaPlayer>(),
                child: const SubtitleAndAudioTrack(),
              ),
              direction: PopupDirection.right,
            ),
          );
          break;
        // 退出全屏
        case LogicalKeyboardKey.escape:
          if (isDesktop && playerUiState.isFullScreen) {
            usePlayerUiStore().updateFullScreen(false);
          }
          break;
        // 全屏
        case LogicalKeyboardKey.enter:
        case LogicalKeyboardKey.f11:
          if (isDesktop) {
            usePlayerUiStore().updateFullScreen(!playerUiState.isFullScreen);
          }
          break;
        case LogicalKeyboardKey.tab:
          showControl();
          break;
        case LogicalKeyboardKey.f10:
          showControl();
          await usePlayerUiStore().toggleIsAlwaysOnTop();
          break;
        case LogicalKeyboardKey.equal:
          await player.stepForward();
          break;
        case LogicalKeyboardKey.minus:
          await player.stepBackward();
          break;
        case LogicalKeyboardKey.contextMenu:
          showControl();
          moreMenuKey.currentState?.showButtonMenu();
          break;
        default:
          break;
      }
    }

    if (event.runtimeType == KeyDownEvent ||
        event.runtimeType == KeyRepeatEvent) {
      switch (event.logicalKey) {
        // 快退
        case LogicalKeyboardKey.arrowLeft:
          if (usePlayerUiStore().state.isShowControl) {
            showControl();
          } else {
            showProgress();
          }
          player.backward(5);
          break;
        // 快进
        case LogicalKeyboardKey.arrowRight:
          if (usePlayerUiStore().state.isShowControl) {
            showControl();
          } else {
            showProgress();
          }
          player.forward(5);
          break;
        // 提升音量
        case LogicalKeyboardKey.arrowUp:
          showControl();
          await useAppStore().updateVolume(useAppStore().state.volume + 1);
          break;
        // 降低音量
        case LogicalKeyboardKey.arrowDown:
          showControl();
          await useAppStore().updateVolume(useAppStore().state.volume - 1);
          break;
        default:
          break;
      }
    }
  }

  return onKeyEvent;
}


================================================
FILE: lib/hooks/use_volume.dart
================================================
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
import 'package:iris/utils/logger.dart';

ValueNotifier<double?> useVolume(bool isGesture) {
  final volume = useState<double?>(null);

  useEffect(() {
    try {
      () async {
        if (!isGesture) return;
        volume.value = await FlutterVolumeController.getVolume();
      }();
    } catch (e) {
      logger('Error getting volume: $e');
    }
    return () {
      volume.value = null;
    };
  }, [isGesture]);

  useEffect(() {
    try {
      if (volume.value != null && isGesture) {
        FlutterVolumeController.setVolume(volume.value!);
      }
    } catch (e) {
      logger('Error setting volume: $e');
    }
    return;
  }, [volume.value]);

  return volume;
}


================================================
FILE: lib/info.dart
================================================
class INFO {
  static const String title = 'IRIS';
  static const String author = '22';
  static const String authorUrl = 'https://github.com/nini22P';
  static const String githubUrl = 'https://github.com/nini22P/iris';
  static const String msStoreId = '9NML7WNHNRTJ';
}


================================================
FILE: lib/l10n/app_en.arb
================================================
{
  "@@locale": "en",
  "about": "About",
  "add": "Add",
  "add_favorite": "Add favorite",
  "add_folder": "Add folder",
  "add_ftp_storage": "Add FTP storage",
  "add_local_storage": "Add local storage",
  "add_storage": "Add storage",
  "add_to_play_queue": "Add to play queue",
  "add_webdav_storage": "Add WebDAV storage",
  "always_on_top_off": "Always on top: Off",
  "always_on_top_on": "Always on top: On",
  "always_play_from_beginning": "Always play from the beginning",
  "always_play_from_beginning_description": "When enabled, this option will automatically start the video from the beginning each time it is played",
  "app_description": "A lightweight video player",
  "audio": "audio",
  "audio_track": "Audio track",
  "author": "Author",
  "auto": "Auto",
  "auto_resize": "Auto resize window ratio",
  "back": "Back",
  "cancel": "Cancel",
  "check_update": "Check update",
  "checked_new_version": "Checked new version",
  "close": "Close",
  "confirmUpdate": "Confirm update",
  "crop": "Crop",
  "dark": "Datk",
  "dependencies": "Dependencies",
  "device": "Device",
  "download": "Download",
  "download_and_update": "Download and update",
  "download_error": "Download error",
  "edit": "Edit",
  "edit_folder": "Edit folder",
  "edit_ftp_storage": "Edit FTP storage",
  "edit_local_storage": "Edit local storage",
  "edit_webdav_storage": "Edit WebDAV storage",
  "enter_fullscreen": "Enter fullscreen",
  "exit": "Exit",
  "exit_app_back_again": "Press back again to exit.",
  "exit_fullscreen": "Exit fullscreen",
  "experimental": "Experimental",
  "favorites": "Favorites",
  "fit": "Fit",
  "folder": "Folder",
  "folder_first": "Folder first",
  "general": "General",
  "grant_storage_permission": "Grant Storage Permission",
  "history": "History",
  "home": "Home",
  "host": "Host",
  "landscape": "Landscape",
  "language": "Language",
  "last_modified": "Last modified",
  "light": "Light",
  "local_storage": "Local storage",
  "media_file_does_not_exist": "Media file does not exist",
  "menu": "Menu",
  "more_options": "More options",
  "mute": "Mute",
  "name": "Name",
  "network_storage": "Network storage",
  "next": "Next",
  "no_new_version": "No new version",
  "off": "Off",
  "ok": "OK",
  "on": "On",
  "open_file": "Open file",
  "open_in_folder": "Open in folder",
  "open_link": "Open link",
  "password": "Password",
  "path": "Path",
  "pause": "Pause",
  "play": "Play",
  "play_queue": "Play queue",
  "playback_speed": "Playback speed",
  "player_backend": "Player backend",
  "port": "Port",
  "portrait": "Portrait",
  "previous": "Previous",
  "refresh": "Refresh",
  "releasePage": "Release page",
  "remove": "Remove",
  "remove_favorite": "Remove favorite",
  "repeat_all": "Repeat: All",
  "repeat_none": "Repeat: None",
  "repeat_one": "Repeat: One",
  "retry": "Retry",
  "save": "Save",
  "screen_orientation": "ScreenOrientation",
  "select_language": "Select language",
  "settings": "Settings",
  "shuffle": "Shuffle",
  "size": "Size",
  "sort": "Sort",
  "source_code": "Source code",
  "stop": "Stop",
  "storage": "Storage",
  "stretch": "Stretch",
  "subtitle": "Subtitle",
  "subtitle_and_audio_track": "Subtitle and audio track",
  "subtitle_file_does_not_exist": "Subtitle file does not exist",
  "system": "System",
  "test_connection": "Test connection",
  "theme_mode": "Theme mode",
  "unable_to_fetch_files": "Unable to fetch files.",
  "unmute": "Unmute",
  "url": "URL",
  "usb_storage": "USB storage",
  "username": "Username",
  "version": "Version",
  "video": "Video",
  "video_zoom": "Video zoom",
  "volume": "Volume"
}

================================================
FILE: lib/l10n/app_zh.arb
================================================
{
  "@@locale": "zh",
  "about": "关于",
  "add": "添加",
  "add_favorite": "添加收藏",
  "add_folder": "添加文件夹",
  "add_ftp_storage": "添加 FTP 存储",
  "add_local_storage": "添加本地存储",
  "add_storage": "添加存储",
  "add_to_play_queue": "添加到播放队列",
  "add_webdav_storage": "添加 WebDAV 存储",
  "always_on_top_off": "窗口置顶: 关",
  "always_on_top_on": "窗口置顶: 开",
  "always_play_from_beginning": "总是从头开始播放",
  "always_play_from_beginning_description": "启用此选项后,每次播放视频时将自动从头开始播放",
  "app_description": "轻量级视频播放器",
  "audio": "音频",
  "audio_track": "音轨",
  "author": "作者",
  "auto": "自动",
  "auto_resize": "自动调整窗口比例",
  "back": "返回",
  "cancel": "取消",
  "check_update": "检查更新",
  "checked_new_version": "检查到新版本",
  "close": "关闭",
  "confirmUpdate": "确认更新",
  "crop": "裁切",
  "dark": "暗色",
  "dependencies": "开源库",
  "device": "设备",
  "download": "下载",
  "download_and_update": "下载并更新",
  "download_error": "下载错误",
  "edit": "编辑",
  "edit_folder": "编辑文件夹",
  "edit_ftp_storage": "编辑 FTP 存储",
  "edit_local_storage": "编辑本地存储",
  "edit_webdav_storage": "编辑 WebDAV 存储",
  "enter_fullscreen": "进入全屏",
  "exit": "退出",
  "exit_app_back_again": "再次点击返回退出应用。",
  "exit_fullscreen": "退出全屏",
  "experimental": "实验性",
  "favorites": "收藏",
  "fit": "适应",
  "folder": "文件夹",
  "folder_first": "文件夹优先",
  "general": "通用",
  "grant_storage_permission": "授予存储权限",
  "history": "历史",
  "home": "主页",
  "host": "主机",
  "landscape": "横向",
  "language": "语言",
  "last_modified": "最后修改",
  "light": "亮色",
  "local_storage": "本地存储",
  "media_file_does_not_exist": "媒体文件不存在",
  "menu": "菜单",
  "more_options": "更多选项",
  "mute": "静音",
  "name": "名称",
  "network_storage": "网络存储",
  "next": "下一个",
  "no_new_version": "没有新版本",
  "off": "关闭",
  "ok": "确定",
  "on": "开启",
  "open_file": "打开文件",
  "open_in_folder": "打开所在文件夹",
  "open_link": "打开链接",
  "password": "密码",
  "path": "路径",
  "pause": "暂停",
  "play": "播放",
  "play_queue": "播放队列",
  "playback_speed": "播放速度",
  "player_backend": "播放器后端",
  "port": "端口",
  "portrait": "纵向",
  "previous": "上一个",
  "refresh": "刷新",
  "releasePage": "发布页面",
  "remove": "移除",
  "remove_favorite": "移除收藏",
  "repeat_all": "重复: 全部",
  "repeat_none": "重复: 关闭",
  "repeat_one": "重复: 当前文件",
  "retry": "重试",
  "save": "保存",
  "screen_orientation": "屏幕方向",
  "select_language": "选择语言",
  "settings": "设置",
  "shuffle": "随机",
  "size": "大小",
  "sort": "排序",
  "source_code": "源码",
  "stop": "停止",
  "storage": "存储",
  "stretch": "拉伸",
  "subtitle": "字幕",
  "subtitle_and_audio_track": "字幕和音轨",
  "subtitle_file_does_not_exist": "字幕文件不存在",
  "system": "系统",
  "test_connection": "测试连接",
  "theme_mode": "主题模式",
  "unable_to_fetch_files": "无法获取文件",
  "unmute": "取消静音",
  "url": "URL",
  "usb_storage": "USB 存储",
  "username": "用户名",
  "version": "版本",
  "video": "视频",
  "video_zoom": "视频缩放",
  "volume": "音量"
}

================================================
FILE: lib/l10n/iso_639.dart
================================================
class Info {
  final List<String> en;

  const Info({required this.en});
}

const Map<String, Info> customLanguageCodes = {
  'chs': Info(en: ['Chinese (Simplified)']),
  'cht': Info(en: ['Chinese (Traditional)']),
};

const Map<String, Info> iso_639_1 = {
  'aa': Info(en: ['Afar']),
  'ab': Info(en: ['Abkhazian']),
  'ae': Info(en: ['Avestan']),
  'af': Info(en: ['Afrikaans']),
  'ak': Info(en: ['Akan']),
  'am': Info(en: ['Amharic']),
  'an': Info(en: ['Aragonese']),
  'ar': Info(en: ['Arabic']),
  'as': Info(en: ['Assamese']),
  'av': Info(en: ['Avaric']),
  'ay': Info(en: ['Aymara']),
  'az': Info(en: ['Azerbaijani']),
  'ba': Info(en: ['Bashkir']),
  'be': Info(en: ['Belarusian']),
  'bg': Info(en: ['Bulgarian']),
  'bh': Info(en: ['Bihari languages']),
  'bi': Info(en: ['Bislama']),
  'bm': Info(en: ['Bambara']),
  'bn': Info(en: ['Bengali']),
  'bo': Info(en: ['Tibetan']),
  'br': Info(en: ['Breton']),
  'bs': Info(en: ['Bosnian']),
  'ca': Info(en: ['Catalan', 'Valencian']),
  'ce': Info(en: ['Chechen']),
  'ch': Info(en: ['Chamorro']),
  'co': Info(en: ['Corsican']),
  'cr': Info(en: ['Cree']),
  'cs': Info(en: ['Czech']),
  'cu': Info(en: [
    'Church Slavic',
    'Old Slavonic',
    'Church Slavonic',
    'Old Bulgarian',
    'Old Church Slavonic'
  ]),
  'cv': Info(en: ['Chuvash']),
  'cy': Info(en: ['Welsh']),
  'da': Info(en: ['Danish']),
  'de': Info(en: ['German']),
  'dv': Info(en: ['Divehi', 'Dhivehi', 'Maldivian']),
  'dz': Info(en: ['Dzongkha']),
  'ee': Info(en: ['Ewe']),
  'el': Info(en: ['Greek, Modern (1453-)']),
  'en': Info(en: ['English']),
  'eo': Info(en: ['Esperanto']),
  'es': Info(en: ['Spanish', 'Castilian']),
  'et': Info(en: ['Estonian']),
  'eu': Info(en: ['Basque']),
  'fa': Info(en: ['Persian']),
  'ff': Info(en: ['Fulah']),
  'fi': Info(en: ['Finnish']),
  'fj': Info(en: ['Fijian']),
  'fo': Info(en: ['Faroese']),
  'fr': Info(en: ['French']),
  'fy': Info(en: ['Western Frisian']),
  'ga': Info(en: ['Irish']),
  'gd': Info(en: ['Gaelic', 'Scottish Gaelic']),
  'gl': Info(en: ['Galician']),
  'gn': Info(en: ['Guarani']),
  'gu': Info(en: ['Gujarati']),
  'gv': Info(en: ['Manx']),
  'ha': Info(en: ['Hausa']),
  'he': Info(en: ['Hebrew']),
  'hi': Info(en: ['Hindi']),
  'ho': Info(en: ['Hiri Motu']),
  'hr': Info(en: ['Croatian']),
  'ht': Info(en: ['Haitian', 'Haitian Creole']),
  'hu': Info(en: ['Hungarian']),
  'hy': Info(en: ['Armenian']),
  'hz': Info(en: ['Herero']),
  'ia':
      Info(en: ['Interlingua (International Auxiliary Language Association)']),
  'id': Info(en: ['Indonesian']),
  'ie': Info(en: ['Interlingue', 'Occidental']),
  'ig': Info(en: ['Igbo']),
  'ii': Info(en: ['Sichuan Yi', 'Nuosu']),
  'ik': Info(en: ['Inupiaq']),
  'io': Info(en: ['Ido']),
  'is': Info(en: ['Icelandic']),
  'it': Info(en: ['Italian']),
  'iu': Info(en: ['Inuktitut']),
  'ja': Info(en: ['Japanese']),
  'jv': Info(en: ['Javanese']),
  'ka': Info(en: ['Georgian']),
  'kg': Info(en: ['Kongo']),
  'ki': Info(en: ['Kikuyu', 'Gikuyu']),
  'kj': Info(en: ['Kuanyama', 'Kwanyama']),
  'kk': Info(en: ['Kazakh']),
  'kl': Info(en: ['Kalaallisut', 'Greenlandic']),
  'km': Info(en: ['Central Khmer']),
  'kn': Info(en: ['Kannada']),
  'ko': Info(en: ['Korean']),
  'kr': Info(en: ['Kanuri']),
  'ks': Info(en: ['Kashmiri']),
  'ku': Info(en: ['Kurdish']),
  'kv': Info(en: ['Komi']),
  'kw': Info(en: ['Cornish']),
  'ky': Info(en: ['Kirghiz', 'Kyrgyz']),
  'la': Info(en: ['Latin']),
  'lb': Info(en: ['Luxembourgish', 'Letzeburgesch']),
  'lg': Info(en: ['Ganda']),
  'li': Info(en: ['Limburgan', 'Limburger', 'Limburgish']),
  'ln': Info(en: ['Lingala']),
  'lo': Info(en: ['Lao']),
  'lt': Info(en: ['Lithuanian']),
  'lu': Info(en: ['Luba-Katanga']),
  'lv': Info(en: ['Latvian']),
  'mg': Info(en: ['Malagasy']),
  'mh': Info(en: ['Marshallese']),
  'mi': Info(en: ['Maori']),
  'mk': Info(en: ['Macedonian']),
  'ml': Info(en: ['Malayalam']),
  'mn': Info(en: ['Mongolian']),
  'mr': Info(en: ['Marathi']),
  'ms': Info(en: ['Malay']),
  'mt': Info(en: ['Maltese']),
  'my': Info(en: ['Burmese']),
  'na': Info(en: ['Nauru']),
  'nb': Info(en: ['Bokmål, Norwegian', 'Norwegian Bokmål']),
  'nd': Info(en: ['Ndebele, North', 'North Ndebele']),
  'ne': Info(en: ['Nepali']),
  'ng': Info(en: ['Ndonga']),
  'nl': Info(en: ['Dutch', 'Flemish']),
  'nn': Info(en: ['Norwegian Nynorsk', 'Nynorsk, Norwegian']),
  'no': Info(en: ['Norwegian']),
  'nr': Info(en: ['Ndebele, South', 'South Ndebele']),
  'nv': Info(en: ['Navajo', 'Navaho']),
  'ny': Info(en: ['Chichewa', 'Chewa', 'Nyanja']),
  'oc': Info(en: ['Occitan (post 1500)']),
  'oj': Info(en: ['Ojibwa']),
  'om': Info(en: ['Oromo']),
  'or': Info(en: ['Oriya']),
  'os': Info(en: ['Ossetian', 'Ossetic']),
  'pa': Info(en: ['Panjabi', 'Punjabi']),
  'pi': Info(en: ['Pali']),
  'pl': Info(en: ['Polish']),
  'ps': Info(en: ['Pushto', 'Pashto']),
  'pt': Info(en: ['Portuguese']),
  'qu': Info(en: ['Quechua']),
  'rm': Info(en: ['Romansh']),
  'rn': Info(en: ['Rundi']),
  'ro': Info(en: ['Romanian', 'Moldavian', 'Moldovan']),
  'ru': Info(en: ['Russian']),
  'rw': Info(en: ['Kinyarwanda']),
  'sa': Info(en: ['Sanskrit']),
  'sc': Info(en: ['Sardinian']),
  'sd': Info(en: ['Sindhi']),
  'se': Info(en: ['Northern Sami']),
  'sg': Info(en: ['Sango']),
  'si': Info(en: ['Sinhala', 'Sinhalese']),
  'sk': Info(en: ['Slovak']),
  'sl': Info(en: ['Slovenian']),
  'sm': Info(en: ['Samoan']),
  'sn': Info(en: ['Shona']),
  'so': Info(en: ['Somali']),
  'sq': Info(en: ['Albanian']),
  'sr': Info(en: ['Serbian']),
  'ss': Info(en: ['Swati']),
  'st': Info(en: ['Sotho, Southern']),
  'su': Info(en: ['Sundanese']),
  'sv': Info(en: ['Swedish']),
  'sw': Info(en: ['Swahili']),
  'ta': Info(en: ['Tamil']),
  'te': I
Download .txt
gitextract_lt5ecz80/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .metadata
├── .vscode/
│   ├── launch.json
│   └── settings.json
├── CHANGELOG.md
├── LICENSE
├── PRIVACY.md
├── README.md
├── README_CN.md
├── analysis_options.yaml
├── android/
│   ├── .gitignore
│   ├── app/
│   │   ├── build.gradle
│   │   ├── proguard-rules.pro
│   │   └── src/
│   │       ├── debug/
│   │       │   └── AndroidManifest.xml
│   │       ├── main/
│   │       │   ├── AndroidManifest.xml
│   │       │   ├── kotlin/
│   │       │   │   └── nini22p/
│   │       │   │       └── iris/
│   │       │   │           └── MainActivity.kt
│   │       │   └── res/
│   │       │       ├── drawable/
│   │       │       │   └── launch_background.xml
│   │       │       ├── drawable-v21/
│   │       │       │   └── launch_background.xml
│   │       │       ├── mipmap-anydpi-v26/
│   │       │       │   ├── ic_launcher.xml
│   │       │       │   └── ic_launcher_round.xml
│   │       │       ├── values/
│   │       │       │   ├── ic_launcher_background.xml
│   │       │       │   └── styles.xml
│   │       │       └── values-night/
│   │       │           └── styles.xml
│   │       └── profile/
│   │           └── AndroidManifest.xml
│   ├── build.gradle
│   ├── gradle/
│   │   └── wrapper/
│   │       └── gradle-wrapper.properties
│   ├── gradle.properties
│   └── settings.gradle
├── devtools_options.yaml
├── extract_log.py
├── inno.iss
├── ios/
│   ├── .gitignore
│   ├── Flutter/
│   │   ├── AppFrameworkInfo.plist
│   │   ├── Debug.xcconfig
│   │   └── Release.xcconfig
│   ├── Runner/
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets/
│   │   │   ├── AppIcon.appiconset/
│   │   │   │   └── Contents.json
│   │   │   └── LaunchImage.imageset/
│   │   │       ├── Contents.json
│   │   │       └── README.md
│   │   ├── Base.lproj/
│   │   │   ├── LaunchScreen.storyboard
│   │   │   └── Main.storyboard
│   │   ├── Info.plist
│   │   └── 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
├── l10n.yaml
├── lib/
│   ├── globals.dart
│   ├── hooks/
│   │   ├── player/
│   │   │   ├── use_fvp_player.dart
│   │   │   └── use_media_kit_player.dart
│   │   ├── ui/
│   │   │   ├── use_full_screen.dart
│   │   │   ├── use_orientation.dart
│   │   │   └── use_resize_window.dart
│   │   ├── use_app_lifecycle.dart
│   │   ├── use_brightness.dart
│   │   ├── use_cover.dart
│   │   ├── use_gesture.dart
│   │   ├── use_keyboard.dart
│   │   └── use_volume.dart
│   ├── info.dart
│   ├── l10n/
│   │   ├── app_en.arb
│   │   ├── app_zh.arb
│   │   ├── iso_639.dart
│   │   └── languages.dart
│   ├── main.dart
│   ├── models/
│   │   ├── file.dart
│   │   ├── player.dart
│   │   ├── progress.dart
│   │   ├── storages/
│   │   │   ├── ftp.dart
│   │   │   ├── local.dart
│   │   │   ├── storage.dart
│   │   │   └── webdav.dart
│   │   └── store/
│   │       ├── app_state.dart
│   │       ├── history_state.dart
│   │       ├── play_queue_state.dart
│   │       ├── player_ui_state.dart
│   │       └── storage_state.dart
│   ├── oss_licenses.dart
│   ├── pages/
│   │   ├── home/
│   │   │   └── home.dart
│   │   └── player/
│   │       ├── audio.dart
│   │       ├── control_bar/
│   │       │   ├── control_bar.dart
│   │       │   ├── control_bar_slider.dart
│   │       │   ├── volume_control.dart
│   │       │   └── volume_slider.dart
│   │       ├── overlays/
│   │       │   ├── controls_overlay.dart
│   │       │   ├── gesture_overlay.dart
│   │       │   ├── minimal_progress_overlay.dart
│   │       │   └── speed_selector.dart
│   │       ├── player.dart
│   │       ├── player_view.dart
│   │       ├── title_bar.dart
│   │       └── video_view.dart
│   ├── store/
│   │   ├── persistent_store.dart
│   │   ├── use_app_store.dart
│   │   ├── use_history_store.dart
│   │   ├── use_play_queue_store.dart
│   │   ├── use_player_ui_store.dart
│   │   └── use_storage_store.dart
│   ├── theme.dart
│   ├── utils/
│   │   ├── check_content_type.dart
│   │   ├── check_data_source_type.dart
│   │   ├── data_migration.dart
│   │   ├── file_size_convert.dart
│   │   ├── files_sort.dart
│   │   ├── format_duration_to_minutes.dart
│   │   ├── get_latest_release.dart
│   │   ├── get_localizations.dart
│   │   ├── get_shuffle_play_queue.dart
│   │   ├── get_subtitle_map.dart
│   │   ├── logger.dart
│   │   ├── path.dart
│   │   ├── path_conv.dart
│   │   ├── platform.dart
│   │   ├── request_storage_permission.dart
│   │   └── url.dart
│   └── widgets/
│       ├── bottom_sheets/
│       │   └── show_open_link_bottom_sheet.dart
│       ├── card.dart
│       ├── chip.dart
│       ├── dialogs/
│       │   ├── show_folder_dialog.dart
│       │   ├── show_ftp_dialog.dart
│       │   ├── show_language_dialog.dart
│       │   ├── show_open_link_dialog.dart
│       │   ├── show_orientation_dialog.dart
│       │   ├── show_rate_dialog.dart
│       │   ├── show_release_dialog.dart
│       │   ├── show_theme_mode_dialog.dart
│       │   └── show_webdav_dialog.dart
│       ├── drag_area.dart
│       ├── popup.dart
│       └── popups/
│           ├── history.dart
│           ├── play_queue.dart
│           ├── settings/
│           │   ├── about.dart
│           │   ├── dependencies.dart
│           │   ├── general.dart
│           │   ├── play.dart
│           │   └── settings.dart
│           ├── storages/
│           │   ├── favorites.dart
│           │   ├── files.dart
│           │   ├── storages.dart
│           │   └── storages_list.dart
│           └── track/
│               ├── audio_track_list.dart
│               ├── subtitle_and_audio_track.dart
│               └── subtitle_list.dart
├── linux/
│   ├── .gitignore
│   ├── CMakeLists.txt
│   ├── flutter/
│   │   ├── CMakeLists.txt
│   │   ├── generated_plugin_registrant.cc
│   │   ├── generated_plugin_registrant.h
│   │   └── generated_plugins.cmake
│   ├── main.cc
│   ├── my_application.cc
│   └── my_application.h
├── macos/
│   ├── .gitignore
│   ├── Flutter/
│   │   ├── Flutter-Debug.xcconfig
│   │   ├── Flutter-Release.xcconfig
│   │   └── GeneratedPluginRegistrant.swift
│   ├── Runner/
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets/
│   │   │   └── AppIcon.appiconset/
│   │   │       └── Contents.json
│   │   ├── Base.lproj/
│   │   │   └── MainMenu.xib
│   │   ├── Configs/
│   │   │   ├── AppInfo.xcconfig
│   │   │   ├── Debug.xcconfig
│   │   │   ├── Release.xcconfig
│   │   │   └── Warnings.xcconfig
│   │   ├── DebugProfile.entitlements
│   │   ├── Info.plist
│   │   ├── MainFlutterWindow.swift
│   │   └── Release.entitlements
│   ├── Runner.xcodeproj/
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace/
│   │   │   └── xcshareddata/
│   │   │       └── IDEWorkspaceChecks.plist
│   │   └── xcshareddata/
│   │       └── xcschemes/
│   │           └── Runner.xcscheme
│   ├── Runner.xcworkspace/
│   │   ├── contents.xcworkspacedata
│   │   └── xcshareddata/
│   │       └── IDEWorkspaceChecks.plist
│   └── RunnerTests/
│       └── RunnerTests.swift
├── pubspec.yaml
├── test/
│   └── widget_test.dart
└── windows/
    ├── .gitignore
    ├── CMakeLists.txt
    ├── flutter/
    │   ├── CMakeLists.txt
    │   ├── generated_plugin_registrant.cc
    │   ├── generated_plugin_registrant.h
    │   └── generated_plugins.cmake
    ├── inno-languages/
    │   ├── ChineseSimplified.isl
    │   └── English.isl
    ├── iris-updater.bat
    └── runner/
        ├── CMakeLists.txt
        ├── Runner.rc
        ├── flutter_window.cpp
        ├── flutter_window.h
        ├── main.cpp
        ├── resource.h
        ├── runner.exe.manifest
        ├── utils.cpp
        ├── utils.h
        ├── win32_window.cpp
        └── win32_window.h
Download .txt
SYMBOL INDEX (387 symbols across 103 files)

FILE: extract_log.py
  function extract_log (line 3) | def extract_log(version):

FILE: lib/hooks/player/use_fvp_player.dart
  function useFvpPlayer (line 25) | FvpPlayer useFvpPlayer(BuildContext context)
  function init (line 92) | Future<void> init(FileItem? file)
  function play (line 292) | Future<void> play()
  function pause (line 301) | Future<void> pause()
  function seek (line 305) | Future<void> seek(Duration newPosition)
  function backward (line 315) | Future<void> backward(int seconds)
  function forward (line 318) | Future<void> forward(int seconds)
  function stepBackward (line 321) | Future<void> stepBackward()
  function stepForward (line 328) | Future<void> stepForward()
  function saveProgress (line 335) | Future<void> saveProgress()

FILE: lib/hooks/player/use_media_kit_player.dart
  function useMediaKitPlayer (line 24) | MediaKitPlayer useMediaKitPlayer(BuildContext context)
  function init (line 145) | Future<void> init(FileItem file)
  function saveProgress (line 293) | Future<void> saveProgress()
  function play (line 315) | Future<void> play()
  function pause (line 322) | Future<void> pause()
  function seek (line 326) | Future<void> seek(Duration newPosition)
  function backward (line 333) | Future<void> backward(int seconds)
  function forward (line 337) | Future<void> forward(int seconds)
  function stepBackward (line 341) | Future<void> stepBackward()
  function stepForward (line 349) | Future<void> stepForward()

FILE: lib/hooks/ui/use_full_screen.dart
  function useFullScreen (line 7) | void useFullScreen()

FILE: lib/hooks/ui/use_orientation.dart
  function useOrientation (line 9) | void useOrientation()
  function setOrientation (line 17) | setOrientation(ScreenOrientation orientation, double? aspect)

FILE: lib/hooks/ui/use_resize_window.dart
  function _applyResize (line 14) | Future<void> _applyResize(Rect newBounds)
  function useResizeWindow (line 21) | void useResizeWindow()
  function performResize (line 43) | Future<void> performResize()

FILE: lib/hooks/use_app_lifecycle.dart
  function useAppLifecycle (line 7) | void useAppLifecycle()

FILE: lib/hooks/use_brightness.dart
  function useBrightness (line 6) | ValueNotifier<double?> useBrightness(bool isGesture)

FILE: lib/hooks/use_cover.dart
  function useCover (line 10) | FileItem? useCover()

FILE: lib/hooks/use_gesture.dart
  class Gesture (line 16) | class Gesture {
  function useGesture (line 57) | Gesture useGesture({
  function onTapDown (line 82) | void onTapDown(TapDownDetails details)
  function onTap (line 88) | void onTap()
  function onDoubleTapDown (line 96) | void onDoubleTapDown(TapDownDetails details)
  function onLongPressStart (line 129) | void onLongPressStart(LongPressStartDetails details)
  function onLongPressMoveUpdate (line 146) | void onLongPressMoveUpdate(LongPressMoveUpdateDetails details)
  function onLongPressEnd (line 170) | void onLongPressEnd(LongPressEndDetails details)
  function onLongPressCancel (line 178) | void onLongPressCancel()
  function onPanStart (line 186) | void onPanStart(DragStartDetails details)
  function onPanUpdate (line 217) | void onPanUpdate(DragUpdateDetails details)
  function _resetPanState (line 285) | void _resetPanState()
  function onPanEnd (line 300) | void onPanEnd(DragEndDetails details)
  function onPanCancel (line 301) | void onPanCancel()
  function onHover (line 303) | void onHover(PointerHoverEvent event)

FILE: lib/hooks/use_keyboard.dart
  type KeyboardEvent (line 22) | typedef KeyboardEvent = void Function(KeyEvent event);
  function useKeyboard (line 24) | KeyboardEvent useKeyboard({
  function onKeyEvent (line 31) | void onKeyEvent(KeyEvent event)

FILE: lib/hooks/use_volume.dart
  function useVolume (line 6) | ValueNotifier<double?> useVolume(bool isGesture)

FILE: lib/info.dart
  class INFO (line 1) | class INFO {

FILE: lib/l10n/iso_639.dart
  class Info (line 1) | class Info {

FILE: lib/main.dart
  function main (line 26) | void main(List<String> arguments)
  class MyApp (line 84) | class MyApp extends HookWidget {
    method build (line 88) | Widget build(BuildContext context)

FILE: lib/models/file.dart
  type ContentType (line 8) | enum ContentType {
  type FileOptions (line 15) | enum FileOptions {
  class FileItem (line 21) | @freezed
    method getID (line 40) | String getID()
  class Subtitle (line 43) | @freezed
  class PlayQueueItem (line 54) | @freezed

FILE: lib/models/player.dart
  class MediaPlayer (line 7) | class MediaPlayer {
  class MediaKitPlayer (line 45) | class MediaKitPlayer extends MediaPlayer {
  class FvpPlayer (line 79) | class FvpPlayer extends MediaPlayer {

FILE: lib/models/progress.dart
  class Progress (line 8) | @freezed

FILE: lib/models/storages/ftp.dart
  function getFTPFiles (line 10) | Future<List<FileItem>> getFTPFiles(
  function getUri (line 38) | String getUri(String fileName)
  function testFTP (line 80) | Future<bool> testFTP(FTPStorage storage)
  function getFTPAuth (line 105) | String getFTPAuth(FTPStorage storage)

FILE: lib/models/storages/local.dart
  function getLocalStorages (line 23) | Future<List<LocalStorage>> getLocalStorages(
  function getLocalPlayQueue (line 142) | Future<PlayQueueState?> getLocalPlayQueue(String filePath)
  function pickLocalFile (line 178) | Future<void> pickLocalFile()
  function getLocalFiles (line 199) | Future<List<FileItem>> getLocalFiles(
  function pickContentFile (line 281) | Future<void> pickContentFile()
  function getContentFiles (line 301) | Future<List<FileItem>> getContentFiles(String uri)

FILE: lib/models/storages/storage.dart
  type StorageType (line 18) | enum StorageType {
  type StorageOptions (line 28) | enum StorageOptions {
  class _Storage (line 33) | abstract class _Storage {
    method toJson (line 39) | Map<String, dynamic> toJson()
    method getFiles (line 41) | Future<List<FileItem>> getFiles(List<String> path)
    method getAuth (line 43) | String? getAuth()
  class Storage (line 46) | @freezed
    method getFiles (line 84) | Future<List<FileItem>> getFiles(List<String> path)
    method getAuth (line 105) | String? getAuth()
  function openInFolder (line 117) | Future<void> openInFolder(BuildContext context, FileItem file)

FILE: lib/models/storages/webdav.dart
  function testWebDAV (line 10) | Future<bool> testWebDAV(WebDAVStorage storage)
  function getWebDAVFiles (line 40) | Future<List<FileItem>> getWebDAVFiles(
  function getUri (line 74) | String getUri(String fileName)
  function getWebDAVAuth (line 117) | String getWebDAVAuth(WebDAVStorage storage)

FILE: lib/models/store/app_state.dart
  type PlayerBackend (line 7) | enum PlayerBackend {
  type Repeat (line 12) | enum Repeat {
  type SortBy (line 18) | enum SortBy {
  type SortOrder (line 24) | enum SortOrder {
  type ScreenOrientation (line 29) | enum ScreenOrientation {
  class AppState (line 35) | @freezed

FILE: lib/models/store/history_state.dart
  class HistoryState (line 8) | @freezed

FILE: lib/models/store/play_queue_state.dart
  class PlayQueueState (line 8) | @freezed

FILE: lib/models/store/player_ui_state.dart
  class PlayerUiState (line 7) | @freezed

FILE: lib/models/store/storage_state.dart
  class Favorite (line 8) | @freezed
  class StorageState (line 19) | @freezed

FILE: lib/oss_licenses.dart
  class Package (line 283) | class Package {
  class PackageRef (line 328) | class PackageRef {
    method resolve (line 333) | Package resolve()

FILE: lib/pages/home/home.dart
  class Home (line 10) | class Home extends HookWidget {
    method build (line 14) | Widget build(BuildContext context)

FILE: lib/pages/player/audio.dart
  class _CoverImage (line 9) | class _CoverImage extends StatelessWidget {
    method build (line 21) | Widget build(BuildContext context)
  class Audio (line 40) | class Audio extends HookWidget {
    method build (line 49) | Widget build(BuildContext context)
    method _buildNarrowLayout (line 110) | Widget _buildNarrowLayout(
    method _buildWideLayout (line 140) | Widget _buildWideLayout(
    method _buildCoverCard (line 193) | Widget _buildCoverCard({

FILE: lib/pages/player/control_bar/control_bar.dart
  class ControlBar (line 30) | class ControlBar extends HookWidget {
    method build (line 45) | Widget build(BuildContext context)

FILE: lib/pages/player/control_bar/control_bar_slider.dart
  class ControlBarSlider (line 10) | class ControlBarSlider extends HookWidget {
    method build (line 23) | Widget build(BuildContext context)
  class _CustomTrackShape (line 129) | class _CustomTrackShape extends RoundedRectSliderTrackShape {
    method paint (line 133) | void paint(

FILE: lib/pages/player/control_bar/volume_control.dart
  function showVolumePopover (line 10) | Future<void> showVolumePopover(
  class VolumeControl (line 29) | class VolumeControl extends HookWidget {
    method build (line 44) | Widget build(BuildContext context)

FILE: lib/pages/player/control_bar/volume_slider.dart
  class VolumeSlider (line 6) | class VolumeSlider extends HookWidget {
    method build (line 17) | Widget build(BuildContext context)

FILE: lib/pages/player/overlays/controls_overlay.dart
  class ControlsOverlay (line 15) | class ControlsOverlay extends HookWidget {
    method build (line 34) | Widget build(BuildContext context)
    method onHover (line 58) | void onHover(PointerHoverEvent event)

FILE: lib/pages/player/overlays/gesture_overlay.dart
  class GestureOverlay (line 12) | class GestureOverlay extends HookWidget {
    method build (line 25) | Widget build(BuildContext context)
    method showSpeedSelectorCallback (line 44) | void showSpeedSelectorCallback(Offset position)
    method hideSpeedSelectorCallback (line 51) | void hideSpeedSelectorCallback(double finalSpeed)
    method updateSelectedSpeedCallback (line 69) | void updateSelectedSpeedCallback(double speed, double newVisualOffset)

FILE: lib/pages/player/overlays/minimal_progress_overlay.dart
  class MinimalProgressOverlay (line 10) | class MinimalProgressOverlay extends StatelessWidget {
    method build (line 21) | Widget build(BuildContext context)

FILE: lib/pages/player/overlays/speed_selector.dart
  class SpeedSelector (line 5) | class SpeedSelector extends HookWidget {
    method build (line 18) | Widget build(BuildContext context)

FILE: lib/pages/player/player.dart
  class Player (line 29) | class Player extends HookWidget {
    method build (line 33) | Widget build(BuildContext context)
    method startControlHideTimer (line 77) | void startControlHideTimer()
    method startProgressHideTimer (line 89) | void startProgressHideTimer()
    method resetControlHideTimer (line 100) | void resetControlHideTimer()
    method resetBottomProgressTimer (line 105) | void resetBottomProgressTimer()
    method showControl (line 110) | void showControl()
    method hideControl (line 116) | void hideControl()
    method showControlForHover (line 122) | Future<void> showControlForHover(Future<void> callback)
    method showProgress (line 134) | void showProgress()

FILE: lib/pages/player/player_view.dart
  class PlayerView (line 10) | class PlayerView extends HookWidget {
    method build (line 16) | Widget build(BuildContext context)
  class _MediaKitPlayerHost (line 26) | class _MediaKitPlayerHost extends HookWidget {
    method build (line 30) | Widget build(BuildContext context)
  class _FvpPlayerHost (line 39) | class _FvpPlayerHost extends HookWidget {
    method build (line 43) | Widget build(BuildContext context)

FILE: lib/pages/player/title_bar.dart
  class TitleBar (line 10) | class TitleBar extends HookWidget {
    method build (line 27) | Widget build(BuildContext context)

FILE: lib/pages/player/video_view.dart
  class VideoView (line 8) | class VideoView extends HookWidget {
    method build (line 17) | Widget build(BuildContext context)

FILE: lib/store/persistent_store.dart
  class PersistentStore (line 4) | abstract class PersistentStore<T> extends Store<T> {
    method _init (line 9) | Future<void> _init()
    method load (line 16) | Future<T?> load()
    method save (line 18) | Future<void> save(T state)
    method dispose (line 22) | Future<void> dispose()

FILE: lib/store/use_app_store.dart
  class AppStore (line 9) | class AppStore extends PersistentStore<AppState> {
    method updateAutoPlay (line 12) | Future<void> updateAutoPlay(bool autoPlay)
    method updateShuffle (line 15) | Future<void> updateShuffle(bool shuffle)
    method updateRepeat (line 20) | Future<void> updateRepeat(Repeat repeat)
    method toggleRepeat (line 25) | Future<void> toggleRepeat()
    method updateFit (line 40) | Future<void> updateFit(BoxFit fit)
    method toggleFit (line 45) | Future<void> toggleFit()
    method updateRate (line 65) | Future<void> updateRate(double value)
    method updateVolume (line 71) | Future<void> updateVolume(int volume)
    method updateMute (line 81) | Future<void> updateMute(bool isMuted)
    method toggleMute (line 86) | Future<void> toggleMute()
    method updateThemeMode (line 91) | Future<void> updateThemeMode(ThemeMode themeMode)
    method updateLanguage (line 96) | Future<void> updateLanguage(String language)
    method toggleAutoResize (line 101) | Future<void> toggleAutoResize()
    method toggleAlwaysPlayFromBeginning (line 106) | Future<void> toggleAlwaysPlayFromBeginning()
    method updatePlayerBackend (line 112) | Future<void> updatePlayerBackend(PlayerBackend backend)
    method updateSortBy (line 117) | Future<void> updateSortBy(SortBy sortBy)
    method updateSortOrder (line 122) | Future<void> updateSortOrder(SortOrder sortOrder)
    method updateFolderFirst (line 127) | Future<void> updateFolderFirst(bool folderFirst)
    method updateOrientation (line 132) | Future<void> updateOrientation(ScreenOrientation orientation)
    method load (line 138) | Future<AppState?> load()
    method getAndroidOptions (line 141) | AndroidOptions getAndroidOptions()
    method save (line 160) | Future<void> save(AppState state)
    method getAndroidOptions (line 162) | AndroidOptions getAndroidOptions()
  function useAppStore (line 174) | AppStore useAppStore()

FILE: lib/store/use_history_store.dart
  class HistoryStore (line 9) | class HistoryStore extends PersistentStore<HistoryState> {
    method findById (line 12) | Progress? findById(String id)
    method add (line 14) | Future<void> add(Progress progress)
    method remove (line 24) | Future<void> remove(Progress progress)
    method clear (line 30) | Future<void> clear()
    method load (line 36) | Future<HistoryState?> load()
    method getAndroidOptions (line 39) | AndroidOptions getAndroidOptions()
    method save (line 55) | Future<void> save(HistoryState state)
    method getAndroidOptions (line 57) | AndroidOptions getAndroidOptions()
  function useHistoryStore (line 70) | HistoryStore useHistoryStore()

FILE: lib/store/use_play_queue_store.dart
  class PlayQueueStore (line 18) | class PlayQueueStore extends PersistentStore<PlayQueueState> {
    method update (line 21) | Future<void> update({
    method updateCurrentIndex (line 36) | Future<void> updateCurrentIndex(int index)
    method add (line 47) | Future<void> add(List<FileItem> files)
    method remove (line 70) | Future<void> remove(PlayQueueItem item)
    method previous (line 100) | Future<void> previous()
    method next (line 110) | Future<void> next()
    method shuffle (line 120) | Future<void> shuffle()
    method sort (line 125) | Future<void> sort()
    method load (line 132) | Future<PlayQueueState?> load()
    method getAndroidOptions (line 191) | AndroidOptions getAndroidOptions()
    method save (line 207) | Future<void> save(PlayQueueState state)
    method getAndroidOptions (line 209) | AndroidOptions getAndroidOptions()
  function usePlayQueueStore (line 222) | PlayQueueStore usePlayQueueStore()

FILE: lib/store/use_player_ui_store.dart
  class PlayerUiStore (line 6) | class PlayerUiStore extends Store<PlayerUiState> {
    method updateAspectRatio (line 9) | void updateAspectRatio(double ratio)
    method toggleIsAlwaysOnTop (line 13) | Future<void> toggleIsAlwaysOnTop()
    method updateFullScreen (line 20) | Future<void> updateFullScreen(bool bool)
    method updateIsSeeking (line 27) | void updateIsSeeking(bool bool)
    method updateIsHovering (line 31) | void updateIsHovering(bool bool)
    method updateIsShowControl (line 35) | void updateIsShowControl(bool bool)
    method updateIsShowProgress (line 39) | void updateIsShowProgress(bool bool)
  function usePlayerUiStore (line 44) | PlayerUiStore usePlayerUiStore()

FILE: lib/store/use_storage_store.dart
  class StorageStore (line 10) | class StorageStore extends PersistentStore<StorageState> {
    method findById (line 13) | Storage? findById(String id)
    method addStorage (line 16) | Future<void> addStorage(Storage storage)
    method updateStorage (line 21) | Future<void> updateStorage(int index, Storage storage)
    method removeStorage (line 33) | Future<void> removeStorage(Storage storage)
    method addFavorite (line 38) | Future<void> addFavorite(Favorite favorite)
    method removeFavorite (line 43) | Future<void> removeFavorite(Favorite favorite)
    method updateCurrentStorage (line 48) | Future<void> updateCurrentStorage(Storage? storage)
    method updateCurrentPath (line 53) | Future<void> updateCurrentPath(List<String> path)
    method load (line 59) | Future<StorageState?> load()
    method getAndroidOptions (line 62) | AndroidOptions getAndroidOptions()
    method save (line 78) | Future<void> save(StorageState state)
    method getAndroidOptions (line 80) | AndroidOptions getAndroidOptions()
  function useStorageStore (line 93) | StorageStore useStorageStore()

FILE: lib/theme.dart
  function baseTheme (line 5) | ThemeData baseTheme(BuildContext context)
  class CustomTheme (line 28) | class CustomTheme {
  function getTheme (line 35) | CustomTheme getTheme({

FILE: lib/utils/check_content_type.dart
  class Formats (line 3) | class Formats {
  function checkContentType (line 91) | ContentType checkContentType(String name)
  function isMediaFile (line 107) | bool isMediaFile(String name)
  function isVideoFile (line 111) | bool isVideoFile(String name)
  function isAudioFile (line 112) | bool isAudioFile(String name)
  function isImageFile (line 113) | bool isImageFile(String name)

FILE: lib/utils/check_data_source_type.dart
  function checkDataSourceType (line 6) | DataSourceType checkDataSourceType(FileItem file)

FILE: lib/utils/data_migration.dart
  function dataMigration (line 6) | Future<bool> dataMigration()

FILE: lib/utils/file_size_convert.dart
  function fileSizeConvert (line 1) | String fileSizeConvert(int fileSize)

FILE: lib/utils/files_sort.dart
  function filesSort (line 4) | List<FileItem> filesSort({

FILE: lib/utils/format_duration_to_minutes.dart
  function formatDurationToMinutes (line 1) | String formatDurationToMinutes(Duration duration)
  function twoDigits (line 3) | String twoDigits(int n)

FILE: lib/utils/get_latest_release.dart
  class Release (line 7) | class Release {
  function getLatestRelease (line 23) | Future<Release?> getLatestRelease()
  function isVersionUpdated (line 87) | bool isVersionUpdated(String currentVersion, String latestVersion)

FILE: lib/utils/get_localizations.dart
  function getLocalizations (line 4) | AppLocalizations getLocalizations(BuildContext context)

FILE: lib/utils/get_shuffle_play_queue.dart
  function getShufflePlayQueue (line 4) | List<PlayQueueItem> getShufflePlayQueue(

FILE: lib/utils/get_subtitle_map.dart
  function getSubtitleMap (line 4) | Map<String, List<Subtitle>> getSubtitleMap<T>({

FILE: lib/utils/logger.dart
  function logger (line 4) | void logger(String message)

FILE: lib/utils/path.dart
  function getExecutableDirPath (line 5) | Future<String> getExecutableDirPath()
  function getTempPath (line 10) | Future<String> getTempPath()

FILE: lib/utils/path_conv.dart
  function pathConv (line 4) | List<String> pathConv(String path)

FILE: lib/utils/request_storage_permission.dart
  function requestStoragePermission (line 6) | Future<void> requestStoragePermission()
  function isAndroid11OrHigher (line 26) | Future<bool> isAndroid11OrHigher()

FILE: lib/utils/url.dart
  function launchURL (line 3) | Future<void> launchURL(String url)

FILE: lib/widgets/bottom_sheets/show_open_link_bottom_sheet.dart
  function showOpenLinkBottomSheet (line 8) | Future<void> showOpenLinkBottomSheet(BuildContext context)
  class OpenLinkBottomSheet (line 21) | class OpenLinkBottomSheet extends HookWidget {
    method build (line 25) | Widget build(BuildContext context)
    method play (line 29) | void play()

FILE: lib/widgets/card.dart
  class Card (line 4) | class Card extends StatelessWidget {
    method build (line 21) | Widget build(BuildContext context)

FILE: lib/widgets/chip.dart
  class Chip (line 3) | class Chip extends StatelessWidget {
    method build (line 10) | Widget build(BuildContext context)

FILE: lib/widgets/dialogs/show_folder_dialog.dart
  function showFolderDialog (line 10) | Future<void> showFolderDialog(BuildContext context,
  class LocalDialog (line 16) | class LocalDialog extends HookWidget {
    method build (line 24) | Widget build(BuildContext context)
    method add (line 34) | void add()
    method update (line 44) | void update()

FILE: lib/widgets/dialogs/show_ftp_dialog.dart
  function showFTPDialog (line 10) | Future<void> showFTPDialog(BuildContext context, {FTPStorage? storage})
  class FTPDialog (line 18) | class FTPDialog extends HookWidget {
    method build (line 26) | Widget build(BuildContext context)
    method add (line 43) | void add()
    method update (line 57) | void update()
    method testConnection (line 72) | void testConnection()

FILE: lib/widgets/dialogs/show_language_dialog.dart
  function showLanguageDialog (line 8) | Future<void> showLanguageDialog(BuildContext context)
  class LanguageDialog (line 14) | class LanguageDialog extends HookWidget {
    method build (line 18) | Widget build(BuildContext context)
    method updateLanguage (line 23) | void updateLanguage(String? newLanguage)

FILE: lib/widgets/dialogs/show_open_link_dialog.dart
  function showOpenLinkDialog (line 8) | Future<void> showOpenLinkDialog(BuildContext context)
  class OpenLinkDialog (line 14) | class OpenLinkDialog extends HookWidget {
    method build (line 18) | Widget build(BuildContext context)
    method play (line 22) | void play()

FILE: lib/widgets/dialogs/show_orientation_dialog.dart
  function showOrientationDialog (line 8) | Future<void> showOrientationDialog(BuildContext context)
  class OrientationDialog (line 14) | class OrientationDialog extends HookWidget {
    method build (line 18) | Widget build(BuildContext context)
    method updateOrientation (line 23) | void updateOrientation(ScreenOrientation? newOrientation)

FILE: lib/widgets/dialogs/show_rate_dialog.dart
  function showRateDialog (line 8) | Future<void> showRateDialog(BuildContext context)
  class RateDialog (line 14) | class RateDialog extends HookWidget {
    method build (line 18) | Widget build(BuildContext context)
    method updateRate (line 22) | void updateRate(double? newRate)

FILE: lib/widgets/dialogs/show_release_dialog.dart
  function isPortable (line 13) | bool isPortable()
  function showReleaseDialog (line 20) | Future<void> showReleaseDialog(BuildContext context,
  class ReleaseDialog (line 29) | class ReleaseDialog extends HookWidget {
    method build (line 35) | Widget build(BuildContext context)
    method update (line 47) | void update()
    method cancel (line 64) | Future<void> cancel()

FILE: lib/widgets/dialogs/show_theme_mode_dialog.dart
  function showThemeModeDialog (line 7) | Future<void> showThemeModeDialog(BuildContext context)
  class ThemeModeDialog (line 13) | class ThemeModeDialog extends HookWidget {
    method build (line 17) | Widget build(BuildContext context)
    method updateThemeMode (line 21) | void updateThemeMode(ThemeMode? newThemeMode)

FILE: lib/widgets/dialogs/show_webdav_dialog.dart
  function showWebDAVDialog (line 10) | Future<void> showWebDAVDialog(BuildContext context,
  class WebDAVDialog (line 19) | class WebDAVDialog extends HookWidget {
    method build (line 27) | Widget build(BuildContext context)
    method add (line 45) | void add()
    method update (line 60) | void update()
    method testConnection (line 76) | void testConnection()

FILE: lib/widgets/drag_area.dart
  class DragArea (line 7) | class DragArea extends StatelessWidget {
    method build (line 15) | Widget build(BuildContext context)

FILE: lib/widgets/popup.dart
  type PopupDirection (line 8) | enum PopupDirection { left, right }
  function showPopup (line 10) | Future<void> showPopup({
  function replacePopup (line 17) | Future<void> replacePopup({
  class Popup (line 25) | class Popup<T> extends PopupRoute<T> {
    method _popOnce (line 39) | void _popOnce(BuildContext context)
    method dispose (line 58) | void dispose()
    method buildPage (line 64) | Widget buildPage(BuildContext context, Animation<double> animation,

FILE: lib/widgets/popups/history.dart
  class History (line 17) | class History extends HookWidget {
    method build (line 21) | Widget build(BuildContext context)
    method play (line 32) | Future<void> play(int index)

FILE: lib/widgets/popups/play_queue.dart
  class PlayQueue (line 15) | class PlayQueue extends HookWidget {
    method build (line 19) | Widget build(BuildContext context)

FILE: lib/widgets/popups/settings/about.dart
  function isMsix (line 13) | bool isMsix()
  class About (line 23) | class About extends HookWidget {
    method build (line 29) | Widget build(BuildContext context)
    method getPackageInfo (line 35) | void getPackageInfo()

FILE: lib/widgets/popups/settings/dependencies.dart
  class Dependencies (line 6) | class Dependencies extends HookWidget {
    method build (line 14) | Widget build(BuildContext context)

FILE: lib/widgets/popups/settings/general.dart
  class General (line 10) | class General extends HookWidget {
    method build (line 14) | Widget build(BuildContext context)

FILE: lib/widgets/popups/settings/play.dart
  class Play (line 12) | class Play extends HookWidget {
    method build (line 16) | Widget build(BuildContext context)

FILE: lib/widgets/popups/settings/settings.dart
  class ITab (line 9) | class ITab {
  class Settings (line 19) | class Settings extends HookWidget {
    method build (line 25) | Widget build(BuildContext context)

FILE: lib/widgets/popups/storages/favorites.dart
  class Favorites (line 11) | class Favorites extends HookWidget {
    method build (line 15) | Widget build(BuildContext context)

FILE: lib/widgets/popups/storages/files.dart
  class Files (line 25) | class Files extends HookWidget {
    method build (line 31) | Widget build(BuildContext context)
    method refresh (line 35) | void refresh()
    method play (line 94) | void play(List<FileItem> files, int index)
    method back (line 113) | void back()

FILE: lib/widgets/popups/storages/storages.dart
  class ITab (line 18) | class ITab {
  class Storages (line 28) | class Storages extends HookWidget {
    method build (line 32) | Widget build(BuildContext context)

FILE: lib/widgets/popups/storages/storages_list.dart
  class StoragesList (line 13) | class StoragesList extends HookWidget {
    method build (line 17) | Widget build(BuildContext context)

FILE: lib/widgets/popups/track/audio_track_list.dart
  class AudioTrackList (line 10) | class AudioTrackList extends HookWidget {
    method build (line 14) | Widget build(BuildContext context)

FILE: lib/widgets/popups/track/subtitle_and_audio_track.dart
  class ITab (line 7) | class ITab {
  class SubtitleAndAudioTrack (line 17) | class SubtitleAndAudioTrack extends HookWidget {
    method build (line 21) | Widget build(BuildContext context)

FILE: lib/widgets/popups/track/subtitle_list.dart
  class SubtitleList (line 15) | class SubtitleList extends HookWidget {
    method build (line 19) | Widget build(BuildContext context)

FILE: linux/flutter/generated_plugin_registrant.cc
  function fl_register_plugins (line 23) | void fl_register_plugins(FlPluginRegistry* registry) {

FILE: linux/main.cc
  function main (line 3) | int main(int argc, char** argv) {

FILE: linux/my_application.cc
  type _MyApplication (line 10) | struct _MyApplication {
  function my_application_activate (line 18) | static void my_application_activate(GApplication* application) {
  function gboolean (line 66) | static gboolean my_application_local_command_line(GApplication* applicat...
  function my_application_startup (line 85) | static void my_application_startup(GApplication* application) {
  function my_application_shutdown (line 94) | static void my_application_shutdown(GApplication* application) {
  function my_application_dispose (line 103) | static void my_application_dispose(GObject* object) {
  function my_application_class_init (line 109) | static void my_application_class_init(MyApplicationClass* klass) {
  function my_application_init (line 117) | static void my_application_init(MyApplication* self) {}
  function MyApplication (line 119) | MyApplication* my_application_new() {

FILE: test/widget_test.dart
  function main (line 13) | void main()

FILE: windows/flutter/generated_plugin_registrant.cc
  function RegisterPlugins (line 26) | void RegisterPlugins(flutter::PluginRegistry* registry) {

FILE: windows/runner/flutter_window.cpp
  function LRESULT (line 50) | LRESULT

FILE: windows/runner/flutter_window.h
  function class (line 12) | class FlutterWindow : public Win32Window {

FILE: windows/runner/main.cpp
  function wWinMain (line 8) | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,

FILE: windows/runner/utils.cpp
  function CreateAndAttachConsole (line 10) | void CreateAndAttachConsole() {
  function GetCommandLineArguments (line 24) | std::vector<std::string> GetCommandLineArguments() {
  function Utf8FromUtf16 (line 44) | std::string Utf8FromUtf16(const wchar_t* utf16_string) {

FILE: windows/runner/win32_window.cpp
  function Scale (line 36) | int Scale(int source, double scale_factor) {
  function EnableFullDpiSupportIfAvailable (line 42) | void EnableFullDpiSupportIfAvailable(HWND hwnd) {
  class WindowClassRegistrar (line 59) | class WindowClassRegistrar {
    method WindowClassRegistrar (line 64) | static WindowClassRegistrar* GetInstance() {
    method WindowClassRegistrar (line 80) | WindowClassRegistrar() = default;
  function wchar_t (line 89) | const wchar_t* WindowClassRegistrar::GetWindowClass() {
  function LRESULT (line 157) | LRESULT CALLBACK Win32Window::WndProc(HWND const window,
  function LRESULT (line 176) | LRESULT
  function Win32Window (line 236) | Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
  function RECT (line 252) | RECT Win32Window::GetClientArea() {
  function HWND (line 258) | HWND Win32Window::GetHandle() {

FILE: windows/runner/win32_window.h
  type Size (line 21) | struct Size {
Condensed preview — 202 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,365K chars).
[
  {
    "path": ".github/workflows/ci.yml",
    "chars": 5998,
    "preview": "name: ci\non:\n  push:\n    branches: \n      - main\n      - dev\n    paths-ignore:\n      - README.md\n      - README_CN.md\n  "
  },
  {
    "path": ".gitignore",
    "chars": 809,
    "preview": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\nmigrate_working_dir/\n\n# IntelliJ re"
  },
  {
    "path": ".metadata",
    "chars": 1706,
    "preview": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrade"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 512,
    "preview": "{\n  // 使用 IntelliSense 了解相关属性。 \n  // 悬停以查看现有属性的描述。\n  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387\n  \"v"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 76,
    "preview": "{\n  \"arb-editor.suppressedWarnings\": [\n    \"missing_metadata_for_key\"\n  ],\n}"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 4738,
    "preview": "## v1.5.2\n\n### Changelog\n\n* Migrate to MPL-2.0 license\n* Add more media type support\n* Fix key open subtitle and audio t"
  },
  {
    "path": "LICENSE",
    "chars": 16710,
    "preview": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n\n--------------\n\n1.1. \"Contributor"
  },
  {
    "path": "PRIVACY.md",
    "chars": 3264,
    "preview": "# IRIS Privacy Policy\n\n**Last Updated:** June 7, 2025\n\nThank you for using IRIS (\"the Application\"). We are committed to"
  },
  {
    "path": "README.md",
    "chars": 6040,
    "preview": "<img height=\"100px\" width=\"100px\" alt=\"logo\" src=\"./assets/images/logo.png\"/>\n\n# IRIS - A lightweight video player\n\n[![B"
  },
  {
    "path": "README_CN.md",
    "chars": 4518,
    "preview": "<img height=\"100px\" width=\"100px\" alt=\"logo\" src=\"./assets/images/logo.png\"/>\n\n# IRIS - 轻量级视频播放器\n\n[![Build Status](https"
  },
  {
    "path": "analysis_options.yaml",
    "chars": 1508,
    "preview": "# This file configures the analyzer, which statically analyzes Dart code to\n# check for errors, warnings, and lints.\n#\n#"
  },
  {
    "path": "android/.gitignore",
    "chars": 304,
    "preview": "gradle-wrapper.jar\n/.gradle\n/captures/\n/gradlew\n/gradlew.bat\n/local.properties\nGeneratedPluginRegistrant.java\n\n.cxx\n\n# R"
  },
  {
    "path": "android/app/build.gradle",
    "chars": 3363,
    "preview": "import java.nio.file.Files\nimport java.security.MessageDigest\n\nplugins {\n    id \"com.android.application\"\n    id \"kotlin"
  },
  {
    "path": "android/app/proguard-rules.pro",
    "chars": 321,
    "preview": "-dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue\n-dontwarn com.google.errorprone.annotations.CheckReturn"
  },
  {
    "path": "android/app/src/debug/AndroidManifest.xml",
    "chars": 378,
    "preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- The INTERNET permission is required for d"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "chars": 3096,
    "preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <uses-permission android:name=\"android.permiss"
  },
  {
    "path": "android/app/src/main/kotlin/nini22p/iris/MainActivity.kt",
    "chars": 113,
    "preview": "package nini22p.iris\n\nimport io.flutter.embedding.android.FlutterActivity\n\nclass MainActivity: FlutterActivity()\n"
  },
  {
    "path": "android/app/src/main/res/drawable/launch_background.xml",
    "chars": 434,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmln"
  },
  {
    "path": "android/app/src/main/res/drawable-v21/launch_background.xml",
    "chars": 438,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmln"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "chars": 265,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <b"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "chars": 265,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <b"
  },
  {
    "path": "android/app/src/main/res/values/ic_launcher_background.xml",
    "chars": 120,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#2F2F2F</color>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values/styles.xml",
    "chars": 1075,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is sta"
  },
  {
    "path": "android/app/src/main/res/values-night/styles.xml",
    "chars": 995,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is sta"
  },
  {
    "path": "android/app/src/profile/AndroidManifest.xml",
    "chars": 378,
    "preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- The INTERNET permission is required for d"
  },
  {
    "path": "android/build.gradle",
    "chars": 322,
    "preview": "allprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\nrootProject.buildDir = \"../build\"\nsubp"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "chars": 201,
    "preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dist"
  },
  {
    "path": "android/gradle.properties",
    "chars": 135,
    "preview": "org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError\nandroid.useAndroidX=true\nandroid.enabl"
  },
  {
    "path": "android/settings.gradle",
    "chars": 728,
    "preview": "pluginManagement {\n    def flutterSdkPath = {\n        def properties = new Properties()\n        file(\"local.properties\")"
  },
  {
    "path": "devtools_options.yaml",
    "chars": 184,
    "preview": "description: This file stores settings for Dart & Flutter DevTools.\ndocumentation: https://docs.flutter.dev/tools/devtoo"
  },
  {
    "path": "extract_log.py",
    "chars": 1041,
    "preview": "import sys\n\ndef extract_log(version):\n    try:\n        with open(\"CHANGELOG.md\", \"r\", encoding=\"utf-8\") as file:\n       "
  },
  {
    "path": "inno.iss",
    "chars": 21554,
    "preview": "; Script generated by the Inno Setup Script Wizard.\n; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FI"
  },
  {
    "path": "ios/.gitignore",
    "chars": 569,
    "preview": "**/dgph\n*.mode1v3\n*.mode2v3\n*.moved-aside\n*.pbxuser\n*.perspectivev3\n**/*sync/\n.sconsign.dblite\n.tags*\n**/.vagrant/\n**/De"
  },
  {
    "path": "ios/Flutter/AppFrameworkInfo.plist",
    "chars": 774,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "ios/Flutter/Debug.xcconfig",
    "chars": 30,
    "preview": "#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Flutter/Release.xcconfig",
    "chars": 30,
    "preview": "#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Runner/AppDelegate.swift",
    "chars": 391,
    "preview": "import Flutter\nimport UIKit\n\n@main\n@objc class AppDelegate: FlutterAppDelegate {\n  override func application(\n    _ appl"
  },
  {
    "path": "ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "chars": 2519,
    "preview": "{\n  \"images\" : [\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n   "
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json",
    "chars": 391,
    "preview": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage.png\",\n      \"scale\" : \"1x\"\n    },\n  "
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md",
    "chars": 336,
    "preview": "# Launch Screen Assets\n\nYou can customize the launch screen with your own desired assets by replacing the image files in"
  },
  {
    "path": "ios/Runner/Base.lproj/LaunchScreen.storyboard",
    "chars": 2377,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard"
  },
  {
    "path": "ios/Runner/Base.lproj/Main.storyboard",
    "chars": 1605,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard"
  },
  {
    "path": "ios/Runner/Info.plist",
    "chars": 1774,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "ios/Runner/Runner-Bridging-Header.h",
    "chars": 38,
    "preview": "#import \"GeneratedPluginRegistrant.h\"\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.pbxproj",
    "chars": 23606,
    "preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "chars": 135,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "chars": 238,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "chars": 226,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "chars": 3647,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      "
  },
  {
    "path": "ios/Runner.xcworkspace/contents.xcworkspacedata",
    "chars": 152,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodepr"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "chars": 238,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "chars": 226,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "ios/RunnerTests/RunnerTests.swift",
    "chars": 285,
    "preview": "import Flutter\nimport UIKit\nimport XCTest\n\nclass RunnerTests: XCTestCase {\n\n  func testExample() {\n    // If you add cod"
  },
  {
    "path": "l10n.yaml",
    "chars": 121,
    "preview": "arb-dir: lib/l10n\ntemplate-arb-file: app_en.arb\noutput-localization-file: app_localizations.dart\nsynthetic-package: fals"
  },
  {
    "path": "lib/globals.dart",
    "chars": 560,
    "preview": "// ignore: unnecessary_library_name\nlibrary my_app.globals;\n\nimport 'package:flutter/material.dart';\nimport 'package:per"
  },
  {
    "path": "lib/hooks/player/use_fvp_player.dart",
    "chars": 13069,
    "preview": "import 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'pac"
  },
  {
    "path": "lib/hooks/player/use_media_kit_player.dart",
    "chars": 13065,
    "preview": "import 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutte"
  },
  {
    "path": "lib/hooks/ui/use_full_screen.dart",
    "chars": 575,
    "preview": "import 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_zustand/flutter_zustand.dart';\nimport 'packag"
  },
  {
    "path": "lib/hooks/ui/use_orientation.dart",
    "chars": 1497,
    "preview": "import 'dart:io';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'pac"
  },
  {
    "path": "lib/hooks/ui/use_resize_window.dart",
    "chars": 4212,
    "preview": "import 'dart:math' as math;\nimport 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\ni"
  },
  {
    "path": "lib/hooks/use_app_lifecycle.dart",
    "chars": 623,
    "preview": "import 'dart:ui';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:iris/models/player.dart';\nimport 'p"
  },
  {
    "path": "lib/hooks/use_brightness.dart",
    "chars": 1123,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:iris/utils/lo"
  },
  {
    "path": "lib/hooks/use_cover.dart",
    "chars": 2228,
    "preview": "import 'package:collection/collection.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_"
  },
  {
    "path": "lib/hooks/use_gesture.dart",
    "chars": 10581,
    "preview": "import 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_ho"
  },
  {
    "path": "lib/hooks/use_keyboard.dart",
    "chars": 8052,
    "preview": "import 'dart:io';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'pac"
  },
  {
    "path": "lib/hooks/use_volume.dart",
    "chars": 851,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_volum"
  },
  {
    "path": "lib/info.dart",
    "chars": 273,
    "preview": "class INFO {\n  static const String title = 'IRIS';\n  static const String author = '22';\n  static const String authorUrl "
  },
  {
    "path": "lib/l10n/app_en.arb",
    "chars": 3613,
    "preview": "{\n  \"@@locale\": \"en\",\n  \"about\": \"About\",\n  \"add\": \"Add\",\n  \"add_favorite\": \"Add favorite\",\n  \"add_folder\": \"Add folder\""
  },
  {
    "path": "lib/l10n/app_zh.arb",
    "chars": 2787,
    "preview": "{\n  \"@@locale\": \"zh\",\n  \"about\": \"关于\",\n  \"add\": \"添加\",\n  \"add_favorite\": \"添加收藏\",\n  \"add_folder\": \"添加文件夹\",\n  \"add_ftp_stor"
  },
  {
    "path": "lib/l10n/iso_639.dart",
    "chars": 25320,
    "preview": "class Info {\n  final List<String> en;\n\n  const Info({required this.en});\n}\n\nconst Map<String, Info> customLanguageCodes "
  },
  {
    "path": "lib/l10n/languages.dart",
    "chars": 78,
    "preview": "const Map<String, String> languages = {\n  'zh': '简体中文',\n  'en': 'English',\n};\n"
  },
  {
    "path": "lib/main.dart",
    "chars": 5002,
    "preview": "import 'dart:io';\nimport 'package:app_links/app_links.dart';\nimport 'package:flutter/services.dart';\nimport 'package:fvp"
  },
  {
    "path": "lib/models/file.dart",
    "chars": 1448,
    "preview": "import 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:i"
  },
  {
    "path": "lib/models/player.dart",
    "chars": 2980,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:iris/models/file.dart';\nimport 'package:media_kit/media_kit.dart"
  },
  {
    "path": "lib/models/progress.dart",
    "chars": 512,
    "preview": "import 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:i"
  },
  {
    "path": "lib/models/storages/ftp.dart",
    "chars": 3122,
    "preview": "import 'dart:convert';\nimport 'package:iris/models/file.dart';\nimport 'package:iris/models/storages/storage.dart';\nimpor"
  },
  {
    "path": "lib/models/storages/local.dart",
    "chars": 9818,
    "preview": "import 'dart:io';\nimport 'package:android_x_storage/android_x_storage.dart';\nimport 'package:disks_desktop/disks_desktop"
  },
  {
    "path": "lib/models/storages/storage.dart",
    "chars": 3877,
    "preview": "import 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\nimport 'package:freezed_annotation/"
  },
  {
    "path": "lib/models/storages/webdav.dart",
    "chars": 3355,
    "preview": "import 'dart:convert';\nimport 'package:iris/models/storages/storage.dart';\nimport 'package:iris/utils/check_content_type"
  },
  {
    "path": "lib/models/store/app_state.dart",
    "chars": 1358,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'app_state.fr"
  },
  {
    "path": "lib/models/store/history_state.dart",
    "chars": 474,
    "preview": "import 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:i"
  },
  {
    "path": "lib/models/store/play_queue_state.dart",
    "chars": 522,
    "preview": "import 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:i"
  },
  {
    "path": "lib/models/store/player_ui_state.dart",
    "chars": 660,
    "preview": "import 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:flutter/foundation.dart';\n\npart 'player_ui_"
  },
  {
    "path": "lib/models/store/storage_state.dart",
    "chars": 846,
    "preview": "import 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:i"
  },
  {
    "path": "lib/oss_licenses.dart",
    "chars": 667772,
    "preview": "// dart format off\n// cSpell:disable\n// ignore_for_file: always_put_required_named_parameters_first\n// ignore_for_file: "
  },
  {
    "path": "lib/pages/home/home.dart",
    "chars": 810,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_zusta"
  },
  {
    "path": "lib/pages/player/audio.dart",
    "chars": 5743,
    "preview": "import 'dart:io';\nimport 'dart:ui';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks."
  },
  {
    "path": "lib/pages/player/control_bar/control_bar.dart",
    "chars": 19563,
    "preview": "import 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutte"
  },
  {
    "path": "lib/pages/player/control_bar/control_bar_slider.dart",
    "chars": 6174,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_zusta"
  },
  {
    "path": "lib/pages/player/control_bar/volume_control.dart",
    "chars": 3190,
    "preview": "import 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_ho"
  },
  {
    "path": "lib/pages/player/control_bar/volume_slider.dart",
    "chars": 1471,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_zusta"
  },
  {
    "path": "lib/pages/player/overlays/controls_overlay.dart",
    "chars": 3755,
    "preview": "import 'dart:async';\nimport 'dart:ui';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_zustand/flutter_z"
  },
  {
    "path": "lib/pages/player/overlays/gesture_overlay.dart",
    "chars": 6972,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_zusta"
  },
  {
    "path": "lib/pages/player/overlays/minimal_progress_overlay.dart",
    "chars": 2238,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_zustand/flutter_zustand.dart';\nimport 'package:iris/mode"
  },
  {
    "path": "lib/pages/player/overlays/speed_selector.dart",
    "chars": 3395,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:iris/globals."
  },
  {
    "path": "lib/pages/player/player.dart",
    "chars": 8735,
    "preview": "import 'dart:async';\nimport 'dart:io';\nimport 'package:desktop_drop/desktop_drop.dart';\nimport 'package:flutter/material"
  },
  {
    "path": "lib/pages/player/player_view.dart",
    "chars": 1378,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:iris/hooks/pl"
  },
  {
    "path": "lib/pages/player/title_bar.dart",
    "chars": 7721,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_zusta"
  },
  {
    "path": "lib/pages/player/video_view.dart",
    "chars": 980,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:iris/models/p"
  },
  {
    "path": "lib/store/persistent_store.dart",
    "chars": 498,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_zustand/flutter_zustand.dart';\n\nabstract class Persisten"
  },
  {
    "path": "lib/store/use_app_store.dart",
    "chars": 4613,
    "preview": "import 'dart:convert';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_secure_storage/flutter_secure_sto"
  },
  {
    "path": "lib/store/use_history_store.dart",
    "chars": 2042,
    "preview": "import 'dart:convert';\nimport 'package:flutter_secure_storage/flutter_secure_storage.dart';\nimport 'package:flutter_zust"
  },
  {
    "path": "lib/store/use_play_queue_store.dart",
    "chars": 6835,
    "preview": "import 'dart:convert';\nimport 'dart:io';\nimport 'dart:math';\nimport 'package:flutter_secure_storage/flutter_secure_stora"
  },
  {
    "path": "lib/store/use_player_ui_store.dart",
    "chars": 1223,
    "preview": "import 'package:flutter_zustand/flutter_zustand.dart';\nimport 'package:iris/models/store/player_ui_state.dart';\nimport '"
  },
  {
    "path": "lib/store/use_storage_store.dart",
    "chars": 2857,
    "preview": "import 'dart:convert';\nimport 'package:collection/collection.dart';\nimport 'package:flutter_secure_storage/flutter_secur"
  },
  {
    "path": "lib/theme.dart",
    "chars": 2054,
    "preview": "import 'package:dynamic_color/dynamic_color.dart';\nimport 'package:flutter/material.dart';\nimport 'package:google_fonts/"
  },
  {
    "path": "lib/utils/check_content_type.dart",
    "chars": 1797,
    "preview": "import 'package:iris/models/file.dart';\n\nclass Formats {\n  static const List<String> audio = [\n    'aac',\n    'aiff',\n  "
  },
  {
    "path": "lib/utils/check_data_source_type.dart",
    "chars": 625,
    "preview": "import 'dart:io';\nimport 'package:iris/models/file.dart';\nimport 'package:iris/models/storages/storage.dart';\nimport 'pa"
  },
  {
    "path": "lib/utils/data_migration.dart",
    "chars": 1521,
    "preview": "import 'dart:io';\nimport 'package:iris/utils/logger.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'pa"
  },
  {
    "path": "lib/utils/file_size_convert.dart",
    "chars": 89,
    "preview": "String fileSizeConvert(int fileSize) =>\n    (fileSize / 1024 / 1024).toStringAsFixed(2);\n"
  },
  {
    "path": "lib/utils/files_sort.dart",
    "chars": 909,
    "preview": "import 'package:iris/models/file.dart';\nimport 'package:iris/models/store/app_state.dart';\n\nList<FileItem> filesSort({\n "
  },
  {
    "path": "lib/utils/format_duration_to_minutes.dart",
    "chars": 362,
    "preview": "String formatDurationToMinutes(Duration duration) {\n  int totalMinutes = duration.inHours * 60 + duration.inMinutes.rema"
  },
  {
    "path": "lib/utils/get_latest_release.dart",
    "chars": 2764,
    "preview": "import 'dart:convert';\nimport 'dart:io';\nimport 'package:http/http.dart' as http;\nimport 'package:iris/utils/logger.dart"
  },
  {
    "path": "lib/utils/get_localizations.dart",
    "chars": 192,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:iris/l10n/app_localizations.dart';\n\nAppLocalizations getLocaliza"
  },
  {
    "path": "lib/utils/get_shuffle_play_queue.dart",
    "chars": 848,
    "preview": "import 'dart:math';\nimport 'package:iris/models/file.dart';\n\nList<PlayQueueItem> getShufflePlayQueue(\n    List<PlayQueue"
  },
  {
    "path": "lib/utils/get_subtitle_map.dart",
    "chars": 1143,
    "preview": "import 'package:iris/models/file.dart';\nimport 'package:path/path.dart' as path;\n\nMap<String, List<Subtitle>> getSubtitl"
  },
  {
    "path": "lib/utils/logger.dart",
    "chars": 142,
    "preview": "import 'dart:developer';\nimport 'package:flutter/foundation.dart';\n\nvoid logger(String message) {\n  if (kDebugMode) {\n  "
  },
  {
    "path": "lib/utils/path.dart",
    "chars": 531,
    "preview": "import 'dart:io';\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nFuture<Strin"
  },
  {
    "path": "lib/utils/path_conv.dart",
    "chars": 793,
    "preview": "import 'package:iris/utils/logger.dart';\nimport 'package:path/path.dart' as p;\n\nList<String> pathConv(String path) {\n  t"
  },
  {
    "path": "lib/utils/platform.dart",
    "chars": 305,
    "preview": "import 'dart:io';\n\nfinal bool isDesktop =\n    Platform.isWindows || Platform.isLinux || Platform.isMacOS;\nfinal bool isW"
  },
  {
    "path": "lib/utils/request_storage_permission.dart",
    "chars": 953,
    "preview": "import 'dart:io';\nimport 'package:device_info_plus/device_info_plus.dart';\nimport 'package:iris/globals.dart' as globals"
  },
  {
    "path": "lib/utils/url.dart",
    "chars": 187,
    "preview": "import 'package:url_launcher/url_launcher.dart';\n\nFuture<void> launchURL(String url) async {\n  if (!await launchUrl(Uri."
  },
  {
    "path": "lib/widgets/bottom_sheets/show_open_link_bottom_sheet.dart",
    "chars": 3235,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:iris/models/f"
  },
  {
    "path": "lib/widgets/card.dart",
    "chars": 1733,
    "preview": "import 'dart:ui';\nimport 'package:flutter/material.dart';\n\nclass Card extends StatelessWidget {\n  const Card({\n    super"
  },
  {
    "path": "lib/widgets/chip.dart",
    "chars": 822,
    "preview": "import 'package:flutter/material.dart';\n\nclass Chip extends StatelessWidget {\n  final String text;\n  final bool primary;"
  },
  {
    "path": "lib/widgets/dialogs/show_folder_dialog.dart",
    "chars": 3300,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:iris/models/s"
  },
  {
    "path": "lib/widgets/dialogs/show_ftp_dialog.dart",
    "chars": 6560,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_hooks/flutter_ho"
  },
  {
    "path": "lib/widgets/dialogs/show_language_dialog.dart",
    "chars": 1962,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_zusta"
  },
  {
    "path": "lib/widgets/dialogs/show_open_link_dialog.dart",
    "chars": 2732,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:iris/models/f"
  },
  {
    "path": "lib/widgets/dialogs/show_orientation_dialog.dart",
    "chars": 1947,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_zusta"
  },
  {
    "path": "lib/widgets/dialogs/show_rate_dialog.dart",
    "chars": 1610,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_zusta"
  },
  {
    "path": "lib/widgets/dialogs/show_release_dialog.dart",
    "chars": 3084,
    "preview": "import 'dart:async';\nimport 'dart:io';\nimport 'package:iris/utils/file_size_convert.dart';\nimport 'package:iris/utils/pl"
  },
  {
    "path": "lib/widgets/dialogs/show_theme_mode_dialog.dart",
    "chars": 1957,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_zusta"
  },
  {
    "path": "lib/widgets/dialogs/show_webdav_dialog.dart",
    "chars": 9181,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_hooks/flutter_ho"
  },
  {
    "path": "lib/widgets/drag_area.dart",
    "chars": 1067,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_zustand/flutter_zustand.dart';\nimport 'package:iris/stor"
  },
  {
    "path": "lib/widgets/popup.dart",
    "chars": 5097,
    "preview": "import 'dart:io';\nimport 'package:flutter/material.dart' hide Card;\nimport 'package:flutter/services.dart';\nimport 'pack"
  },
  {
    "path": "lib/widgets/popups/history.dart",
    "chars": 7161,
    "preview": "import 'dart:math';\n\nimport 'package:flutter/material.dart' hide Chip;\nimport 'package:flutter_hooks/flutter_hooks.dart'"
  },
  {
    "path": "lib/widgets/popups/play_queue.dart",
    "chars": 8124,
    "preview": "import 'package:flutter/material.dart' hide Chip;\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flu"
  },
  {
    "path": "lib/widgets/popups/settings/about.dart",
    "chars": 3093,
    "preview": "import 'dart:io';\nimport 'package:iris/utils/platform.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:flutt"
  },
  {
    "path": "lib/widgets/popups/settings/dependencies.dart",
    "chars": 2416,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:iris/oss_lice"
  },
  {
    "path": "lib/widgets/popups/settings/general.dart",
    "chars": 1840,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_zusta"
  },
  {
    "path": "lib/widgets/popups/settings/play.dart",
    "chars": 3306,
    "preview": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'pa"
  },
  {
    "path": "lib/widgets/popups/settings/settings.dart",
    "chars": 2560,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:iris/widgets/"
  },
  {
    "path": "lib/widgets/popups/storages/favorites.dart",
    "chars": 3866,
    "preview": "import 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutt"
  },
  {
    "path": "lib/widgets/popups/storages/files.dart",
    "chars": 22387,
    "preview": "import 'dart:io';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart' hide Chip;\nimport "
  },
  {
    "path": "lib/widgets/popups/storages/storages.dart",
    "chars": 6802,
    "preview": "import 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flu"
  },
  {
    "path": "lib/widgets/popups/storages/storages_list.dart",
    "chars": 5296,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_zusta"
  },
  {
    "path": "lib/widgets/popups/track/audio_track_list.dart",
    "chars": 4303,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:fvp/fvp.dart'"
  },
  {
    "path": "lib/widgets/popups/track/subtitle_and_audio_track.dart",
    "chars": 2378,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:iris/widgets/"
  },
  {
    "path": "lib/widgets/popups/track/subtitle_list.dart",
    "chars": 5976,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_hooks/flutter_hooks.dart';\nimport 'package:flutter_zusta"
  },
  {
    "path": "linux/.gitignore",
    "chars": 18,
    "preview": "flutter/ephemeral\n"
  },
  {
    "path": "linux/CMakeLists.txt",
    "chars": 5417,
    "preview": "# Project-level configuration.\ncmake_minimum_required(VERSION 3.10)\nproject(runner LANGUAGES CXX)\n\n# The name of the exe"
  },
  {
    "path": "linux/flutter/CMakeLists.txt",
    "chars": 2815,
    "preview": "# This file controls Flutter-level build steps. It should not be edited.\ncmake_minimum_required(VERSION 3.10)\n\nset(EPHEM"
  },
  {
    "path": "linux/flutter/generated_plugin_registrant.cc",
    "chars": 3723,
    "preview": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#include \"generated_plugin_registrant.h\"\n\n#include <desktop"
  },
  {
    "path": "linux/flutter/generated_plugin_registrant.h",
    "chars": 303,
    "preview": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#ifndef GENERATED_PLUGIN_REGISTRANT_\n#define GENERATED_PLUG"
  },
  {
    "path": "linux/flutter/generated_plugins.cmake",
    "chars": 979,
    "preview": "#\n# Generated file, do not edit.\n#\n\nlist(APPEND FLUTTER_PLUGIN_LIST\n  desktop_drop\n  dynamic_color\n  flutter_secure_stor"
  },
  {
    "path": "linux/main.cc",
    "chars": 180,
    "preview": "#include \"my_application.h\"\n\nint main(int argc, char** argv) {\n  g_autoptr(MyApplication) app = my_application_new();\n  "
  },
  {
    "path": "linux/my_application.cc",
    "chars": 4416,
    "preview": "#include \"my_application.h\"\n\n#include <flutter_linux/flutter_linux.h>\n#ifdef GDK_WINDOWING_X11\n#include <gdk/gdkx.h>\n#en"
  },
  {
    "path": "linux/my_application.h",
    "chars": 388,
    "preview": "#ifndef FLUTTER_MY_APPLICATION_H_\n#define FLUTTER_MY_APPLICATION_H_\n\n#include <gtk/gtk.h>\n\nG_DECLARE_FINAL_TYPE(MyApplic"
  },
  {
    "path": "macos/.gitignore",
    "chars": 89,
    "preview": "# Flutter-related\n**/Flutter/ephemeral/\n**/Pods/\n\n# Xcode-related\n**/dgph\n**/xcuserdata/\n"
  },
  {
    "path": "macos/Flutter/Flutter-Debug.xcconfig",
    "chars": 48,
    "preview": "#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "macos/Flutter/Flutter-Release.xcconfig",
    "chars": 48,
    "preview": "#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "macos/Flutter/GeneratedPluginRegistrant.swift",
    "chars": 2655,
    "preview": "//\n//  Generated file. Do not edit.\n//\n\nimport FlutterMacOS\nimport Foundation\n\nimport app_links\nimport desktop_drop\nimpo"
  },
  {
    "path": "macos/Runner/AppDelegate.swift",
    "chars": 201,
    "preview": "import Cocoa\nimport FlutterMacOS\n\n@main\nclass AppDelegate: FlutterAppDelegate {\n  override func applicationShouldTermina"
  },
  {
    "path": "macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "chars": 1291,
    "preview": "{\n  \"images\" : [\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_16.png\",\n      \"scale"
  },
  {
    "path": "macos/Runner/Base.lproj/MainMenu.xib",
    "chars": 23723,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion"
  },
  {
    "path": "macos/Runner/Configs/AppInfo.xcconfig",
    "chars": 586,
    "preview": "// Application-level settings for the Runner target.\n//\n// This may be replaced with something auto-generated from metad"
  },
  {
    "path": "macos/Runner/Configs/Debug.xcconfig",
    "chars": 77,
    "preview": "#include \"../../Flutter/Flutter-Debug.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Release.xcconfig",
    "chars": 79,
    "preview": "#include \"../../Flutter/Flutter-Release.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Warnings.xcconfig",
    "chars": 580,
    "preview": "WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverl"
  },
  {
    "path": "macos/Runner/DebugProfile.entitlements",
    "chars": 348,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "macos/Runner/Info.plist",
    "chars": 1060,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "macos/Runner/MainFlutterWindow.swift",
    "chars": 388,
    "preview": "import Cocoa\nimport FlutterMacOS\n\nclass MainFlutterWindow: NSWindow {\n  override func awakeFromNib() {\n    let flutterVi"
  },
  {
    "path": "macos/Runner/Release.entitlements",
    "chars": 240,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "macos/Runner.xcodeproj/project.pbxproj",
    "chars": 26295,
    "preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXAggregateTarget sec"
  },
  {
    "path": "macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "chars": 238,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "chars": 3639,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      "
  },
  {
    "path": "macos/Runner.xcworkspace/contents.xcworkspacedata",
    "chars": 152,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodepr"
  },
  {
    "path": "macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "chars": 238,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "macos/RunnerTests/RunnerTests.swift",
    "chars": 290,
    "preview": "import Cocoa\nimport FlutterMacOS\nimport XCTest\n\nclass RunnerTests: XCTestCase {\n\n  func testExample() {\n    // If you ad"
  },
  {
    "path": "pubspec.yaml",
    "chars": 2389,
    "preview": "name: iris\ndescription: A lightweight video player\npublish_to: none\nversion: 1.5.2+3\n\nenvironment:\n  sdk: ^3.5.4\n\ndepend"
  },
  {
    "path": "test/widget_test.dart",
    "chars": 1063,
    "preview": "// This is a basic Flutter widget test.\n//\n// To perform an interaction with a widget in your test, use the WidgetTester"
  },
  {
    "path": "windows/.gitignore",
    "chars": 291,
    "preview": "flutter/ephemeral/\n\n# Visual Studio user-specific files.\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Visual Studio bu"
  },
  {
    "path": "windows/CMakeLists.txt",
    "chars": 4284,
    "preview": "# Project-level configuration.\ncmake_minimum_required(VERSION 3.14)\nproject(iris LANGUAGES CXX)\n\n# The name of the execu"
  },
  {
    "path": "windows/flutter/CMakeLists.txt",
    "chars": 3742,
    "preview": "# This file controls Flutter-level build steps. It should not be edited.\ncmake_minimum_required(VERSION 3.14)\n\nset(EPHEM"
  },
  {
    "path": "windows/flutter/generated_plugin_registrant.cc",
    "chars": 2993,
    "preview": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#include \"generated_plugin_registrant.h\"\n\n#include <app_lin"
  },
  {
    "path": "windows/flutter/generated_plugin_registrant.h",
    "chars": 302,
    "preview": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#ifndef GENERATED_PLUGIN_REGISTRANT_\n#define GENERATED_PLUG"
  },
  {
    "path": "windows/flutter/generated_plugins.cmake",
    "chars": 1076,
    "preview": "#\n# Generated file, do not edit.\n#\n\nlist(APPEND FLUTTER_PLUGIN_LIST\n  app_links\n  desktop_drop\n  disks_desktop\n  dynamic"
  },
  {
    "path": "windows/inno-languages/ChineseSimplified.isl",
    "chars": 12960,
    "preview": "; *** Inno Setup version 6.4.0+ Chinese Simplified messages ***\n;\n; To download user-contributed translations of this f"
  },
  {
    "path": "windows/inno-languages/English.isl",
    "chars": 20724,
    "preview": "; *** Inno Setup version 6.4.0+ English messages ***\n;\n; To download user-contributed translations of this file, go to:"
  },
  {
    "path": "windows/iris-updater.bat",
    "chars": 2725,
    "preview": "@echo off\ntitle IRIS Updater\nsetlocal enabledelayedexpansion\n\nset \"api_url=https://api.github.com/repos/nini22P/iris/rel"
  },
  {
    "path": "windows/runner/CMakeLists.txt",
    "chars": 1796,
    "preview": "cmake_minimum_required(VERSION 3.14)\nproject(runner LANGUAGES CXX)\n\n# Define the application target. To change its name,"
  },
  {
    "path": "windows/runner/Runner.rc",
    "chars": 3005,
    "preview": "// Microsoft Visual C++ generated resource script.\n//\n#pragma code_page(65001)\n#include \"resource.h\"\n\n#define APSTUDIO_R"
  },
  {
    "path": "windows/runner/flutter_window.cpp",
    "chars": 2122,
    "preview": "#include \"flutter_window.h\"\n\n#include <optional>\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nFlutterWindow::Flutt"
  },
  {
    "path": "windows/runner/flutter_window.h",
    "chars": 928,
    "preview": "#ifndef RUNNER_FLUTTER_WINDOW_H_\n#define RUNNER_FLUTTER_WINDOW_H_\n\n#include <flutter/dart_project.h>\n#include <flutter/f"
  },
  {
    "path": "windows/runner/main.cpp",
    "chars": 1257,
    "preview": "#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n#include <windows.h>\n\n#include \"flutter_w"
  },
  {
    "path": "windows/runner/resource.h",
    "chars": 432,
    "preview": "//{{NO_DEPENDENCIES}}\n// Microsoft Visual C++ generated include file.\n// Used by Runner.rc\n//\n#define IDI_APP_ICON      "
  },
  {
    "path": "windows/runner/runner.exe.manifest",
    "chars": 602,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersi"
  },
  {
    "path": "windows/runner/utils.cpp",
    "chars": 1797,
    "preview": "#include \"utils.h\"\n\n#include <flutter_windows.h>\n#include <io.h>\n#include <stdio.h>\n#include <windows.h>\n\n#include <iost"
  },
  {
    "path": "windows/runner/utils.h",
    "chars": 672,
    "preview": "#ifndef RUNNER_UTILS_H_\n#define RUNNER_UTILS_H_\n\n#include <string>\n#include <vector>\n\n// Creates a console for the proce"
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the nini22P/Iris GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 202 files (1.2 MB), approximately 294.6k tokens, and a symbol index with 387 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!