Repository: asmroneapp/Yuro Branch: main Commit: 2817087af4a0 Files: 318 Total size: 818.7 KB Directory structure: gitextract_mk7l9x6b/ ├── .github/ │ ├── scripts/ │ │ └── process_commits.sh │ └── workflows/ │ └── build.yml ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── README_en.md ├── analysis_options.yaml ├── android/ │ ├── .gitignore │ ├── app/ │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src/ │ │ ├── debug/ │ │ │ └── AndroidManifest.xml │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ ├── com/ │ │ │ │ │ └── example/ │ │ │ │ │ └── asmrapp/ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── one/ │ │ │ │ └── asmr/ │ │ │ │ └── yuro/ │ │ │ │ ├── MainActivity.kt │ │ │ │ └── lyric/ │ │ │ │ ├── LyricOverlayPlugin.kt │ │ │ │ └── LyricOverlayService.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-v21/ │ │ │ │ └── launch_background.xml │ │ │ ├── layout/ │ │ │ │ └── lyric_overlay.xml │ │ │ ├── values/ │ │ │ │ └── styles.xml │ │ │ ├── values-night/ │ │ │ │ └── styles.xml │ │ │ └── xml/ │ │ │ └── network_security_config.xml │ │ └── profile/ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ └── settings.gradle ├── devtools_options.yaml ├── docs/ │ ├── architecture.md │ ├── audio_architecture.md │ ├── guidelines.md │ ├── guidelines_en.md │ └── guidelines_zh.md ├── ios/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── 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 ├── lib/ │ ├── common/ │ │ └── constants/ │ │ └── strings.dart │ ├── core/ │ │ ├── audio/ │ │ │ ├── README.md │ │ │ ├── audio_player_handler.dart │ │ │ ├── audio_player_service.dart │ │ │ ├── audio_service.dart │ │ │ ├── cache/ │ │ │ │ └── audio_cache_manager.dart │ │ │ ├── controllers/ │ │ │ │ └── playback_controller.dart │ │ │ ├── events/ │ │ │ │ ├── playback_event.dart │ │ │ │ └── playback_event_hub.dart │ │ │ ├── i_audio_player_service.dart │ │ │ ├── models/ │ │ │ │ ├── audio_track_info.dart │ │ │ │ ├── file_path.dart │ │ │ │ ├── play_mode.dart │ │ │ │ ├── playback_context.dart │ │ │ │ └── subtitle.dart │ │ │ ├── notification/ │ │ │ │ └── audio_notification_service.dart │ │ │ ├── state/ │ │ │ │ └── playback_state_manager.dart │ │ │ ├── storage/ │ │ │ │ ├── i_playback_state_repository.dart │ │ │ │ └── playback_state_repository.dart │ │ │ └── utils/ │ │ │ ├── audio_error_handler.dart │ │ │ ├── playlist_builder.dart │ │ │ └── track_info_creator.dart │ │ ├── cache/ │ │ │ └── recommendation_cache_manager.dart │ │ ├── di/ │ │ │ └── service_locator.dart │ │ ├── platform/ │ │ │ ├── dummy_lyric_overlay_controller.dart │ │ │ ├── i_lyric_overlay_controller.dart │ │ │ ├── lyric_overlay_controller.dart │ │ │ ├── lyric_overlay_manager.dart │ │ │ └── wakelock_controller.dart │ │ ├── subtitle/ │ │ │ ├── cache/ │ │ │ │ └── subtitle_cache_manager.dart │ │ │ ├── i_subtitle_service.dart │ │ │ ├── managers/ │ │ │ │ └── subtitle_state_manager.dart │ │ │ ├── parsers/ │ │ │ │ ├── lrc_parser.dart │ │ │ │ ├── subtitle_parser.dart │ │ │ │ ├── subtitle_parser_factory.dart │ │ │ │ └── vtt_parser.dart │ │ │ ├── subtitle_loader.dart │ │ │ ├── subtitle_service.dart │ │ │ └── utils/ │ │ │ └── subtitle_matcher.dart │ │ └── theme/ │ │ ├── app_colors.dart │ │ ├── app_theme.dart │ │ └── theme_controller.dart │ ├── data/ │ │ ├── models/ │ │ │ ├── audio/ │ │ │ │ └── README.md │ │ │ ├── auth/ │ │ │ │ └── auth_resp/ │ │ │ │ ├── auth_resp.dart │ │ │ │ ├── auth_resp.freezed.dart │ │ │ │ ├── auth_resp.g.dart │ │ │ │ ├── user.dart │ │ │ │ ├── user.freezed.dart │ │ │ │ └── user.g.dart │ │ │ ├── files/ │ │ │ │ ├── child.dart │ │ │ │ ├── child.freezed.dart │ │ │ │ ├── child.g.dart │ │ │ │ ├── files.dart │ │ │ │ ├── files.freezed.dart │ │ │ │ ├── files.g.dart │ │ │ │ ├── work.dart │ │ │ │ ├── work.freezed.dart │ │ │ │ └── work.g.dart │ │ │ ├── mark_lists/ │ │ │ │ ├── mark_lists.dart │ │ │ │ ├── mark_lists.freezed.dart │ │ │ │ ├── mark_lists.g.dart │ │ │ │ ├── pagination.dart │ │ │ │ ├── pagination.freezed.dart │ │ │ │ ├── pagination.g.dart │ │ │ │ ├── playlist.dart │ │ │ │ ├── playlist.freezed.dart │ │ │ │ └── playlist.g.dart │ │ │ ├── mark_status.dart │ │ │ ├── my_lists/ │ │ │ │ ├── README.md │ │ │ │ └── my_playlists/ │ │ │ │ ├── my_playlists.dart │ │ │ │ ├── my_playlists.freezed.dart │ │ │ │ ├── my_playlists.g.dart │ │ │ │ ├── pagination.dart │ │ │ │ ├── pagination.freezed.dart │ │ │ │ ├── pagination.g.dart │ │ │ │ ├── playlist.dart │ │ │ │ ├── playlist.freezed.dart │ │ │ │ └── playlist.g.dart │ │ │ ├── playback/ │ │ │ │ ├── playback_state.dart │ │ │ │ ├── playback_state.freezed.dart │ │ │ │ └── playback_state.g.dart │ │ │ ├── playlists_with_exist_statu/ │ │ │ │ ├── pagination.dart │ │ │ │ ├── pagination.freezed.dart │ │ │ │ ├── pagination.g.dart │ │ │ │ ├── playlist.dart │ │ │ │ ├── playlist.freezed.dart │ │ │ │ ├── playlist.g.dart │ │ │ │ ├── playlists_with_exist_statu.dart │ │ │ │ ├── playlists_with_exist_statu.freezed.dart │ │ │ │ └── playlists_with_exist_statu.g.dart │ │ │ └── works/ │ │ │ ├── circle.dart │ │ │ ├── circle.freezed.dart │ │ │ ├── circle.g.dart │ │ │ ├── en_us.dart │ │ │ ├── en_us.freezed.dart │ │ │ ├── en_us.g.dart │ │ │ ├── i18n.dart │ │ │ ├── i18n.freezed.dart │ │ │ ├── i18n.g.dart │ │ │ ├── ja_jp.dart │ │ │ ├── ja_jp.freezed.dart │ │ │ ├── ja_jp.g.dart │ │ │ ├── language_edition.dart │ │ │ ├── language_edition.freezed.dart │ │ │ ├── language_edition.g.dart │ │ │ ├── other_language_editions_in_db.dart │ │ │ ├── other_language_editions_in_db.freezed.dart │ │ │ ├── other_language_editions_in_db.g.dart │ │ │ ├── pagination.dart │ │ │ ├── pagination.freezed.dart │ │ │ ├── pagination.g.dart │ │ │ ├── tag.dart │ │ │ ├── tag.freezed.dart │ │ │ ├── tag.g.dart │ │ │ ├── translation_bonus_lang.dart │ │ │ ├── translation_bonus_lang.freezed.dart │ │ │ ├── translation_bonus_lang.g.dart │ │ │ ├── translation_info.dart │ │ │ ├── translation_info.freezed.dart │ │ │ ├── translation_info.g.dart │ │ │ ├── work.dart │ │ │ ├── work.freezed.dart │ │ │ ├── work.g.dart │ │ │ ├── works.dart │ │ │ ├── works.freezed.dart │ │ │ ├── works.g.dart │ │ │ ├── zh_cn.dart │ │ │ ├── zh_cn.freezed.dart │ │ │ └── zh_cn.g.dart │ │ ├── repositories/ │ │ │ ├── audio/ │ │ │ │ └── README.md │ │ │ └── auth_repository.dart │ │ └── services/ │ │ ├── api_service.dart │ │ ├── auth_service.dart │ │ └── interceptors/ │ │ └── auth_interceptor.dart │ ├── main.dart │ ├── presentation/ │ │ ├── layouts/ │ │ │ ├── work_layout_config.dart │ │ │ └── work_layout_strategy.dart │ │ ├── models/ │ │ │ └── filter_state.dart │ │ ├── viewmodels/ │ │ │ ├── auth_viewmodel.dart │ │ │ ├── base/ │ │ │ │ └── paginated_works_viewmodel.dart │ │ │ ├── detail_viewmodel.dart │ │ │ ├── favorites_viewmodel.dart │ │ │ ├── home_viewmodel.dart │ │ │ ├── player_viewmodel.dart │ │ │ ├── playlist_works_viewmodel.dart │ │ │ ├── playlists_viewmodel.dart │ │ │ ├── popular_viewmodel.dart │ │ │ ├── recommend_viewmodel.dart │ │ │ ├── search_viewmodel.dart │ │ │ ├── settings/ │ │ │ │ └── cache_manager_viewmodel.dart │ │ │ └── similar_works_viewmodel.dart │ │ └── widgets/ │ │ └── auth/ │ │ └── login_dialog.dart │ ├── screens/ │ │ ├── contents/ │ │ │ ├── home_content.dart │ │ │ ├── playlists/ │ │ │ │ ├── playlist_works_view.dart │ │ │ │ └── playlists_list_view.dart │ │ │ ├── playlists_content.dart │ │ │ ├── popular_content.dart │ │ │ └── recommend_content.dart │ │ ├── detail_screen.dart │ │ ├── docs/ │ │ │ └── main_screen.md │ │ ├── favorites_screen.dart │ │ ├── main_screen.dart │ │ ├── player_screen.dart │ │ ├── search_screen.dart │ │ ├── settings/ │ │ │ └── cache_manager_screen.dart │ │ └── similar_works_screen.dart │ ├── utils/ │ │ ├── file_size_formatter.dart │ │ └── logger.dart │ └── widgets/ │ ├── common/ │ │ └── tag_chip.dart │ ├── detail/ │ │ ├── mark_selection_dialog.dart │ │ ├── playlist_selection_dialog.dart │ │ ├── work_action_buttons.dart │ │ ├── work_cover.dart │ │ ├── work_file_item.dart │ │ ├── work_files_list.dart │ │ ├── work_files_skeleton.dart │ │ ├── work_folder_item.dart │ │ ├── work_info.dart │ │ ├── work_info_header.dart │ │ └── work_stats_info.dart │ ├── drawer_menu.dart │ ├── filter/ │ │ ├── filter_panel.dart │ │ └── filter_with_keyword.dart │ ├── lyrics/ │ │ └── components/ │ │ ├── lyric_line.dart │ │ └── player_lyric_view.dart │ ├── mini_player/ │ │ ├── mini_player.dart │ │ ├── mini_player_controls.dart │ │ ├── mini_player_cover.dart │ │ └── mini_player_progress.dart │ ├── pagination_controls.dart │ ├── player/ │ │ ├── player_controls.dart │ │ ├── player_cover.dart │ │ ├── player_progress.dart │ │ ├── player_seek_controls.dart │ │ └── player_work_info.dart │ ├── work_card/ │ │ ├── components/ │ │ │ ├── work_cover_image.dart │ │ │ ├── work_footer.dart │ │ │ ├── work_info_section.dart │ │ │ ├── work_tags_panel.dart │ │ │ └── work_title.dart │ │ └── work_card.dart │ ├── work_grid/ │ │ ├── components/ │ │ │ ├── grid_content.dart │ │ │ ├── grid_empty.dart │ │ │ ├── grid_error.dart │ │ │ └── grid_loading.dart │ │ ├── enhanced_work_grid_view.dart │ │ └── models/ │ │ └── grid_config.dart │ ├── work_grid.dart │ ├── work_grid_view.dart │ └── work_row.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 │ ├── Podfile │ ├── 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 ├── web/ │ ├── index.html │ └── manifest.json └── windows/ ├── .gitignore ├── CMakeLists.txt ├── flutter/ │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake └── 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/scripts/process_commits.sh ================================================ #!/bin/bash # 主标题的 emoji 映射 process_commit() { local title="$1" case "$title" in feat:*|feature:*) echo "✨ ${title#*: }" ;; # 新功能 fix:*) echo "🐛 ${title#*: }" ;; # 修复 docs:*) echo "📝 ${title#*: }" ;; # 文档 style:*) echo "💄 ${title#*: }" ;; # 样式 refactor:*) echo "♻️ ${title#*: }" ;; # 重构 perf:*) echo "⚡️ ${title#*: }" ;; # 性能 test:*) echo "🧪 ${title#*: }" ;; # 测试 build:*) echo "📦 ${title#*: }" ;; # 构建 ci:*) echo "🎡 ${title#*: }" ;; # CI chore:*) echo "🔧 ${title#*: }" ;; # 杂项 revert:*) echo "⏮️ ${title#*: }" ;; # 回退 *) case "$title" in Add*|Implement*) echo "✨ $title" ;; Enhance*|Improve*) echo "🚀 $title" ;; Update*) echo "⚡️ $title" ;; Integrate*|Configure*) echo "🔌 $title" ;; Fix*|Resolve*) echo "🐛 $title" ;; Refactor*) echo "♻️ $title" ;; Remove*|Delete*) echo "🔥 $title" ;; Revert*) echo "⏮️ $title" ;; *) echo "🔧 $title" ;; esac ;; esac } # 处理详细信息 process_details() { local details="" local in_commit_body=false while IFS= read -r line; do if [[ $line == -* ]]; then # 处理列表项 details+="$(process_detail "$line")\n" elif [[ $line =~ ^These[[:space:]]changes ]]; then # 处理总结行,保持原样并添加换行 details+=" $line\n" elif [[ -n $line ]]; then # 处理其他非空行(比如列表项的延续行),保持缩进 details+=" $line\n" else # 空行处理 details+="\n" fi done echo -e "$details" } # 详细信息的 emoji 映射 process_detail() { local content="${1:2}" # 删除开头的 "- " local prefix=" - " # 修改缩进格式,保持列表形式 # 1. 首先检查动词开头 case "$content" in Replace*|Swap*|Change*) echo "${prefix}🔄 $content" && return ;; Increase*|Add*|Extend*) echo "${prefix}⬆️ $content" && return ;; Decrease*|Reduce*|Remove*) echo "${prefix}⬇️ $content" && return ;; Update*|Refresh*) echo "${prefix}🔁 $content" && return ;; Enhance*|Improve*) echo "${prefix}⚡️ $content" && return ;; Create*|Generate*) echo "${prefix}✨ $content" && return ;; Modify*|Adjust*) echo "${prefix}🔧 $content" && return ;; Fix*|Resolve*) echo "${prefix}🐛 $content" && return ;; Refactor*) echo "${prefix}♻️ $content" && return ;; Implement*) echo "${prefix}🎯 $content" && return ;; Integrate*) echo "${prefix}🔌 $content" && return ;; Ensure*|Verify*) echo "${prefix}✅ $content" && return ;; Develop*) echo "${prefix}🏗️ $content" && return ;; esac # 2. 检查特定功能/组件组合 if [[ "$content" =~ (audio.*player|player.*audio) ]]; then echo "${prefix}🎵 $content" # 音频播放器特定 return fi if [[ "$content" =~ (lyric.*overlay|overlay.*lyric) ]]; then echo "${prefix}📺 $content" # 歌词覆盖层特定 return fi if [[ "$content" =~ (cache.*response|response.*cache) ]]; then echo "${prefix}💾 $content" # 响应缓存特定 return fi if [[ "$content" =~ (error.*handling|handling.*error) ]]; then echo "${prefix}🛡️ $content" # 错误处理特定 return fi # 3. 检查特定技术术语 if [[ "$content" =~ dependency.injection ]]; then echo "${prefix}💉 $content" # 依赖注入 return fi if [[ "$content" =~ state.management ]]; then echo "${prefix}📊 $content" # 状态管理 return fi # 4. 检查具体内容类型 case "$content" in *cache*|*Cache*|*storage*) echo "${prefix}💾 $content" ;; # 缓存/存储相关 *API*|*service*|*Service*|*request*) echo "${prefix}🌐 $content" ;; # API/服务相关 *UI*|*Screen*|*interface*|*layout*|*visual*|*theme*) echo "${prefix}💫 $content" ;; # UI/布局/主题相关 *audio*|*playback*|*media*) echo "${prefix}🎵 $content" ;; # 音频相关 *test*|*Test*) echo "${prefix}🧪 $content" ;; # 测试相关 *security*|*permission*|*auth*) echo "${prefix}🔒 $content" ;; # 安全/权限相关 *document*|*template*|*readability*) echo "${prefix}📝 $content" ;; # 文档相关 *component*|*widget*|*display*) echo "${prefix}🎨 $content" ;; # 组件/显示相关 *logic*|*handling*|*management*|*dependency*) echo "${prefix}🧮 $content" ;; # 逻辑/处理/依赖相关 *performance*|*efficiency*|*optimization*) echo "${prefix}⚡️ $content" ;; # 性能相关 *error*|*exception*|*handling*) echo "${prefix}🛡️ $content" ;; # 错误处理相关 *animation*|*transition*) echo "${prefix}✨ $content" ;; # 动画相关 *network*|*connectivity*) echo "${prefix}📡 $content" ;; # 网络相关 *data*|*model*|*entity*) echo "${prefix}💽 $content" ;; # 数据模型相关 *button*|*control*|*interaction*) echo "${prefix}🎮 $content" ;; # 控件/交互相关 *style*|*color*|*font*) echo "${prefix}🎨 $content" ;; # 样式相关 *) echo "${prefix}📌 $content" ;; # 其他细节 esac } # 主处理逻辑 current_commit="" commit_details="" while IFS= read -r line; do if [[ $line =~ ^[A-Za-z] ]] && [[ ! $line =~ ^These[[:space:]]changes ]]; then # 如果有之前的 commit,先输出它 if [ -n "$current_commit" ]; then if [ -n "$commit_details" ]; then echo "▶ $current_commit" echo -e "$(process_details "$commit_details")\n" else echo " $current_commit" fi fi current_commit=$(process_commit "$line") commit_details="" elif [[ $line == -* ]]; then commit_details+="$line\n" fi done # 输出最后一个 commit if [ -n "$current_commit" ]; then if [ -n "$commit_details" ]; then echo "▶ $current_commit" echo -e "$(process_details "$commit_details")" else echo " $current_commit" fi fi ================================================ FILE: .github/workflows/build.yml ================================================ name: Build and Release permissions: contents: write on: push: tags: - 'v*' # 当推送 v 开头的tag时触发,如 v1.0.0 workflow_dispatch: env: FLUTTER_VERSION: '3.27.0' jobs: build-android: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 # 获取完整的 git 历史 - name: Setup Java uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: '17' - name: Setup Flutter uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.FLUTTER_VERSION }} channel: 'stable' - name: Get dependencies run: flutter pub get - name: Create key.properties run: | echo "storePassword=${{ secrets.KEY_STORE_PASSWORD }}" >> android/key.properties echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties echo "storeFile=upload-keystore.jks" >> android/key.properties - name: Create keystore file run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/upload-keystore.jks - name: Build APK run: flutter build apk --release - name: Build App Bundle run: flutter build appbundle --release - name: Upload Android artifacts uses: actions/upload-artifact@v4 with: name: android-build path: | build/app/outputs/flutter-apk/app-release.apk build/app/outputs/bundle/release/app-release.aab build-ios: runs-on: macos-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 # 获取完整的 git 历史 - name: Setup Flutter uses: subosito/flutter-action@v2 with: flutter-version: '3.27.0' channel: 'stable' - name: Get dependencies run: flutter pub get - name: Build iOS run: flutter build ios --release --no-codesign - name: Package IPA run: | cd build/ios/iphoneos/ mkdir Payload cp -r Runner.app Payload/ zip -qq -r -9 app-release.ipa Payload - name: Upload iOS artifacts uses: actions/upload-artifact@v4 with: name: ios-build path: build/ios/iphoneos/app-release.ipa upload: runs-on: ubuntu-latest needs: [ build-android, build-ios ] steps: - uses: actions/checkout@v3 with: fetch-depth: 0 # 获取完整的 git 历史 - name: Download artifacts uses: actions/download-artifact@v4 with: path: ./dist/ merge-multiple: true - name: List files run: tree dist - name: Prepare scripts run: | mkdir -p .github/scripts chmod +x .github/scripts/process_commits.sh - name: Get Previous tag id: previoustag run: | CURRENT_TAG=${GITHUB_REF#refs/tags/} PREVIOUS_TAG=$(git describe --tags --abbrev=0 ${CURRENT_TAG}^ 2>/dev/null || echo "v0.0.0") echo "tag=${PREVIOUS_TAG}" >> $GITHUB_OUTPUT - name: Generate commit messages id: commits run: | CURRENT_TAG=${GITHUB_REF#refs/tags/} PREV_TAG=${{ steps.previoustag.outputs.tag }} COMMITS=$(git log ${PREV_TAG}..${CURRENT_TAG} --pretty=format:"%s%n%b" | .github/scripts/process_commits.sh) echo "commits<> $GITHUB_OUTPUT echo "$COMMITS" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Create Release if: startsWith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@v1 with: prerelease: true draft: false body: | ## 🚧 Pre-release Version ### 📋 Release Information **Version:** ${{ github.ref_name }} **Previous Version:** ${{ steps.previoustag.outputs.tag }} **Build Environment:** Flutter ${{ env.FLUTTER_VERSION }} ### 📝 Changelog ${{ steps.commits.outputs.commits }} ### 📦 Distribution | File | Description | Purpose | |------|-------------|----------| | `.apk` | Android Package | Direct installation for testing | | `.aab` | Android App Bundle | Google Play Store deployment | ### 🔍 Additional Notes - This is a pre-release build intended for testing purposes - Features and functionality may not be fully stable - Not recommended for production use ### 📱 Compatibility - Minimum Android SDK: 21 (Android 5.0) - Target Android SDK: 33 (Android 13) > **Note:** Please report any issues or bugs through the GitHub issue tracker. files: | dist/flutter-apk/app-release.apk dist/bundle/release/app-release.aab dist/app-release.ipa env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload artifacts if not release if: startsWith(github.ref, 'refs/tags/') == false uses: actions/upload-artifact@v4 with: name: everything path: dist/ ================================================ 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 # 添加以下内容 **/android/key.properties **/android/app/upload-keystore.jks ================================================ 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: "2663184aa79047d0a33a14a3b607954f8fdd8730" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - platform: android create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - platform: ios create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - platform: linux create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - platform: macos create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - platform: web create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - platform: windows create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: LICENSE ================================================ # Creative Commons Attribution-NonCommercial-ShareAlike License (CC BY-NC-SA) ## License Summary This license lets others remix, tweak, and build upon your work non-commercially, as long as they credit you and license their new creations under the identical terms. ## Full License ### 1. You are free to: - Share — copy and redistribute the material in any medium or format. - Adapt — remix, transform, and build upon the material. ### 2. Under the following terms: - Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. - NonCommercial — You may not use the material for commercial purposes. - ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original. ### 3. No additional restrictions: You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. ### 4. Notices: You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation. ### 5. Other rights: In no way are any of the following rights affected by the license: - Your fair dealing or fair use rights; - The rights of others to use the material for their own purposes; - The rights of the licensor to use the material for their own purposes. ### 6. Disclaimer: This license does not grant you any rights to use the material in a way that would infringe on the rights of others. For more information, visit [Creative Commons](https://creativecommons.org/licenses/by-nc-sa/4.0/). ================================================ FILE: README.md ================================================ # Yuro [English](README_en.md) 一个使用 Flutter 构建的 ASMR.ONE 客户端。 ## 项目概述 Yuro 旨在通过精美的动画和现代化的用户界面,提供流畅愉悦的 ASMR 聆听体验。 ## 特性 - 稳定的后台播放,再也不用担心杀后台了 - 精美的动画效果 - 流畅的播放体验 - 简洁的UI设计 - 全方位的智能缓存机制 - 图片智能缓存:优化封面加载速度,告别重复加载 - 字幕本地缓存:实现快速字幕匹配与加载 - 音频文件缓存:减少重复下载,节省流量开销 - 为服务器减轻压力 - 智能的缓存策略确保资源高效利用 - 懒加载机制避免无效请求 - 合理的缓存清理机制平衡本地存储 ## 开发准则 我们维护了一套完整的开发准则以确保代码质量和一致性: - [开发准则](docs/guidelines_zh.md) ## 项目结构
lib/
├── core/                 # 核心功能
├── data/                # 数据层
├── domain/              # 领域层
├── presentation/        # 表现层
└── common/             # 通用功能
## 开始使用 1. 克隆仓库 ```bash git clone [repository-url] ``` 2. 安装依赖 ```bash flutter pub get ``` 3. 运行应用 ```bash flutter run ``` ## 功能特性 - 现代化UI设计 - 流畅的动画效果 - ASMR 播放控制 - 播放列表管理 - 搜索功能 - 收藏功能 ## 贡献指南 在提交贡献之前,请阅读我们的[开发准则](docs/guidelines_zh.md)。 ## 许可证 本项目采用 Creative Commons 非商业性使用-相同方式共享许可证 (CC BY-NC-SA) - 查看 [LICENSE](LICENSE) 文件了解详细信息。该许可证允许他人修改和分享您的作品,但禁止商业用途,要求保留署名,并要求对修改后的作品以相同的许可证发布。 ================================================ FILE: README_en.md ================================================ # ASMR One App [中文说明](README.md) A beautiful and modern ASMR player application built with Flutter. ## Project Overview ASMR One App is designed to provide a smooth and enjoyable ASMR listening experience with beautiful animations and a modern user interface. ## Development Guidelines We maintain a comprehensive set of development guidelines to ensure code quality and consistency: - [Development Guidelines](docs/guidelines_en.md) ## Project Structure
lib/
├── core/                 # Core functionality
├── data/                # Data layer
├── domain/              # Domain layer
├── presentation/        # Presentation layer
└── common/             # Common functionality
## Getting Started 1. Clone the repository ```bash git clone [repository-url] ``` 2. Install dependencies ```bash flutter pub get ``` 3. Run the app ```bash flutter run ``` ## Features - Modern UI design - Smooth animations - ASMR playback control - Playlist management - Search functionality - Favorites collection ## Contributing Please read our [Development Guidelines](docs/guidelines_en.md) before making a contribution. ## License This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike License (CC BY-NC-SA) - see the [LICENSE](LICENSE) file for details. This license allows others to remix, tweak, and build upon your work non-commercially, as long as they credit you and license their new creations under the identical terms. ================================================ 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: exclude: - "**/*.g.dart" - "**/*.freezed.dart" errors: invalid_annotation_target: ignore ================================================ FILE: android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks ================================================ FILE: android/app/build.gradle ================================================ 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 = "one.asmr.yuro" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8 } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "one.asmr.yuro" // 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 { signingConfig signingConfigs.release // 如果还有问题,可以临时禁用混淆 minifyEnabled true // 改为 false 可以禁用混淆 shrinkResources true // 改为 false 可以禁用资源压缩 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } flutter { source = "../.." } ================================================ FILE: android/app/proguard-rules.pro ================================================ ## Flutter wrapper -keep class io.flutter.app.** { *; } -keep class io.flutter.plugin.** { *; } -keep class io.flutter.util.** { *; } -keep class io.flutter.view.** { *; } -keep class io.flutter.** { *; } -keep class io.flutter.plugins.** { *; } -keep class io.flutter.plugin.editing.** { *; } -dontwarn io.flutter.embedding.** -keepattributes Signature -keepattributes *Annotation* ## Gson rules -keepattributes Signature -keepattributes *Annotation* -dontwarn sun.misc.** ## audio_service plugin -keep class com.ryanheise.audioservice.** { *; } ## Fix Play Store Split -keep class com.google.android.play.core.splitcompat.** { *; } -dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication ## Fix for all Android classes that might be accessed via reflection -keep class androidx.lifecycle.DefaultLifecycleObserver -keep class androidx.lifecycle.LifecycleOwner -keepnames class androidx.lifecycle.LifecycleOwner ## Just Audio -keep class com.google.android.exoplayer2.** { *; } -dontwarn com.google.android.exoplayer2.** ## Cached network image -keep class com.bumptech.glide.** { *; } ================================================ FILE: android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/kotlin/com/example/asmrapp/MainActivity.kt ================================================ package com.example.asmrapp import io.flutter.embedding.android.FlutterActivity import com.ryanheise.audioservice.AudioServiceActivity class MainActivity: AudioServiceActivity() ================================================ FILE: android/app/src/main/kotlin/one/asmr/yuro/MainActivity.kt ================================================ package one.asmr.yuro import io.flutter.embedding.android.FlutterActivity import com.ryanheise.audioservice.AudioServiceActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import one.asmr.yuro.lyric.LyricOverlayPlugin class MainActivity: AudioServiceActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel( flutterEngine.dartExecutor.binaryMessenger, "one.asmr.yuro/lyric_overlay" ).setMethodCallHandler(LyricOverlayPlugin(this)) } } ================================================ FILE: android/app/src/main/kotlin/one/asmr/yuro/lyric/LyricOverlayPlugin.kt ================================================ package one.asmr.yuro.lyric import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.IBinder import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result class LyricOverlayPlugin(private val context: Context) : MethodCallHandler { private var service: LyricOverlayService? = null private val serviceIntent by lazy { Intent(context, LyricOverlayService::class.java) } private val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { service = (binder as? LyricOverlayService.LocalBinder)?.service } override fun onServiceDisconnected(name: ComponentName?) { service = null } } override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { "initialize" -> { try { context.startService(serviceIntent) context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE) result.success(null) } catch (e: Exception) { result.error( "SERVICE_START_ERROR", e.message, e.toString() ) } } "show" -> { service?.showLyric("") result.success(null) } "hide" -> { service?.hideLyric() result.success(null) } "updateLyric" -> { val arguments = call.arguments as? Map<*, *> val text = arguments?.get("text") as? String ?: "无字幕" service?.showLyric(text) result.success(null) } "dispose" -> { context.unbindService(serviceConnection) context.stopService(serviceIntent) service = null result.success(null) } "isShowing" -> { result.success(service?.isShowing() ?: false) } else -> result.notImplemented() } } } ================================================ FILE: android/app/src/main/kotlin/one/asmr/yuro/lyric/LyricOverlayService.kt ================================================ package one.asmr.yuro.lyric import android.app.Service import android.content.Context import android.content.Intent import android.graphics.PixelFormat import android.os.Binder import android.os.IBinder import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.WindowManager import android.widget.TextView import one.asmr.yuro.R import android.view.Gravity class LyricOverlayService : Service() { private var windowManager: WindowManager? = null private var lyricView: View? = null private var params: WindowManager.LayoutParams? = null private var initialX = 0 private var initialY = 0 private var initialTouchX = 0f private var initialTouchY = 0f private val binder = LocalBinder() companion object { private const val PREFS_NAME = "LyricOverlayPrefs" private const val KEY_X = "window_x" private const val KEY_Y = "window_y" private const val KEY_SHOWING = "is_showing" } inner class LocalBinder : Binder() { val service: LyricOverlayService get() = this@LyricOverlayService } override fun onBind(intent: Intent?): IBinder = binder override fun onCreate() { super.onCreate() windowManager = getSystemService(WINDOW_SERVICE) as WindowManager } fun showLyric(text: String) { if (lyricView == null) { createLyricView() } (lyricView as? TextView)?.text = text getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) .edit() .putBoolean(KEY_SHOWING, true) .apply() } private fun createLyricView() { lyricView = LayoutInflater.from(this).inflate(R.layout.lyric_overlay, null) // 获取屏幕高度 val displayMetrics = resources.displayMetrics val screenHeight = displayMetrics.heightPixels // 读取保存的位置,默认位置设在屏幕2/3处 val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) val savedX = prefs.getInt(KEY_X, 50) // 距离右边50dp val savedY = prefs.getInt(KEY_Y, (screenHeight * 2 / 3)) // 屏幕高度的2/3处 params = WindowManager.LayoutParams( 360.dpToPx(), WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT ).apply { gravity = Gravity.TOP or Gravity.END x = savedX y = savedY windowAnimations = 0 } lyricView?.setOnTouchListener { _, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { initialX = params?.x ?: 0 initialY = params?.y ?: 0 initialTouchX = event.rawX initialTouchY = event.rawY } MotionEvent.ACTION_MOVE -> { val dx = (event.rawX - initialTouchX).toInt() val dy = (event.rawY - initialTouchY).toInt() params?.x = initialX - dx params?.y = initialY + dy params?.let { windowManager?.updateViewLayout(lyricView, it) } } MotionEvent.ACTION_UP -> { // 保存新位置 params?.let { params -> getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) .edit() .putInt(KEY_X, params.x) .putInt(KEY_Y, params.y) .apply() } } } true } windowManager?.addView(lyricView, params) } private fun Int.dpToPx(): Int { val scale = resources.displayMetrics.density return (this * scale + 0.5f).toInt() } fun hideLyric() { try { if (lyricView != null) { windowManager?.removeView(lyricView) lyricView = null getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) .edit() .putBoolean(KEY_SHOWING, false) .apply() } } catch (e: Exception) { e.printStackTrace() } } override fun onDestroy() { super.onDestroy() hideLyric() } fun isShowing(): Boolean { if (lyricView == null) { return getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) .getBoolean(KEY_SHOWING, false) } return true } } ================================================ FILE: android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/layout/lyric_overlay.xml ================================================ ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/xml/network_security_config.xml ================================================ 127.0.0.1 ================================================ FILE: android/app/src/profile/AndroidManifest.xml ================================================ ================================================ 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.3-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.1.0" apply false id "org.jetbrains.kotlin.android" version "1.8.22" 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: docs/architecture.md ================================================ # ASMR Music App 架构设计 ## 目录结构
lib/
├── main.dart              # 应用程序入口
├── screens/              # 页面
│   ├── home_screen.dart   # 主页(音乐列表)
│   ├── player_screen.dart # 播放页面
│   └── detail_screen.dart # 详情页面
├── widgets/              # 可重用组件
│   └── drawer_menu.dart   # 侧滑菜单
└── models/              # 数据模型(待添加)
    └── music.dart        # 音乐模型(待添加)
## 主要功能模块 1. 主页 (HomeScreen) - 显示音乐列表 - 搜索功能 - 侧滑菜单访问 2. 播放页 (PlayerScreen) - 音乐播放控制 - 进度条 - 音量控制 3. 详情页 (DetailScreen) - 显示音乐详细信息 - 评论功能(待实现) - 收藏功能(待实现) 4. 侧滑菜单 (DrawerMenu) - 主页导航 - 收藏列表 - 设置页面 ## 技术栈 - Flutter SDK - Material Design 3 - 路由管理: Flutter 内置导航 - 状态管理: 待定 ## 开发计划 1. 第一阶段:基础框架搭建 - [x] 创建基本页面结构 - [x] 实现页面导航 - [x] 设计侧滑菜单 2. 第二阶段:UI 实现 - [ ] 设计并实现音乐列表 - [ ] 设计并实现播放器界面 - [ ] 设计并实现详情页面 3. 第三阶段:功能实现 - [ ] 音乐播放功能 - [ ] 搜索功能 - [ ] 收藏功能 4. 第四阶段:优化 - [ ] 性能优化 - [ ] UI/UX 改进 - [ ] 代码重构 ## 注意事项 1. 代码规范 - 使用 const 构造函数 - 遵循 Flutter 官方代码风格 - 添加必要的代码注释 2. 性能考虑 - 合理使用 StatelessWidget 和 StatefulWidget - 避免不必要的重建 - 图片资源优化 3. 用户体验 - 添加加载状态提示 - 错误处理和提示 - 合理的动画过渡 ================================================ FILE: docs/audio_architecture.md ================================================ # ASMR One App 音频播放架构设计 ## 1. 架构概述 本文档描述了 ASMR One App 音频播放功能的架构设计。遵循 Clean Architecture 原则,采用事件驱动架构,将音频播放功能分为核心层、数据层和表现层。 ## 2. 目录结构
lib/
├── core/
│   └── audio/                      # 音频核心功能
│       ├── audio_player_service.dart    # 音频服务实现
│       ├── i_audio_player_service.dart  # 音频服务接口
│       ├── controllers/                 # 控制器
│       │   └── playback_controller.dart # 播放控制器
│       ├── events/                      # 事件系统
│       │   ├── playback_event.dart     # 事件定义
│       │   └── playback_event_hub.dart # 事件中心
│       ├── models/                      # 数据模型
│       │   ├── audio_track_info.dart   # 音轨信息
│       │   └── playback_context.dart   # 播放上下文
│       ├── notification/                # 通知栏
│       │   └── audio_notification_service.dart
│       ├── state/                       # 状态管理
│       │   └── playback_state_manager.dart
│       ├── storage/                     # 状态持久化
│       │   ├── i_playback_state_repository.dart
│       │   └── playback_state_repository.dart
│       └── utils/                       # 工具类
│           ├── audio_error_handler.dart
│           ├── playlist_builder.dart
│           └── track_info_creator.dart
└── presentation/
    └── viewmodels/
        └── player_viewmodel.dart   # 播放器视图模型
## 3. 核心组件设计 ### 3.1 音频服务接口 (IAudioPlayerService)
abstract class IAudioPlayerService {
  // 基础播放控制
  Future pause();
  Future resume();
  Future stop();
  Future seek(Duration position);
  Future previous();
  Future next();
  
  // 上下文管理
  Future playWithContext(PlaybackContext context);
  
  // 状态访问
  AudioTrackInfo? get currentTrack;
  PlaybackContext? get currentContext;
  
  // 状态持久化
  Future savePlaybackState();
  Future restorePlaybackState();
}
### 3.2 事件系统 (PlaybackEventHub)
class PlaybackEventHub {
  // 主事件流
  final _eventSubject = PublishSubject();
  
  // 分类事件流
  late final Stream playbackState;
  late final Stream trackChange;
  late final Stream contextChange;
  late final Stream playbackProgress;
  late final Stream errors;
  
  void emit(PlaybackEvent event);
}
## 4. 事件模型 ### 4.1 播放事件 (PlaybackEvent)
abstract class PlaybackEvent {}

class PlaybackStateEvent extends PlaybackEvent {
  final PlayerState state;
  final Duration position;
  final Duration? duration;
}

class TrackChangeEvent extends PlaybackEvent {
  final AudioTrackInfo track;
  final Child file;
  final Work work;
}

// ... 其他事件定义
## 5. 状态管理 ### 5.1 播放状态管理器 (PlaybackStateManager)
class PlaybackStateManager {
  final AudioPlayer _player;
  final PlaybackEventHub _eventHub;
  final IPlaybackStateRepository _stateRepository;
  
  void initStateListeners();
  void updateContext(PlaybackContext? context);
  void updateTrackInfo(AudioTrackInfo track);
  Future saveState();
  Future loadState();
}
## 6. 通知栏集成 ### 6.1 通知栏服务 (AudioNotificationService)
class AudioNotificationService {
  final AudioPlayer _player;
  final PlaybackEventHub _eventHub;
  AudioHandler? _audioHandler;
  
  Future init();
  void updateMetadata(AudioTrackInfo trackInfo);
}
## 7. 技术实现细节 ### 7.1 依赖注入 使用 GetIt 进行依赖管理: - PlaybackEventHub 注册为单例 - AudioPlayerService 注册为懒加载单例 - 所有依赖通过构造函数注入 ### 7.2 事件驱动 - 使用 RxDart 实现事件流 - 统一的事件中心管理所有播放相关事件 - 各组件通过事件通信,降低耦合 ### 7.3 错误处理 - 统一的错误处理机制 - 错误事件通过 EventHub 传递 - 支持错误追踪和日志记录 ## 8. 开发计划 1. 优化播放体验 - 优化事件处理性能 - 完善错误处理机制 - 改进状态同步逻辑 2. 增强功能 - 添加播放列表功能 - 支持更多播放模式 - 优化缓存策略 3. 改进架构 - 进一步解耦组件 - 优化依赖注入 - 完善单元测试 ================================================ FILE: docs/guidelines.md ================================================ # ASMR Music App Development Guidelines # ASMR Music App 开发准则 ## Important Notice | 重要说明 These guidelines are living documents that will evolve with the project. Any changes in practice must be reflected in these guidelines, especially the architecture section which must stay consistent with the actual project structure. 本准则是一个动态文档,会随项目发展而演进。实践中的任何变更都必须更新到本准则中,特别是架构部分必须与实际项目结构保持一致。 ## 1. Architecture Design Guidelines | 架构设计准则 ### 1.1 Decoupling Principles | 解耦原则 - Follow SOLID principles strictly | 严格遵循 SOLID 原则 - Use dependency injection | 使用依赖注入管理组件依赖 - Implement BLoC pattern | 采用 BLoC 模式分离业务逻辑和UI - Define interfaces for inter-module communication | 使用接口定义模块间通信契约 ### 1.2 Modularization Principles | 模块化原则 - Divide modules by functionality | 按功能划分模块 - Follow single responsibility principle | 遵循单一职责原则 - Clear interface communication | 模块间通过清晰的接口通信 - Share common components | 共享组件放置在 common/shared 目录下 ### 1.3 Code Organization | 代码组织
lib/
├── core/                 # Core functionality | 核心功能
│   ├── di/              # Dependency injection | 依赖注入
│   ├── theme/           # Theme configuration | 主题配置
│   └── utils/           # Utilities | 工具类
├── data/                # Data layer | 数据层
│   ├── models/          # Data models | 数据模型
│   ├── repositories/    # Data repositories | 数据仓库
│   └── services/        # Service implementations | 服务实现
├── domain/              # Domain layer | 领域层
│   ├── entities/        # Business entities | 业务实体
│   └── repositories/    # Repository interfaces | 仓库接口
├── presentation/        # Presentation layer | 表现层
│   ├── blocs/          # State management | 状态管理
│   ├── screens/        # Pages | 页面
│   └── widgets/        # Components | 组件
└── common/             # Common functionality | 通用功能
    ├── constants/      # Constants | 常量
    └── extensions/     # Extensions | 扩展方法
## 2. UI/UX Design Guidelines | UI/UX 设计准则 ### 2.1 Interface Design | 界面设计 - Follow Material Design 3 | 遵循 Material Design 3 设计规范 - Consistent color theme and typography | 使用一致的颜色主题和字体 - Maintain visual hierarchy | 保持视觉层次感和空间布局的平衡 - Pixel-perfect alignment | 注重细节,保持像素级别的对齐 ### 2.2 Animation Effects | 动画效果 - Use Flutter's built-in animation system | 使用 Flutter 内置动画系统 - Animation duration: 200-300ms | 所有动画持续时间保持在 200-300ms 之间 - Use curve animations (Curves.easeInOut) | 使用曲线动画 - Smooth page transitions | 实现平滑的页面转场效果 - Meaningful micro-interactions | 添加有意义的微交互动画 ### 2.3 Performance Optimization | 性能优化 - Use const constructors | 使用 const 构造器 - Proper use of StatelessWidget | 合理使用 StatelessWidget - Avoid complex calculations in build | 避免在 build 方法中进行复杂计算 - Use ListView.builder for long lists | 使用 ListView.builder 处理长列表 - Image compression and caching | 图片资源进行适当压缩和缓存 ## 3. Code Quality Guidelines | 代码质量准则 ### 3.1 Code Style | 代码风格 - Follow Dart style guide | 遵循 Dart 官方代码风格指南 - Use dartfmt | 使用 dartfmt 格式化代码 - Type safety | 类型安全,避��使用 dynamic - Proper documentation | 添加必要的注释,特别是复杂业务逻辑 ### 3.2 Testing Standards | 测试规范 - Unit test coverage > 80% | 单元测试覆盖率要求 > 80% - Widget testing | 编写 Widget 测试验证UI行为 - Integration testing | 集成测试覆盖关键业务流程 - Dependency isolation | 使用 mock 进行依赖隔离 ## 4. Version Control Guidelines | 版本控制准则 ### 4.1 Git Standards | Git 规范 - Feature Branch workflow | 使用 Feature Branch 工作流 - Angular commit convention | commit 信息遵循 Angular 规范 - Regular code reviews | 定期进行代码审查 - Stable main branch | 保持 main 分支稳定可用 ### 4.2 Release Standards | 发布规范 - Semantic versioning | 遵循语义化版本控制 - Clear changelog | 每次发布都要有清晰的更新日志 - Complete testing before release | 重要版本发布前进行完整测试 - Documentation updates | 保留每个版本的文档更新 ## 5. Project Management Guidelines | 项目管理准则 ### 5.1 Documentation Management | 文档管理 - API documentation | 及时更新 API 文档 - Clear README | 维护清晰的 README - Design decisions | 记录重要的设计决策 - User and developer guides | 编写用户指南和开发指南 ### 5.2 Issue Tracking | 问题追踪 - Track bugs and features | 使用 Issue 跟踪 bug 和新功能 - Proper labeling | 为每个 Issue 添加适当的标签 - Task traceability | 保持任务的可追踪性 - Regular status updates | 定期回顾和更新任务状态 ================================================ FILE: docs/guidelines_en.md ================================================ # ASMR One App Development Guidelines [中文版本](guidelines_zh.md) ## Important Notice These guidelines are living documents that will evolve with the project. Any changes in practice must be reflected in these guidelines, especially the architecture section which must stay consistent with the actual project structure. ## 1. Architecture Design Guidelines ### 1.1 Decoupling Principles - Follow SOLID principles strictly - Use dependency injection - Implement BLoC pattern - Define interfaces for inter-module communication ### 1.2 Modularization Principles - Divide modules by functionality - Follow single responsibility principle - Clear interface communication - Share common components ### 1.3 Code Organization
lib/
├── core/                 # Core functionality
│   ├── di/              # Dependency injection
│   ├── theme/           # Theme configuration
│   └── utils/           # Utilities
├── data/                # Data layer
│   ├── models/          # Data models
│   ├── repositories/    # Data repositories
│   └── services/        # Service implementations
├── domain/              # Domain layer
│   ├── entities/        # Business entities
│   └── repositories/    # Repository interfaces
├── presentation/        # Presentation layer
│   ├── blocs/          # State management
│   ├── screens/        # Pages
│   └── widgets/        # Components
└── common/             # Common functionality
    ├── constants/      # Constants definitions
    └── extensions/     # Extensions
### 1.4 String Management - All text strings must be centrally defined in `lib/common/constants/strings.dart` - No hardcoded strings allowed in the code - String constants should be grouped by feature modules - Prepared for future internationalization - String names should clearly express their purpose Example: ```dart class Strings { // App static const String appName = 'asmr.one'; // Common static const String loading = 'Loading...'; // Feature specific static const String search = 'Search'; } ``` ## 2. UI/UX Design Guidelines ### 2.1 Interface Design - Follow Material Design 3 - Consistent color theme and typography - Maintain visual hierarchy - Pixel-perfect alignment ### 2.2 Animation Effects - Use Flutter's built-in animation system - Animation duration: 200-300ms - Use curve animations (Curves.easeInOut) - Smooth page transitions - Meaningful micro-interactions ### 2.3 Performance Optimization - Use const constructors - Proper use of StatelessWidget - Avoid complex calculations in build - Use ListView.builder for long lists - Image compression and caching ## 3. Code Quality Guidelines ### 3.1 Code Style - Follow Dart style guide - Use dartfmt - Type safety - Proper documentation ### 3.2 Testing Standards - Unit test coverage > 80% - Widget testing - Integration testing - Dependency isolation ## 4. Version Control Guidelines ### 4.1 Git Standards - Feature Branch workflow - Angular commit convention - Regular code reviews - Stable main branch ### 4.2 Release Standards - Semantic versioning - Clear changelog - Complete testing before release - Documentation updates ## 5. Project Management Guidelines ### 5.1 Documentation Management - API documentation - Clear README - Design decisions - User and developer guides ### 5.2 Issue Tracking - Track bugs and features - Proper labeling - Task traceability - Regular status updates ================================================ FILE: docs/guidelines_zh.md ================================================ # ASMR One App 开发准则 [English Version](guidelines_en.md) ## 重要说明 本准则是一个动态文档,会随项目发展而演进。实践中的任何变更都必须更新到本准则中,特别是架构部分必须与实际项目结构保持一致。 ## 1. 架构设计准则 ### 1.1 解耦原则 - 严格遵循 SOLID 原则 - 使用依赖注入管理组件依赖 - 采用 BLoC 模式分离业务逻辑和UI - 使用接口定义模块间通信契约 ### 1.2 模块化原则 - 按功能划分模块 - 遵循单一职责原则 - 模块间通过清晰的接口通信 - 共享组件放置在 common/shared 目录下 ### 1.3 代码组织
lib/
├── core/                 # 核心功能
│   ├── di/              # 依赖注入
│   ├── theme/           # 主题配置
│   └── utils/           # 工具类
├── data/                # 数据层
│   ├── models/          # 数据模型
│   ├── repositories/    # 数据仓库
│   └── services/        # 服务实现
├── domain/              # 领域层
│   ├── entities/        # 业务实体
│   └── repositories/    # 仓库接口
├── presentation/        # 表现层
│   ├── blocs/          # 状态管理
│   ├── screens/        # 页面
│   └── widgets/        # 组件
└── common/             # 通用功能
    ├── constants/      # 常量定义
    └── extensions/     # 扩展方法
### 1.4 字符串管理 - 所有文本字符串必须在 `lib/common/constants/strings.dart` 中集中定义 - 禁止在代码中使用硬编码的字符串 - 字符串常量按功能模块分组管理 - 为后续国际化做好准备 - 字符串命名应清晰表达其用途 示例: ```dart class Strings { // App static const String appName = 'asmr.one'; // Common static const String loading = '加载中...'; // Feature specific static const String search = '搜索'; } ``` ## 2. UI/UX 设计准则 ### 2.1 界面设计 - 遵循 Material Design 3 设计规范 - 使用一致的颜色主题和字体 - 保持视觉层次感和空间布局的平衡 - 注重细节,保持像素级别的对齐 ### 2.2 动画效果 - 使用 Flutter 内置动画系统 - 所有动画持续时间保持在 200-300ms 之间 - 使用曲线动画(推荐 Curves.easeInOut) - 实现平滑的页面转场效果 - 添加有意义的微交互动画 ### 2.3 性能优化 - 使用 const 构造器 - 合理使用 StatelessWidget - 避免在 build 方法中进行复杂计算 - 使用 ListView.builder 处理长列表 - 图片资源进行适当压缩和缓存 ## 3. 代码质量准则 ### 3.1 代码风格 - 遵循 Dart 官方代码风格指南 - 使用 dartfmt 格式化代码 - 类型安全,避免使用 dynamic - 添加必要的注释,特别是复杂业务逻辑 ### 3.2 测试规范 - 单元测试覆盖率要求 > 80% - 编写 Widget 测试验证UI行为 - 集成测试覆盖关键业务流程 - 使用 mock 进行依赖隔离 ## 4. 版本控制准则 ### 4.1 Git 规范 - 使用 Feature Branch 工作流 - commit 信息遵循 Angular 规范 - 定期进行代码审查 - 保持 main 分支稳定可用 ### 4.2 发布规范 - 遵循语义化版本控制 - 每次发布都要有清晰的更新日志 - 重要版本发布前进行完整测试 - 保留���个版本的文档更新 ## 5. 项目管理准则 ### 5.1 文档管理 - 及时更新 API 文档 - 维护清晰的 README - 记录重要的设计决策 - 编写用户指南和开发指南 ### 5.2 问题追踪 - 使用 Issue 跟踪 bug 和新功能 - 为每个 Issue 添加适当的标签 - 保持任务的可追踪性 - 定期回顾和更新任务状态 ================================================ FILE: ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 12.0 ================================================ FILE: ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end ================================================ 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 ================================================ ================================================ FILE: ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: ios/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName Asmrapp CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName asmrapp CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents ================================================ 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 */; }; 9B54596A1AA247DB213D70D9 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DB3CD38BE4BEDD7984A4C53 /* Pods_RunnerTests.framework */; }; FA9547A1CDFA8BA9285E4F0E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D991BFF7F7E1123F21EC9991 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 1864816EC7CE99BF3A03B425 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3BD433FFFD3DA36C18DC04D5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7B17DE0D71E08E74104A6DA7 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 8DB3CD38BE4BEDD7984A4C53 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9D845508FF1BDF0DBEC753A3 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; A315059E1F405C4EDE197F3C /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; A7561FF68870DE952DF640B5 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; D991BFF7F7E1123F21EC9991 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( FA9547A1CDFA8BA9285E4F0E /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; C30863FD7117B3791F210A0B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 9B54596A1AA247DB213D70D9 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C807B294A618700263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 575ABBC37CDFC79162ECE7F7 /* Pods */ = { isa = PBXGroup; children = ( 7B17DE0D71E08E74104A6DA7 /* Pods-Runner.debug.xcconfig */, 9D845508FF1BDF0DBEC753A3 /* Pods-Runner.release.xcconfig */, 3BD433FFFD3DA36C18DC04D5 /* Pods-Runner.profile.xcconfig */, A7561FF68870DE952DF640B5 /* Pods-RunnerTests.debug.xcconfig */, 1864816EC7CE99BF3A03B425 /* Pods-RunnerTests.release.xcconfig */, A315059E1F405C4EDE197F3C /* Pods-RunnerTests.profile.xcconfig */, ); name = Pods; path = Pods; sourceTree = ""; }; 78A3B507541DF7DDED4343BF /* Frameworks */ = { isa = PBXGroup; children = ( D991BFF7F7E1123F21EC9991 /* Pods_Runner.framework */, 8DB3CD38BE4BEDD7984A4C53 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 575ABBC37CDFC79162ECE7F7 /* Pods */, 78A3B507541DF7DDED4343BF /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; }; 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 = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C8080294A63A400263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( 60B65332F5596392DBC91AC5 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, C30863FD7117B3791F210A0B /* Frameworks */, ); 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 = ( 4C22EBC450685F3AE387A893 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, B62C690B77C8F1A1692748AD /* [CP] Embed Pods Frameworks */, AE562F71A72CF708023D2129 /* [CP] Copy Pods Resources */, ); 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"; }; 4C22EBC450685F3AE387A893 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 60B65332F5596392DBC91AC5 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 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"; }; AE562F71A72CF708023D2129 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; B62C690B77C8F1A1692748AD /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* 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 = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.example.asmrapp; 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; baseConfigurationReference = A7561FF68870DE952DF640B5 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.asmrapp.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; baseConfigurationReference = 1864816EC7CE99BF3A03B425 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.asmrapp.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; baseConfigurationReference = A315059E1F405C4EDE197F3C /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.asmrapp.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 = AppIcon; 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 = AppIcon; 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 = com.example.asmrapp; 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 = com.example.asmrapp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C8088294A63A400263BE5 /* Debug */, 331C8089294A63A400263BE5 /* Release */, 331C808A294A63A400263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/RunnerTests/RunnerTests.swift ================================================ import Flutter import UIKit import XCTest class RunnerTests: XCTestCase { func testExample() { // If you add code to the Runner application, consider adding tests here. // See https://developer.apple.com/documentation/xctest for more information about using XCTest. } } ================================================ FILE: lib/common/constants/strings.dart ================================================ class Strings { // App static const String appName = 'asmr.one'; // Common static const String loading = '加载中...'; static const String error = '出错了'; static const String retry = '重试'; static const String cancel = '取消'; static const String confirm = '确认'; // Home static const String search = '搜索'; static const String musicList = '音乐列表将在这里显示'; // Player static const String nowPlaying = '正在播放'; static const String playerPlaceholder = '播放器控件将在这里显示'; // Detail static const String detail = '音乐详情'; static const String detailPlaceholder = '音乐详细信息将在这里显示'; // Drawer static const String home = '主页'; static const String favorites = '我的收藏'; static const String settings = '设置'; } ================================================ FILE: lib/core/audio/README.md ================================================ # 音频核心功能 ## 当前架构 ### 1. 事件驱动系统 - 基于 RxDart 的事件中心 - 统一的事件定义和处理 - 支持事件过滤和转换 ### 2. 核心服务 (AudioPlayerService) - 实现 IAudioPlayerService 接口 - 通过依赖注入管理依赖 - 负责协调各个组件 ### 3. 状态管理 - PlaybackStateManager 负责状态维护 - 通过 EventHub 发送状态更新 - 支持状态持久化 ### 4. 通知栏集成 - 基于 audio_service 包 - 响应系统媒体控制 - 支持后台播放 ### 5. 依赖注入 通过 GetIt 管理所有依赖:
void setupServiceLocator() {
  // 注册 EventHub
  getIt.registerLazySingleton(() => PlaybackEventHub());
  
  // 注册音频服务
  getIt.registerLazySingleton(
    () => AudioPlayerService(
      eventHub: getIt(),
      stateRepository: getIt(),
    ),
  );
}
## 注意事项 - 所有状态更新通过 EventHub 传递 - 避免组件间直接调用 - 优先使用依赖注入 - 保持组件职责单一 ================================================ FILE: lib/core/audio/audio_player_handler.dart ================================================ import 'package:asmrapp/core/audio/events/playback_event_hub.dart'; import 'package:audio_service/audio_service.dart'; import 'package:just_audio/just_audio.dart'; import 'package:asmrapp/utils/logger.dart'; class AudioPlayerHandler extends BaseAudioHandler { final AudioPlayer _player; final PlaybackEventHub _eventHub; AudioPlayerHandler(this._player, this._eventHub) { AppLogger.debug('AudioPlayerHandler 初始化'); // 改为监听 EventHub _eventHub.playbackState.listen((event) { final state = PlaybackState( controls: [ MediaControl.skipToPrevious, event.state.playing ? MediaControl.pause : MediaControl.play, MediaControl.skipToNext, ], systemActions: const { MediaAction.seek, MediaAction.seekForward, MediaAction.seekBackward, }, androidCompactActionIndices: const [0, 1, 2], processingState: const { ProcessingState.idle: AudioProcessingState.idle, ProcessingState.loading: AudioProcessingState.loading, ProcessingState.buffering: AudioProcessingState.buffering, ProcessingState.ready: AudioProcessingState.ready, ProcessingState.completed: AudioProcessingState.completed, }[event.state.processingState]!, playing: event.state.playing, updatePosition: event.position, bufferedPosition: _player.bufferedPosition, speed: _player.speed, queueIndex: 0, ); playbackState.add(state); }); } @override Future play() async { AppLogger.debug('AudioHandler: 播放命令'); _player.play(); } @override Future pause() async { AppLogger.debug('AudioHandler: 暂停命令'); _player.pause(); } @override Future seek(Duration position) async { AppLogger.debug('AudioHandler: 跳转命令 position=$position'); await _player.seek(position); } @override Future stop() async { AppLogger.debug('AudioHandler: 停止命令'); await _player.stop(); } } ================================================ FILE: lib/core/audio/audio_player_service.dart ================================================ import 'dart:async'; import 'package:asmrapp/utils/logger.dart'; import 'package:just_audio/just_audio.dart'; import 'package:audio_session/audio_session.dart'; import './i_audio_player_service.dart'; import './models/audio_track_info.dart'; import './models/playback_context.dart'; import './notification/audio_notification_service.dart'; import './storage/i_playback_state_repository.dart'; import './utils/audio_error_handler.dart'; import './state/playback_state_manager.dart'; import './controllers/playback_controller.dart'; import './events/playback_event_hub.dart'; class AudioPlayerService implements IAudioPlayerService { late final AudioPlayer _player; late final AudioNotificationService _notificationService; late final ConcatenatingAudioSource _playlist; late final PlaybackStateManager _stateManager; late final PlaybackController _playbackController; final PlaybackEventHub _eventHub; final IPlaybackStateRepository _stateRepository; AudioPlayerService._internal({ required PlaybackEventHub eventHub, required IPlaybackStateRepository stateRepository, }) : _eventHub = eventHub, _stateRepository = stateRepository { _init(); } static AudioPlayerService? _instance; factory AudioPlayerService({ required PlaybackEventHub eventHub, required IPlaybackStateRepository stateRepository, }) { _instance ??= AudioPlayerService._internal( eventHub: eventHub, stateRepository: stateRepository, ); return _instance!; } Future _init() async { try { _player = AudioPlayer(); _notificationService = AudioNotificationService( _player, _eventHub, ); _playlist = ConcatenatingAudioSource(children: []); _stateManager = PlaybackStateManager( player: _player, stateRepository: _stateRepository, eventHub: _eventHub, ); _playbackController = PlaybackController( player: _player, stateManager: _stateManager, playlist: _playlist, ); final session = await AudioSession.instance; await session.configure(const AudioSessionConfiguration.music()); await _notificationService.init(); _stateManager.initStateListeners(); await restorePlaybackState(); } catch (e, stack) { AudioErrorHandler.handleError( AudioErrorType.init, '音频播放器初始化', e, stack, ); AudioErrorHandler.throwError( AudioErrorType.init, '音频播放器初始化', e, ); } } // 基础播放控制 @override Future pause() => _playbackController.pause(); @override Future resume() => _playbackController.play(); @override Future stop() async { await _playbackController.stop(); _stateManager.clearState(); } @override Future seek(Duration position) => _playbackController.seek(position); @override Future previous() => _playbackController.previous(); @override Future next() => _playbackController.next(); // 上下文管理 @override Future playWithContext(PlaybackContext context) async { await _playbackController.setPlaybackContext(context); // 添加自动播放 await resume(); } // 状态访问 @override AudioTrackInfo? get currentTrack => _stateManager.currentTrack; @override PlaybackContext? get currentContext => _stateManager.currentContext; // 状态持久化 @override Future savePlaybackState() => _stateManager.saveState(); @override Future restorePlaybackState() async { try { AppLogger.debug('开始恢复播放状态'); final state = await _stateManager.loadState(); if (state == null) { AppLogger.debug('没有可恢复的播放状态'); return; } AppLogger.debug('已加载保存的状态: workId=${state.work.id}'); AppLogger.debug('播放列表信息: 长度=${state.playlist.length}, 索引=${state.currentIndex}'); if (state.playlist.isEmpty) { AppLogger.debug('保存的播放列表为空,跳过恢复'); return; } final context = PlaybackContext( work: state.work, files: state.files, currentFile: state.currentFile, playMode: state.playMode, ); try { await _playbackController.setPlaybackContext( context, initialPosition: Duration(milliseconds: state.position), ); AppLogger.debug('播放状态恢复成功'); } catch (e) { AppLogger.error('设置播放上下文失败,跳过状态恢复', e); } } catch (e, stack) { AudioErrorHandler.handleError( AudioErrorType.init, '恢复播放状态', e, stack, ); rethrow; } } @override Future dispose() async { _player.dispose(); _notificationService.dispose(); } } ================================================ FILE: lib/core/audio/audio_service.dart ================================================ import 'package:just_audio/just_audio.dart'; abstract class AudioService { Future play(String url); Future pause(); Future resume(); Future stop(); Future dispose(); Stream get playerState; } ================================================ FILE: lib/core/audio/cache/audio_cache_manager.dart ================================================ import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:crypto/crypto.dart'; import 'dart:convert'; import 'package:just_audio/just_audio.dart'; import 'package:asmrapp/utils/logger.dart'; /// 音频缓存管理器 /// 负责管理音频文件的缓存,对外隐藏具体的缓存实现 class AudioCacheManager { static const int _maxCacheSize = 1024 * 1024 * 1024; // 总缓存限制 1024MB static const Duration _cacheExpiration = Duration(days: 30); /// 创建音频源 /// 内部处理缓存逻辑,对外只返回 AudioSource static Future createAudioSource(String url) async { try { final cacheFile = await _getCacheFile(url); final fileName = _generateFileName(url); AppLogger.debug('准备创建音频源 - URL: $url, 缓存文件名: $fileName'); // 检查缓存文件是否存在且有效 final isValid = await _isCacheValid(cacheFile, fileName); if (isValid) { AppLogger.debug('[$fileName] 使用已有缓存文件'); return _createCachingSource(url, cacheFile); } AppLogger.debug('[$fileName] 创建新的缓存源'); return _createCachingSource(url, cacheFile); } catch (e) { AppLogger.error('创建缓存音频源失败,使用非缓存源', e); return ProgressiveAudioSource(Uri.parse(url)); } } /// 清理过期和超量的缓存 static Future cleanCache() async { try { final cacheDir = await _getCacheDir(); final files = await cacheDir.list().toList(); // 按修改时间排序 files.sort((a, b) { return a.statSync().modified.compareTo(b.statSync().modified); }); var totalSize = 0; for (var file in files) { if (file is File) { final stat = await file.stat(); // 检查是否过期 if (DateTime.now().difference(stat.modified) > _cacheExpiration) { await file.delete(); continue; } totalSize += stat.size; // 如果总大小超过限制,删除最旧的文件 if (totalSize > _maxCacheSize) { await file.delete(); } } } } catch (e) { AppLogger.error('清理缓存失败', e); } } /// 获取缓存大小 static Future getCacheSize() async { try { final cacheDir = await _getCacheDir(); final files = await cacheDir.list().toList(); var totalSize = 0; for (var file in files) { if (file is File) { totalSize += (await file.stat()).size; } } return totalSize; } catch (e) { AppLogger.error('获取缓存大小失败', e); return 0; } } // 私有方法 /// 创建缓存音频源 static AudioSource _createCachingSource(String url, File cacheFile) { return LockCachingAudioSource( Uri.parse(url), cacheFile: cacheFile ); } /// 检查缓存是否有效 static Future _isCacheValid(File cacheFile, String fileName) async { final exists = await cacheFile.exists(); if (!exists) { AppLogger.debug('[$fileName] 缓存验证: 文件不存在'); return false; } try { final stat = await cacheFile.stat(); final size = stat.size; final age = DateTime.now().difference(stat.modified); AppLogger.debug('[$fileName] 缓存验证: 大小=${size}bytes, 年龄=$age'); // 移除单个文件大小检查,只保留过期检查 if (age > _cacheExpiration) { AppLogger.debug('[$fileName] 缓存无效: 文件过期 ($age > $_cacheExpiration)'); await cacheFile.delete(); return false; } AppLogger.debug('[$fileName] 缓存验证: 有效'); return true; } catch (e) { AppLogger.error('[$fileName] 检查缓存有效性失败', e); return false; } } /// 获取缓存文件 static Future _getCacheFile(String url) async { final cacheDir = await _getCacheDir(); final fileName = _generateFileName(url); return File('${cacheDir.path}/$fileName'); } /// 生成缓存文件名 static String _generateFileName(String url) { final bytes = utf8.encode(url); final digest = md5.convert(bytes); return digest.toString(); } /// 获取缓存目录 static Future _getCacheDir() async { final cacheDir = await getTemporaryDirectory(); final audioCacheDir = Directory('${cacheDir.path}/audio_cache'); if (!await audioCacheDir.exists()) { await audioCacheDir.create(recursive: true); } return audioCacheDir; } } ================================================ FILE: lib/core/audio/controllers/playback_controller.dart ================================================ import 'package:asmrapp/utils/logger.dart'; import 'package:just_audio/just_audio.dart'; import '../models/playback_context.dart'; import '../state/playback_state_manager.dart'; import '../utils/playlist_builder.dart'; import '../utils/audio_error_handler.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/data/models/works/work.dart'; class PlaybackController { final AudioPlayer _player; final PlaybackStateManager _stateManager; final ConcatenatingAudioSource _playlist; PlaybackController({ required AudioPlayer player, required PlaybackStateManager stateManager, required ConcatenatingAudioSource playlist, }) : _player = player, _stateManager = stateManager, _playlist = playlist; // 基础播放控制 Future play() => _player.play(); Future pause() => _player.pause(); Future stop() => _player.stop(); Future seek(Duration position, {int? index}) => _player.seek(position, index: index); // 播放列表控制 Future next() async { try { AppLogger.debug('尝试切换下一曲'); if (_stateManager.currentContext == null) { AppLogger.debug('当前上下文为空,无法切换下一曲'); return; } if (_player.hasNext) { AppLogger.debug('执行切换到下一曲'); await _player.seekToNext(); } else { AppLogger.debug('没有下一曲可切换'); } } catch (e, stack) { AppLogger.error('切换下一曲失败', e, stack); AudioErrorHandler.handleError( AudioErrorType.playback, '切换下一曲', e, stack, ); } } Future previous() async { try { AppLogger.debug('尝试切换上一曲'); if (_stateManager.currentContext == null) { AppLogger.debug('当前上下文为空,无法切换上一曲'); return; } if (_player.hasPrevious) { final previousFile = _stateManager.currentContext!.getPreviousFile(); AppLogger.debug('获取到上一个文件: ${previousFile?.title}'); if (previousFile != null) { _updateTrackAndContext( previousFile, _stateManager.currentContext!.work ); AppLogger.debug('执行切换到上一曲'); await _player.seekToPrevious(); } } else { AppLogger.debug('没有上一曲可切换'); } } catch (e, stack) { AppLogger.error('切换上一曲失败', e, stack); AudioErrorHandler.handleError( AudioErrorType.playback, '切换上一曲', e, stack, ); } } // 播放上下文设置 Future setPlaybackContext(PlaybackContext context, {Duration? initialPosition}) async { try { AppLogger.debug('准备设置播放上下文: workId=${context.work.id}, file=${context.currentFile.title}'); AppLogger.debug('播放列表状态: 长度=${context.playlist.length}, 当前索引=${context.currentIndex}'); // 验证上下文 try { context.validate(); } catch (e) { AppLogger.error('播放上下文验证失败', e); rethrow; } // 1. 先停止当前播放 AppLogger.debug('停止当前播放'); await _player.stop(); // 2. 等待播放器就绪 AppLogger.debug('暂停播放器'); await _player.pause(); // 3. 更新上下文 AppLogger.debug('更新播放上下文'); _stateManager.updateContext(context); // 4. 设置新的播放源 AppLogger.debug('设置播放源: 初始位置=${initialPosition?.inMilliseconds}ms'); try { await PlaylistBuilder.setPlaylistSource( player: _player, playlist: _playlist, files: context.playlist, initialIndex: context.currentIndex, initialPosition: initialPosition ?? Duration.zero, ); } catch (e, stack) { AppLogger.error('设置播放源失败', e, stack); rethrow; } // 5. 等待播放器准备完成 // 删掉,会导致播放器索引回到0 // AppLogger.debug('等待播放器加载'); // await _player.load(); // 6. 更新轨道信息 AppLogger.debug('更新轨道信息'); _updateTrackAndContext(context.currentFile, context.work); AppLogger.debug('播放上下文设置完成'); } catch (e, stack) { AppLogger.error('设置播放上下文失败', e, stack); AudioErrorHandler.handleError( AudioErrorType.context, '设置播放上下文', e, stack, ); rethrow; } } // 私有辅助方法 void _updateTrackAndContext(Child file, Work work) { AppLogger.debug('更新轨道和上下文: file=${file.title}'); _stateManager.updateTrackAndContext(file, work); } } ================================================ FILE: lib/core/audio/events/playback_event.dart ================================================ import 'package:just_audio/just_audio.dart'; import '../models/audio_track_info.dart'; import '../models/playback_context.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/data/models/works/work.dart'; /// 播放事件基类 abstract class PlaybackEvent {} /// 播放状态事件 class PlaybackStateEvent extends PlaybackEvent { final PlayerState state; final Duration position; final Duration? duration; PlaybackStateEvent(this.state, this.position, this.duration); } /// 播放上下文事件 class PlaybackContextEvent extends PlaybackEvent { final PlaybackContext context; PlaybackContextEvent(this.context); } /// 音轨变更事件 class TrackChangeEvent extends PlaybackEvent { final AudioTrackInfo track; final Child file; final Work work; TrackChangeEvent(this.track, this.file, this.work); } /// 播放错误事件 class PlaybackErrorEvent extends PlaybackEvent { final String operation; final dynamic error; final StackTrace? stackTrace; PlaybackErrorEvent(this.operation, this.error, [this.stackTrace]); } /// 播放完成事件 class PlaybackCompletedEvent extends PlaybackEvent { final PlaybackContext context; PlaybackCompletedEvent(this.context); } /// 播放进度事件 class PlaybackProgressEvent extends PlaybackEvent { final Duration position; final Duration? bufferedPosition; PlaybackProgressEvent(this.position, this.bufferedPosition); } /// 添加初始状态相关事件 class RequestInitialStateEvent extends PlaybackEvent {} class InitialStateEvent extends PlaybackEvent { final AudioTrackInfo? track; final PlaybackContext? context; InitialStateEvent(this.track, this.context); } ================================================ FILE: lib/core/audio/events/playback_event_hub.dart ================================================ import 'package:rxdart/rxdart.dart'; import './playback_event.dart'; class PlaybackEventHub { // 统一的事件流,处理所有类型的事件 final _eventSubject = PublishSubject(); // 分类后的特定事件流 late final Stream playbackState = _eventSubject .whereType() .distinct(); late final Stream trackChange = _eventSubject .whereType(); late final Stream contextChange = _eventSubject .whereType(); late final Stream playbackProgress = _eventSubject .whereType() .distinct((prev, next) => prev.position == next.position); late final Stream errors = _eventSubject .whereType(); // 添加新的事件流 late final Stream initialState = _eventSubject .whereType(); late final Stream requestInitialState = _eventSubject .whereType(); // 发送事件 void emit(PlaybackEvent event) => _eventSubject.add(event); // 资源释放 void dispose() => _eventSubject.close(); } ================================================ FILE: lib/core/audio/i_audio_player_service.dart ================================================ import './models/audio_track_info.dart'; import './models/playback_context.dart'; abstract class IAudioPlayerService { // 基础播放控制 Future pause(); Future resume(); Future stop(); Future seek(Duration position); Future previous(); Future next(); Future dispose(); // 上下文管理 Future playWithContext(PlaybackContext context); // 状态访问 AudioTrackInfo? get currentTrack; PlaybackContext? get currentContext; // 状态持久化 Future savePlaybackState(); Future restorePlaybackState(); } ================================================ FILE: lib/core/audio/models/audio_track_info.dart ================================================ class AudioTrackInfo { final String title; final String artist; final String coverUrl; final String url; final Duration? duration; AudioTrackInfo({ required this.title, required this.artist, required this.coverUrl, required this.url, this.duration, }); } ================================================ FILE: lib/core/audio/models/file_path.dart ================================================ import 'package:asmrapp/data/models/files/files.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/utils/logger.dart'; /// 文件路径工具类 /// 用于在文件树中定位文件和获取同级文件 class FilePath { static const separator = '/'; /// 获取文件的完整路径 /// 返回类似 /folder1/folder2/file.mp3 的路径 static String? getPath(Child targetFile, Files root) { AppLogger.debug('开始查找文件路径: ${targetFile.title}'); final segments = _findPathSegments(root.children, targetFile); if (segments == null) { AppLogger.debug('未找到文件路径'); return null; } final path = separator + segments.join(separator); AppLogger.debug('找到文件路径: $path'); return path; } /// 递归查找文件路径段 static List? _findPathSegments(List? children, Child targetFile, [List currentPath = const []]) { if (children == null) return null; for (final child in children) { if (child.title == targetFile.title && child.mediaDownloadUrl == targetFile.mediaDownloadUrl && child.type == targetFile.type && child.size == targetFile.size) { // size 作为额外验证 return [...currentPath, child.title!]; } if (child.type == 'folder' && child.children != null) { final result = _findPathSegments( child.children, targetFile, [...currentPath, child.title!] ); if (result != null) return result; } } return null; } /// 获取同级文件列表 /// 返回与目标文件在同一目录下的所有文件 static List getSiblings(Child targetFile, Files root) { AppLogger.debug('开始获取同级文件: ${targetFile.title}'); // 获取目标文件的路径 final path = getPath(targetFile, root); if (path == null) { AppLogger.debug('无法获取文件路径,返回空列表'); return []; } // 获取父目录路径 final lastSeparator = path.lastIndexOf(separator); final parentPath = lastSeparator > 0 ? path.substring(0, lastSeparator) : separator; AppLogger.debug('父目录路径: $parentPath'); // 查找父目录内容 List? siblings; if (parentPath == separator) { // 如果是根目录,直接使用 root.children AppLogger.debug('文件位于根目录,使用根目录文件列表'); siblings = root.children; } else { // 否则查找父目录 siblings = _findDirectoryByPath(root.children, parentPath); } if (siblings == null) { AppLogger.debug('未找到父目录内容,返回空列表'); return []; } AppLogger.debug('找到同级文件数量: ${siblings.length}'); return siblings; } /// 根据路径查找目录内容 static List? _findDirectoryByPath(List? children, String path) { if (children == null || path.isEmpty) return null; // 如果是根路径,直接返回 if (path == separator) return children; // 分割路径 final segments = path.split(separator) ..removeWhere((s) => s.isEmpty); List? current = children; // 逐级查找目录 for (final segment in segments) { final nextDir = current?.firstWhere( (child) => child.title == segment && child.type == 'folder', orElse: () => Child(), ); if (nextDir?.title == null) return null; current = nextDir?.children; } return current; } /// 查找第一个包含音频文件的目录路径 /// 返回从根目录到目标目录的完整路径数组 static List? findFirstAudioFolderPath( List? children, { List formats = const ['.mp3', '.wav'], }) { if (children == null) return null; List? audioFolderPath; void findPath(Child folder, List currentPath) { if (audioFolderPath != null) return; if (folder.children != null) { // 首先检查当前��录是否直接包含音频文件 bool hasDirectAudio = folder.children!.any((child) { if (child.type != 'folder') { final fileName = child.title?.toLowerCase() ?? ''; return formats.any((format) => fileName.endsWith(format)); } return false; }); // 如果当前目录包含音频文件,记录完整路径 if (hasDirectAudio) { audioFolderPath = currentPath; return; } // 如果当前目录没有音频文件,递归检查子目录 for (final child in folder.children!) { if (child.type == 'folder') { List newPath = List.from(currentPath)..add(child.title ?? ''); findPath(child, newPath); } } } } // 遍历根目录下的所有文件夹 for (final child in children) { if (child.type == 'folder') { findPath(child, [child.title ?? '']); if (audioFolderPath != null) break; } } return audioFolderPath; } /// 检查路径是否包含指定的目录名 /// 用于判断某个目录是否在音频文件夹的路径上 static bool isInPath(List? path, String? folderName) { if (path == null || folderName == null) return false; return path.contains(folderName); } } ================================================ FILE: lib/core/audio/models/play_mode.dart ================================================ enum PlayMode { single, // 单曲循环 loop, // 列表循环 sequence, // 顺序播放 } ================================================ FILE: lib/core/audio/models/playback_context.dart ================================================ import 'package:asmrapp/core/audio/utils/audio_error_handler.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/data/models/files/files.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:asmrapp/core/audio/models/play_mode.dart'; import 'package:asmrapp/core/audio/models/file_path.dart'; class PlaybackContext { final Work work; final Files files; final Child currentFile; final List playlist; final int currentIndex; final PlayMode playMode; void validate() { if (playlist.isEmpty) { throw AudioError( AudioErrorType.state, '无效的播放列表状态:播放列表为空', ); } if (currentIndex < 0 || currentIndex >= playlist.length) { throw AudioError( AudioErrorType.state, '无效的播放列表索引:$currentIndex,列表长度:${playlist.length}', ); } if (!playlist.contains(currentFile)) { throw AudioError( AudioErrorType.state, '当前文件不在播放列表中', ); } } // 私有构造函数 const PlaybackContext._({ required this.work, required this.files, required this.currentFile, required this.playlist, required this.currentIndex, this.playMode = PlayMode.sequence, }); // 公开的工厂构造函数,只需要基本参数 factory PlaybackContext({ required Work work, required Files files, required Child currentFile, PlayMode playMode = PlayMode.sequence, }) { final playlist = _getPlaylistFromSameDirectory(currentFile, files); final currentIndex = playlist.indexWhere((file) => file.title == currentFile.title); return PlaybackContext._( work: work, files: files, currentFile: currentFile, playlist: playlist, currentIndex: currentIndex, playMode: playMode, ); } // 获取同级文件列表 static List _getPlaylistFromSameDirectory(Child currentFile, Files files) { // AppLogger.debug('开始获取播放列表...'); // AppLogger.debug('当前文件: ${currentFile.title}'); // AppLogger.debug('当前文件类型: ${currentFile.type}'); // 获取当前文件的扩展名 final extension = currentFile.title?.split('.').last.toLowerCase(); // AppLogger.debug('当前文件扩展名: $extension'); if (extension != 'mp3' && extension != 'wav') { AppLogger.debug('不支持的文件类型: $extension'); return []; } // 使用 FilePath 获取同级文件 final siblings = FilePath.getSiblings(currentFile, files); // 过滤出相同扩展名的文件 final playlist = siblings.where((file) => file.title?.toLowerCase().endsWith('.$extension') ?? false ).toList(); // AppLogger.debug('找到 ${playlist.length} 个可播放文件:'); // for (var file in playlist) { // AppLogger.debug('- [${file.type}] ${file.title} (URL: ${file.mediaDownloadUrl != null ? '有' : '无'})'); // } return playlist; } // 便捷方法:检查是否有下一曲 bool get hasNext => currentIndex < playlist.length - 1; // 便捷方法:检查是否有上一曲 bool get hasPrevious => currentIndex > 0; // 获取下一曲(考虑播放模式) Child? getNextFile() { if (playlist.isEmpty) return null; switch (playMode) { case PlayMode.single: return currentFile; // 单曲循环返回当前文件 case PlayMode.loop: // 列表循环:最后一首返回第一首,否则返回下一首 return hasNext ? playlist[currentIndex + 1] : playlist[0]; case PlayMode.sequence: // 顺序播放:有下一首则返回,否则返回null return hasNext ? playlist[currentIndex + 1] : null; } } // 获取上一曲 Child? getPreviousFile() { if (playlist.isEmpty) return null; switch (playMode) { case PlayMode.single: return currentFile; case PlayMode.loop: // 列表循环:第一首返回最后一首,否则返回上一首 return hasPrevious ? playlist[currentIndex - 1] : playlist[playlist.length - 1]; case PlayMode.sequence: // 顺序播放:有上一首则返回,否则返回null return hasPrevious ? playlist[currentIndex - 1] : null; } } // 这两个方法 copy 的设计思路是遵循了"不可变对象"模式, // 通过创建新的实例而不是修改现有实例来更新状态。这种模式有以下好处: // 状态可预测 // 线程安全 // 便于调试 // 符合函数式编程思想 // 创建新的上下文(用于切换文件) PlaybackContext copyWithFile(Child newFile) { return PlaybackContext( work: work, files: files, currentFile: newFile, playMode: playMode, ); } // 创建新的上下文(用于切换播放模式) PlaybackContext copyWithMode(PlayMode newMode) { return PlaybackContext( work: work, files: files, currentFile: currentFile, playMode: newMode, ); } // 便捷方法:获取可播放文件列表 List getPlayableFiles() { if (files.children == null) return []; return files.children!.where((file) => file.mediaDownloadUrl != null && file.type?.toLowerCase() != 'vtt' ).toList(); } // 工具方法:获取文件名(不含扩展名) String? _getBaseName(String? filename) { if (filename == null) return null; return filename.replaceAll(RegExp(r'\.[^.]+$'), ''); } } ================================================ FILE: lib/core/audio/models/subtitle.dart ================================================ import 'dart:math' as math; enum SubtitleState { current, // 当前播放的字幕 waiting, // 即将播放的字幕 passed // 已经播放过的字幕 } class Subtitle { final Duration start; final Duration end; final String text; final int index; const Subtitle({ required this.start, required this.end, required this.text, required this.index, }); Subtitle? getNext(SubtitleList list) { if (index < list.subtitles.length - 1) { return list.subtitles[index + 1]; } return null; } Subtitle? getPrevious(SubtitleList list) { if (index > 0) { return list.subtitles[index - 1]; } return null; } @override String toString() => '$start --> $end: $text'; } class SubtitleList { final List subtitles; int _currentIndex = -1; SubtitleList(List subtitles) : subtitles = subtitles.asMap().entries.map( (entry) => Subtitle( start: entry.value.start, end: entry.value.end, text: entry.value.text, index: entry.key, ) ).toList(); SubtitleWithState? getCurrentSubtitle(Duration position) { if (subtitles.isEmpty) return null; // 如果位置在第一个字幕之前,仍然返回第一个字幕作为当前字幕 if (position < subtitles.first.start) { return SubtitleWithState(subtitles.first, SubtitleState.current); } // 如果位置在最后一个字幕之后 if (position > subtitles.last.end) { return SubtitleWithState(subtitles.last, SubtitleState.passed); } // 查找当前时间点对应的字幕 for (int i = 0; i < subtitles.length; i++) { final subtitle = subtitles[i]; // 如果在当前字幕的时间范围内 if (position >= subtitle.start && position <= subtitle.end) { _currentIndex = i; return SubtitleWithState(subtitle, SubtitleState.current); } // 如果已经超过了当前字幕,但还没到下一个字幕 if (position > subtitle.end && (i == subtitles.length - 1 || position < subtitles[i + 1].start)) { return SubtitleWithState(subtitle, SubtitleState.passed); } } // 正常情况下不会到达这里,因为上面的逻辑已经覆盖了所有情况 // 但为了安全起见,返回第一个字幕 return SubtitleWithState(subtitles.first, SubtitleState.waiting); } List getSubtitlesInRange(int start, int count) { if (start < 0 || start >= subtitles.length) return []; final end = math.min(start + count, subtitles.length); return subtitles.sublist(start, end); } (Subtitle?, Subtitle?, Subtitle?) getCurrentContext() { if (_currentIndex == -1) return (null, null, null); final previous = _currentIndex > 0 ? subtitles[_currentIndex - 1] : null; final current = subtitles[_currentIndex]; final next = _currentIndex < subtitles.length - 1 ? subtitles[_currentIndex + 1] : null; return (previous, current, next); } static SubtitleList parse(String vttContent) { final lines = vttContent.split('\n'); final subtitles = []; int i = 0; while (i < lines.length && !lines[i].contains('-->')) { i++; } while (i < lines.length) { final line = lines[i].trim(); if (line.contains('-->')) { final times = line.split('-->'); if (times.length == 2) { final start = _parseTimestamp(times[0].trim()); final end = _parseTimestamp(times[1].trim()); i++; String text = ''; while (i < lines.length && lines[i].trim().isNotEmpty) { if (text.isNotEmpty) text += '\n'; text += lines[i].trim(); i++; } if (start != null && end != null && text.isNotEmpty) { subtitles.add(Subtitle( start: start, end: end, text: text, index: subtitles.length, )); } } } i++; } return SubtitleList(subtitles); } static Duration? _parseTimestamp(String timestamp) { try { final parts = timestamp.split(':'); if (parts.length == 3) { final seconds = parts[2].split('.'); return Duration( hours: int.parse(parts[0]), minutes: int.parse(parts[1]), seconds: int.parse(seconds[0]), milliseconds: seconds.length > 1 ? int.parse(seconds[1].padRight(3, '0')) : 0, ); } } catch (e) { return null; } return null; } } class SubtitleWithState { final Subtitle subtitle; final SubtitleState state; SubtitleWithState(this.subtitle, this.state); } ================================================ FILE: lib/core/audio/notification/audio_notification_service.dart ================================================ import 'package:asmrapp/core/audio/events/playback_event_hub.dart'; import 'package:audio_service/audio_service.dart'; import 'package:just_audio/just_audio.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:rxdart/rxdart.dart'; import '../models/audio_track_info.dart'; import '../audio_player_handler.dart'; class AudioNotificationService { final AudioPlayer _player; final PlaybackEventHub _eventHub; AudioHandler? _audioHandler; final _mediaItem = BehaviorSubject(); AudioNotificationService( this._player, this._eventHub, ); Future init() async { try { _audioHandler = await AudioService.init( builder: () => AudioPlayerHandler(_player, _eventHub), config: const AudioServiceConfig( androidNotificationChannelId: 'com.asmrapp.audio', androidNotificationChannelName: 'ASMR One 播放器', androidNotificationOngoing: true, androidStopForegroundOnPause: true, ), ); _setupEventListeners(); AppLogger.debug('通知栏服务初始化成功'); } catch (e) { AppLogger.error('通知栏服务初始化失败', e); rethrow; } } void _setupEventListeners() { // 监听轨道变更事件来更新媒体信息 _eventHub.trackChange.listen((event) { updateMetadata(event.track); }); } void updateMetadata(AudioTrackInfo trackInfo) { final mediaItem = MediaItem( id: trackInfo.url, title: trackInfo.title, artist: trackInfo.artist, artUri: Uri.parse(trackInfo.coverUrl), duration: trackInfo.duration, ); _mediaItem.add(mediaItem); if (_audioHandler != null) { (_audioHandler as BaseAudioHandler).mediaItem.add(mediaItem); } } Future dispose() async { await _audioHandler?.stop(); await _mediaItem.close(); } } ================================================ FILE: lib/core/audio/state/playback_state_manager.dart ================================================ import 'dart:async'; import 'package:just_audio/just_audio.dart'; import '../models/audio_track_info.dart'; import '../models/playback_context.dart'; import '../utils/audio_error_handler.dart'; import '../utils/track_info_creator.dart'; import 'package:asmrapp/data/models/playback/playback_state.dart'; import '../storage/i_playback_state_repository.dart'; import '../events/playback_event.dart'; import '../events/playback_event_hub.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/data/models/works/work.dart'; class PlaybackStateManager { final AudioPlayer _player; final PlaybackEventHub _eventHub; final IPlaybackStateRepository _stateRepository; AudioTrackInfo? _currentTrack; PlaybackContext? _currentContext; final List _subscriptions = []; PlaybackStateManager({ required AudioPlayer player, required PlaybackEventHub eventHub, required IPlaybackStateRepository stateRepository, }) : _player = player, _eventHub = eventHub, _stateRepository = stateRepository; // 初始化状态监听 void initStateListeners() { // 监听播放器索引变化 _player.currentIndexStream.listen((index) { if (index != null && _currentContext != null) { final newFile = _currentContext!.playlist[index]; updateTrackAndContext(newFile, _currentContext!.work); } }); // 直接监听 AudioPlayer 的原始流 _player.playerStateStream.listen((state) async { final position = _player.position; final duration = _player.duration; // 转换并发送到 EventHub _eventHub.emit(PlaybackStateEvent(state, position, duration)); if (state.processingState == ProcessingState.completed) { _onPlaybackCompleted(); } saveState(); }); _player.positionStream.listen((position) { _eventHub.emit(PlaybackProgressEvent( position, _player.bufferedPosition )); }); } // 状态更新方法 void updateContext(PlaybackContext? context) { _currentContext = context; if (context != null) { _eventHub.emit(PlaybackContextEvent(context)); } } void updateTrackInfo(AudioTrackInfo track) { _currentTrack = track; _eventHub.emit(TrackChangeEvent(track, _currentContext!.currentFile, _currentContext!.work)); } void updateTrackAndContext(Child file, Work work) { if (_currentContext != null) { final newContext = _currentContext!.copyWithFile(file); updateContext(newContext); } final trackInfo = TrackInfoCreator.createFromFile(file, work); updateTrackInfo(trackInfo); } void _onPlaybackCompleted() { if (_currentContext == null) return; _eventHub.emit(PlaybackCompletedEvent(_currentContext!)); } // 状态访问 AudioTrackInfo? get currentTrack => _currentTrack; PlaybackContext? get currentContext => _currentContext; void clearState() { _currentTrack = null; _currentContext = null; updateContext(null); } // 状态持久化 Future saveState() async { if (_currentContext == null) return; try { final state = PlaybackState( work: _currentContext!.work, files: _currentContext!.files, currentFile: _currentContext!.currentFile, playlist: _currentContext!.playlist, currentIndex: _currentContext!.currentIndex, playMode: _currentContext!.playMode, position: (_player.position).inMilliseconds, timestamp: DateTime.now().toIso8601String(), ); await _stateRepository.saveState(state); } catch (e, stack) { AudioErrorHandler.handleError( AudioErrorType.state, '保存播放状态', e, stack, ); } } Future loadState() async { try { return await _stateRepository.loadState(); } catch (e, stack) { AudioErrorHandler.handleError( AudioErrorType.state, '加载播放状态', e, stack, ); return null; } } void _setupEventListeners() { // 处理初始状态请求 _subscriptions.add( _eventHub.requestInitialState.listen((_) { _eventHub.emit(InitialStateEvent( _currentTrack, _currentContext )); }), ); } void dispose() { for (var subscription in _subscriptions) { subscription.cancel(); } _subscriptions.clear(); } } ================================================ FILE: lib/core/audio/storage/i_playback_state_repository.dart ================================================ import 'package:asmrapp/data/models/playback/playback_state.dart'; abstract class IPlaybackStateRepository { Future saveState(PlaybackState state); Future loadState(); } ================================================ FILE: lib/core/audio/storage/playback_state_repository.dart ================================================ import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:asmrapp/data/models/playback/playback_state.dart'; import 'i_playback_state_repository.dart'; class PlaybackStateRepository implements IPlaybackStateRepository { static const _key = 'last_playback_state'; final SharedPreferences _prefs; PlaybackStateRepository(this._prefs); @override Future saveState(PlaybackState state) async { try { final json = state.toJson(); final data = jsonEncode(json); await _prefs.setString(_key, data); AppLogger.debug('播放状态已保存'); } catch (e) { AppLogger.error('保存播放状态失败', e); rethrow; } } @override Future loadState() async { try { final data = _prefs.getString(_key); if (data == null) { AppLogger.debug('没有找到保存的播放状态'); return null; } final json = jsonDecode(data) as Map; final state = PlaybackState.fromJson(json); AppLogger.debug('播放状态已加载'); return state; } catch (e) { AppLogger.error('加载播放状态失败', e); return null; } } } ================================================ FILE: lib/core/audio/utils/audio_error_handler.dart ================================================ import 'package:asmrapp/utils/logger.dart'; enum AudioErrorType { playback, // 播放错误 playlist, // 播放列表错误 state, // 状态错误 context, // 上下文错误 init, // 初始化错误 } class AudioError implements Exception { final AudioErrorType type; final String message; final dynamic originalError; AudioError(this.type, this.message, [this.originalError]); @override String toString() => '$message${originalError != null ? ': $originalError' : ''}'; } class AudioErrorHandler { static void handleError( AudioErrorType type, String operation, dynamic error, [ StackTrace? stack, ]) { final message = _getErrorMessage(type, operation); AppLogger.error(message, error, stack); } static Never throwError( AudioErrorType type, String operation, dynamic error, ) { final message = _getErrorMessage(type, operation); throw AudioError(type, message, error); } static String _getErrorMessage(AudioErrorType type, String operation) { switch (type) { case AudioErrorType.playback: return '播放操作失败: $operation'; case AudioErrorType.playlist: return '播放列表操作失败: $operation'; case AudioErrorType.state: return '状态操作失败: $operation'; case AudioErrorType.context: return '上下文操作失败: $operation'; case AudioErrorType.init: return '初始化失败: $operation'; } } } ================================================ FILE: lib/core/audio/utils/playlist_builder.dart ================================================ import 'package:just_audio/just_audio.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/core/audio/cache/audio_cache_manager.dart'; class PlaylistBuilder { static Future> buildAudioSources(List files) async { return await Future.wait( files.map((file) async { return AudioCacheManager.createAudioSource(file.mediaDownloadUrl!); }) ); } static Future updatePlaylist( ConcatenatingAudioSource playlist, List sources, ) async { await playlist.clear(); await playlist.addAll(sources); } static Future setPlaylistSource({ required AudioPlayer player, required ConcatenatingAudioSource playlist, required List files, required int initialIndex, required Duration initialPosition, }) async { final sources = await buildAudioSources(files); await updatePlaylist(playlist, sources); await player.setAudioSource( playlist, initialIndex: initialIndex, initialPosition: initialPosition, ); } } ================================================ FILE: lib/core/audio/utils/track_info_creator.dart ================================================ import 'package:asmrapp/core/audio/models/audio_track_info.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/data/models/works/work.dart'; class TrackInfoCreator { static AudioTrackInfo createTrackInfo({ required String title, required String? artistName, required String? coverUrl, required String url, }) { return AudioTrackInfo( title: title, artist: artistName ?? '', coverUrl: coverUrl ?? '', url: url, ); } static AudioTrackInfo createFromFile(Child file, Work work) { return createTrackInfo( title: file.title ?? '', artistName: work.circle?.name, coverUrl: work.mainCoverUrl, url: file.mediaDownloadUrl!, ); } } ================================================ FILE: lib/core/cache/recommendation_cache_manager.dart ================================================ import 'dart:collection'; import 'package:asmrapp/data/services/api_service.dart'; import 'package:asmrapp/utils/logger.dart'; class RecommendationCacheManager { // 单例模式 static final RecommendationCacheManager _instance = RecommendationCacheManager._internal(); factory RecommendationCacheManager() => _instance; RecommendationCacheManager._internal(); // 使用 LinkedHashMap 便于按访问顺序管理缓存 final _cache = LinkedHashMap(); // 缓存配置 static const int _maxCacheSize = 1000; // 最大缓存条目数 static const Duration _cacheDuration = Duration(hours: 24); // 缓存有效期 /// 生成缓存键 String _generateKey(String itemId, int page, int subtitle) { return '$itemId-$page-$subtitle'; } /// 获取缓存数据 WorksResponse? get(String itemId, int page, int subtitle) { final key = _generateKey(itemId, page, subtitle); final item = _cache[key]; if (item == null) { return null; } // 检查是否过期 if (item.isExpired) { _cache.remove(key); AppLogger.debug('缓存已过期: $key'); return null; } AppLogger.debug('命中缓存: $key'); return item.data; } /// 存储缓存数据 void set(String itemId, int page, int subtitle, WorksResponse data) { final key = _generateKey(itemId, page, subtitle); // 检查缓存大小,如果达到上限则移除最早的条目 if (_cache.length >= _maxCacheSize) { _cache.remove(_cache.keys.first); } _cache[key] = _CacheItem(data); AppLogger.debug('添加缓存: $key'); } /// 清除所有缓存 void clear() { _cache.clear(); AppLogger.debug('清除所有推荐缓存'); } /// 移除指定作品的缓存 void remove(String itemId) { _cache.removeWhere((key, _) => key.startsWith('$itemId-')); AppLogger.debug('移除作品缓存: $itemId'); } } /// 缓存条目包装类 class _CacheItem { final WorksResponse data; final DateTime timestamp; _CacheItem(this.data) : timestamp = DateTime.now(); bool get isExpired => DateTime.now().difference(timestamp) > RecommendationCacheManager._cacheDuration; } ================================================ FILE: lib/core/di/service_locator.dart ================================================ import 'dart:io'; import 'package:asmrapp/core/platform/dummy_lyric_overlay_controller.dart'; import 'package:get_it/get_it.dart'; import '../audio/i_audio_player_service.dart'; import '../audio/audio_player_service.dart'; import '../../data/services/api_service.dart'; import '../../presentation/viewmodels/player_viewmodel.dart'; import '../../data/services/auth_service.dart'; import '../../presentation/viewmodels/auth_viewmodel.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../data/repositories/auth_repository.dart'; import '../subtitle/i_subtitle_service.dart'; import '../subtitle/subtitle_service.dart'; import '../subtitle/subtitle_loader.dart'; import '../../core/audio/storage/i_playback_state_repository.dart'; import '../../core/audio/storage/playback_state_repository.dart'; import '../audio/events/playback_event_hub.dart'; import '../../core/theme/theme_controller.dart'; import '../../core/platform/i_lyric_overlay_controller.dart'; import '../../core/platform/lyric_overlay_controller.dart'; import '../../core/platform/lyric_overlay_manager.dart'; import '../../core/platform/wakelock_controller.dart'; final getIt = GetIt.instance; Future setupServiceLocator() async { final prefs = await SharedPreferences.getInstance(); // 注册 EventHub getIt.registerLazySingleton(() => PlaybackEventHub()); // 注册 SharedPreferences 实例 getIt.registerSingleton(prefs); // 注册 PlaybackStateRepository getIt.registerLazySingleton( () => PlaybackStateRepository(getIt()), ); // 核心服务 getIt.registerLazySingleton( () => AudioPlayerService( eventHub: getIt(), stateRepository: getIt(), ), ); // 注册 PlayerViewModel getIt.registerLazySingleton( () => PlayerViewModel( audioService: getIt(), eventHub: getIt(), subtitleService: getIt(), ), ); // API 服务 getIt.registerLazySingleton( () => ApiService(), ); // 添加 AuthService 注册 getIt.registerLazySingleton( () => AuthService(), ); // 添加 AuthRepository 注册 getIt.registerLazySingleton( () => AuthRepository(prefs), ); // 修改 AuthViewModel 注册 getIt.registerSingleton( AuthViewModel( authService: getIt(), authRepository: getIt(), ), ); // 等待 AuthViewModel 完成初始化 await getIt().loadSavedAuth(); // 添加字幕服务注册 getIt.registerLazySingleton( () => SubtitleService(), ); await setupSubtitleServices(); // 注册主题控制器 getIt.registerLazySingleton( () => ThemeController(prefs), ); // 注册 WakeLockController getIt.registerLazySingleton(() => WakeLockController(prefs)); } Future setupSubtitleServices() async { getIt.registerLazySingleton(() => SubtitleLoader()); if (Platform.isAndroid) { getIt.registerLazySingleton(() => LyricOverlayController()); } else { getIt.registerLazySingleton(() => DummyLyricOverlayController()); } getIt.registerLazySingleton(() => LyricOverlayManager( controller: getIt(), subtitleService: getIt(), )); // 初始化悬浮窗管理器 await getIt().initialize(); } ================================================ FILE: lib/core/platform/dummy_lyric_overlay_controller.dart ================================================ import 'package:asmrapp/utils/logger.dart'; import 'i_lyric_overlay_controller.dart'; class DummyLyricOverlayController implements ILyricOverlayController { static const _tag = 'LyricOverlay'; @override Future initialize() async { } @override Future show() async { } @override Future hide() async { } @override Future updateLyric(String? text) async { } @override Future checkPermission() async { return true; } @override Future requestPermission() async { AppLogger.debug('[$_tag] 请求权限'); return true; } @override Future dispose() async { } @override Future isShowing() async { return false; } } ================================================ FILE: lib/core/platform/i_lyric_overlay_controller.dart ================================================ abstract class ILyricOverlayController { /// 初始化悬浮窗 Future initialize(); /// 显示悬浮窗 Future show(); /// 隐藏悬浮窗 Future hide(); /// 更新歌词内容 Future updateLyric(String? text); /// 检查悬浮窗权限 Future checkPermission(); /// 请求悬浮窗权限 Future requestPermission(); /// 释放资源 Future dispose(); /// 获取悬浮窗当前显示状态 Future isShowing(); } ================================================ FILE: lib/core/platform/lyric_overlay_controller.dart ================================================ import 'package:flutter/services.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:permission_handler/permission_handler.dart'; import 'i_lyric_overlay_controller.dart'; class LyricOverlayController implements ILyricOverlayController { static const _tag = 'LyricOverlay'; static const _channel = MethodChannel('one.asmr.yuro/lyric_overlay'); @override Future initialize() async { try { AppLogger.debug('[$_tag] 初始化'); await _channel.invokeMethod('initialize'); } catch (e) { AppLogger.error('[$_tag] 初始化失败', e); // 这里我们不抛出异常,而是静默失败 // 因为这个错误不应该影响应用的主要功能 } } @override Future show() async { AppLogger.debug('[$_tag] 显示悬浮窗'); await _channel.invokeMethod('show'); } @override Future hide() async { AppLogger.debug('[$_tag] 隐藏悬浮窗'); await _channel.invokeMethod('hide'); } @override Future updateLyric(String? text) async { AppLogger.debug('[$_tag] 更新歌词: ${text ?? '<空>'}'); await _channel.invokeMethod('updateLyric', {'text': text}); } @override Future checkPermission() async { AppLogger.debug('[$_tag] 检查权限'); return await Permission.systemAlertWindow.isGranted; } @override Future requestPermission() async { AppLogger.debug('[$_tag] 请求权限'); final status = await Permission.systemAlertWindow.request(); return status.isGranted; } @override Future dispose() async { AppLogger.debug('[$_tag] 释放资源'); await _channel.invokeMethod('dispose'); } @override Future isShowing() async { final result = await _channel.invokeMethod('isShowing') ?? false; return result; } } ================================================ FILE: lib/core/platform/lyric_overlay_manager.dart ================================================ import 'dart:async'; import 'package:asmrapp/core/platform/i_lyric_overlay_controller.dart'; import 'package:asmrapp/core/subtitle/i_subtitle_service.dart'; import 'package:flutter/material.dart'; class LyricOverlayManager { final ILyricOverlayController _controller; final ISubtitleService _subtitleService; StreamSubscription? _subscription; bool _isShowing = false; LyricOverlayManager({ required ILyricOverlayController controller, required ISubtitleService subtitleService, }) : _controller = controller, _subtitleService = subtitleService; Future initialize() async { await _controller.initialize(); _subscription = _subtitleService.currentSubtitleStream.listen((subtitle) { if (_isShowing) { _controller.updateLyric(subtitle?.text); } }); _isShowing = await _controller.isShowing(); if (_isShowing) { await show(); } } Future dispose() async { await _subscription?.cancel(); await _controller.dispose(); } Future checkPermission() async { return await _controller.checkPermission(); } Future requestPermission() async { return await _controller.requestPermission(); } Future show() async { await _controller.show(); _isShowing = true; final currentSubtitle = _subtitleService.currentSubtitleWithState; if (currentSubtitle != null) { await _controller.updateLyric(currentSubtitle.subtitle.text); } } Future hide() async { await _controller.hide(); _isShowing = false; } bool get isShowing => _isShowing; /// 处理显示悬浮歌词的完整流程 Future showWithPermissionCheck(BuildContext context) async { final hasPermission = await checkPermission(); if (hasPermission) { await show(); return; } if (!context.mounted) return; final shouldRequest = await _showPermissionDialog(context); if (shouldRequest && context.mounted) { final granted = await requestPermission(); if (granted && context.mounted) { await show(); } } } Future _showPermissionDialog(BuildContext context) async { return await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('开启悬浮歌词'), content: const Text('需要悬浮窗权限来显示歌词,是否授予权限?'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text('确定'), ), ], ), ) ?? false; } /// 切换显示/隐藏状态 Future toggle(BuildContext context) async { if (_isShowing) { await hide(); } else { await showWithPermissionCheck(context); } } // 其他控制方法... Future syncState() async { _isShowing = await _controller.isShowing(); } } ================================================ FILE: lib/core/platform/wakelock_controller.dart ================================================ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:asmrapp/utils/logger.dart'; class WakeLockController extends ChangeNotifier { static const _tag = 'WakeLock'; static const _wakeLockKey = 'wakelock_enabled'; final SharedPreferences _prefs; bool _enabled = false; WakeLockController(this._prefs) { _loadState(); } bool get enabled => _enabled; Future _loadState() async { try { _enabled = _prefs.getBool(_wakeLockKey) ?? false; if (_enabled) { await WakelockPlus.enable(); } notifyListeners(); } catch (e) { AppLogger.error('[$_tag] 加载状态失败', e); } } Future toggle() async { try { _enabled = !_enabled; if (_enabled) { await WakelockPlus.enable(); } else { await WakelockPlus.disable(); } await _prefs.setBool(_wakeLockKey, _enabled); notifyListeners(); } catch (e) { AppLogger.error('[$_tag] 切换状态失败', e); // 恢复状态 _enabled = !_enabled; notifyListeners(); } } Future dispose() async { try { await WakelockPlus.disable(); } catch (e) { AppLogger.error('[$_tag] 释放失败', e); } super.dispose(); } } ================================================ FILE: lib/core/subtitle/cache/subtitle_cache_manager.dart ================================================ import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:asmrapp/utils/logger.dart'; class SubtitleCacheManager { static const String key = 'subtitleCache'; static final CacheManager instance = CacheManager( Config( key, stalePeriod: const Duration(days: 365), // 字幕文件不会变更,设置较长的有效期 maxNrOfCacheObjects: 1000, // 最大缓存文件数 repo: JsonCacheInfoRepository(databaseName: key), fileService: HttpFileService(), ), ); /// 获取缓存的字幕内容 static Future getCachedContent(String url) async { try { final file = await instance.getSingleFile(url); AppLogger.debug('使用字幕缓存: $url'); return await file.readAsString(); } catch (e) { AppLogger.error('读取字幕缓存失败', e); return null; } } /// 保存字幕内容到缓存 static Future cacheContent(String url, String content) async { try { await instance.putFile( url, Uint8List.fromList(utf8.encode(content)), fileExtension: 'txt', ); AppLogger.debug('字幕已缓存: $url'); } catch (e) { AppLogger.error('保存字幕缓存失败', e); } } /// 清理缓存 static Future clearCache() async { try { await instance.emptyCache(); AppLogger.debug('字幕缓存已清空'); } catch (e) { AppLogger.error('清理字幕缓存失败', e); } } /// 获取缓存大小 static Future getSize() async { try { return instance.store.getCacheSize(); } catch (e) { AppLogger.error('获取字幕缓存大小失败', e); return 0; } } } ================================================ FILE: lib/core/subtitle/i_subtitle_service.dart ================================================ import 'package:asmrapp/core/audio/models/subtitle.dart'; abstract class ISubtitleService { // 字幕加载 Future loadSubtitle(String url); // 字幕状态流 Stream get subtitleStream; // 当前字幕流 Stream get currentSubtitleStream; // 当前字幕 Subtitle? get currentSubtitle; // 更新播放位置 void updatePosition(Duration position); // 资源释放 void dispose(); // 添加这一行 SubtitleList? get subtitleList; // 获取当前字幕列表 // 添加清除字幕的方法 void clearSubtitle(); Stream get currentSubtitleWithStateStream; SubtitleWithState? get currentSubtitleWithState; } ================================================ FILE: lib/core/subtitle/managers/subtitle_state_manager.dart ================================================ import 'dart:async'; import 'package:asmrapp/core/audio/models/subtitle.dart'; import 'package:asmrapp/utils/logger.dart'; class SubtitleStateManager { SubtitleList? _subtitleList; Subtitle? _currentSubtitle; SubtitleWithState? _currentSubtitleWithState; final _subtitleController = StreamController.broadcast(); final _currentSubtitleController = StreamController.broadcast(); final _currentSubtitleWithStateController = StreamController.broadcast(); Stream get subtitleStream => _subtitleController.stream; Stream get currentSubtitleStream => _currentSubtitleController.stream; Stream get currentSubtitleWithStateStream => _currentSubtitleWithStateController.stream; Subtitle? get currentSubtitle => _currentSubtitle; SubtitleList? get subtitleList => _subtitleList; SubtitleWithState? get currentSubtitleWithState => _currentSubtitleWithState; void setSubtitleList(SubtitleList? subtitleList) { _subtitleList = subtitleList; _subtitleController.add(_subtitleList); } void updatePosition(Duration position) { if (_subtitleList != null) { final newSubtitleWithState = _subtitleList!.getCurrentSubtitle(position); if (newSubtitleWithState?.subtitle != _currentSubtitleWithState?.subtitle) { _currentSubtitleWithState = newSubtitleWithState; _currentSubtitle = newSubtitleWithState?.subtitle; AppLogger.debug('字幕更新: ${_currentSubtitle?.text ?? '无字幕'} (${newSubtitleWithState?.state})'); _currentSubtitleWithStateController.add(newSubtitleWithState); _currentSubtitleController.add(_currentSubtitle); } } } void clear() { _subtitleList = null; _currentSubtitle = null; _currentSubtitleWithState = null; _subtitleController.add(null); _currentSubtitleController.add(null); _currentSubtitleWithStateController.add(null); AppLogger.debug('字幕状态已清除'); } void dispose() { _subtitleController.close(); _currentSubtitleController.close(); _currentSubtitleWithStateController.close(); } } ================================================ FILE: lib/core/subtitle/parsers/lrc_parser.dart ================================================ import 'package:asmrapp/core/audio/models/subtitle.dart'; import 'package:asmrapp/core/subtitle/parsers/subtitle_parser.dart'; import 'package:asmrapp/utils/logger.dart'; class LrcParser extends BaseSubtitleParser { static final _timeTagRegex = RegExp(r'\[(\d{2}):(\d{2})\.(\d{2})\]'); static final _idTagRegex = RegExp(r'^\[(ar|ti|al|by|offset):(.+)\]$'); @override bool canParse(String content) { final lines = content.trim().split('\n'); return lines.any((line) => _timeTagRegex.hasMatch(line)); } @override SubtitleList doParse(String content) { final lines = content.split('\n'); final subtitles = []; final metadata = {}; for (final line in lines) { final trimmedLine = line.trim(); if (trimmedLine.isEmpty) continue; // 检查是否是ID标签 final idMatch = _idTagRegex.firstMatch(trimmedLine); if (idMatch != null) { metadata[idMatch.group(1)!] = idMatch.group(2)!; continue; } // 解析时间标签和歌词 final timeMatches = _timeTagRegex.allMatches(trimmedLine); if (timeMatches.isEmpty) continue; // 获取歌词内容 (移除所有时间标签) final text = trimmedLine.replaceAll(_timeTagRegex, '').trim(); if (text.isEmpty) continue; // 一行可能有多个时间标签 for (final match in timeMatches) { try { final timestamp = _parseTimestamp( minutes: match.group(1)!, seconds: match.group(2)!, milliseconds: match.group(3)!, ); subtitles.add(Subtitle( start: timestamp, end: timestamp + const Duration(seconds: 5), // 默认持续5秒 text: text, index: subtitles.length, )); } catch (e) { AppLogger.debug('解析LRC时间标签失败: $e'); continue; } } } // 按时间排序 subtitles.sort((a, b) => a.start.compareTo(b.start)); // 设置正确的结束时间 for (int i = 0; i < subtitles.length - 1; i++) { subtitles[i] = Subtitle( start: subtitles[i].start, end: subtitles[i + 1].start, text: subtitles[i].text, index: i, ); } AppLogger.debug('LRC解析完成: ${subtitles.length}条字幕, ${metadata.length}个元数据'); return SubtitleList(subtitles); } Duration _parseTimestamp({ required String minutes, required String seconds, required String milliseconds, }) { return Duration( minutes: int.parse(minutes), seconds: int.parse(seconds), milliseconds: int.parse(milliseconds) * 10, ); } } ================================================ FILE: lib/core/subtitle/parsers/subtitle_parser.dart ================================================ import 'package:asmrapp/core/audio/models/subtitle.dart'; /// 字幕解析器接口 abstract class SubtitleParser { /// 解析字幕内容 SubtitleList parse(String content); /// 检查内容格式是否匹配 bool canParse(String content); } /// 字幕解析器基类 abstract class BaseSubtitleParser implements SubtitleParser { @override SubtitleList parse(String content) { if (!canParse(content)) { throw FormatException('不支持的字幕格式'); } return doParse(content); } /// 具体的解析实现 SubtitleList doParse(String content); } ================================================ FILE: lib/core/subtitle/parsers/subtitle_parser_factory.dart ================================================ import 'package:asmrapp/core/subtitle/parsers/subtitle_parser.dart'; import 'package:asmrapp/core/subtitle/parsers/vtt_parser.dart'; import 'package:asmrapp/core/subtitle/parsers/lrc_parser.dart'; import 'package:asmrapp/utils/logger.dart'; class SubtitleParserFactory { static final List _parsers = [ VttParser(), LrcParser(), ]; static SubtitleParser? getParser(String content) { try { return _parsers.firstWhere((parser) => parser.canParse(content)); } catch (e) { AppLogger.debug('没有找到匹配的字幕解析器'); return null; } } } ================================================ FILE: lib/core/subtitle/parsers/vtt_parser.dart ================================================ import 'package:asmrapp/core/audio/models/subtitle.dart'; import 'package:asmrapp/core/subtitle/parsers/subtitle_parser.dart'; class VttParser extends BaseSubtitleParser { static final _vttHeaderRegex = RegExp(r'^WEBVTT'); @override bool canParse(String content) { return content.trim().startsWith(_vttHeaderRegex); } @override SubtitleList doParse(String content) { final lines = content.split('\n'); final subtitles = []; int index = 0; // 跳过WEBVTT头部 while (index < lines.length && !lines[index].contains('-->')) { index++; } while (index < lines.length) { final timeLine = lines[index]; if (timeLine.contains('-->')) { final times = timeLine.split('-->'); if (times.length == 2) { final start = _parseTimeString(times[0].trim()); final end = _parseTimeString(times[1].trim()); // 收集字幕文本 index++; String text = ''; while (index < lines.length && lines[index].trim().isNotEmpty) { text += lines[index].trim() + '\n'; index++; } if (text.isNotEmpty) { subtitles.add(Subtitle( start: start, end: end, text: text.trim(), index: subtitles.length, )); } } } index++; } return SubtitleList(subtitles); } Duration _parseTimeString(String timeString) { final parts = timeString.split(':'); if (parts.length != 3) throw FormatException('Invalid time format'); final seconds = parts[2].split('.'); return Duration( hours: int.parse(parts[0]), minutes: int.parse(parts[1]), seconds: int.parse(seconds[0]), milliseconds: seconds.length > 1 ? int.parse(seconds[1].padRight(3, '0')) : 0, ); } } ================================================ FILE: lib/core/subtitle/subtitle_loader.dart ================================================ import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/data/models/files/files.dart'; import 'package:asmrapp/core/audio/models/file_path.dart'; import 'package:asmrapp/core/audio/models/subtitle.dart'; import 'package:dio/dio.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:asmrapp/core/subtitle/utils/subtitle_matcher.dart'; import 'package:asmrapp/core/subtitle/parsers/subtitle_parser_factory.dart'; import 'package:asmrapp/core/subtitle/cache/subtitle_cache_manager.dart'; class SubtitleLoader { final _dio = Dio(); // 查找字幕文件 Child? findSubtitleFile(Child audioFile, Files files) { if (files.children == null || audioFile.title == null) { AppLogger.debug('无法查找字幕文件: ${files.children == null ? '文件列表为空' : '当前文件名为空'}'); return null; } AppLogger.debug('开始查找字幕文件...'); // 使用 FilePath 获取同级文件 final siblings = FilePath.getSiblings(audioFile, files); // 使用 SubtitleMatcher 查找匹配的字幕文件 final subtitleFile = SubtitleMatcher.findMatchingSubtitle( audioFile.title!, siblings ); if (subtitleFile != null) { AppLogger.debug('找到字幕文件: ${subtitleFile.title}, URL: ${subtitleFile.mediaDownloadUrl}'); } else { AppLogger.debug('在当前目录中未找到字幕文件'); } return subtitleFile; } // 修改: 加载字幕内容 Future loadSubtitleContent(String url) async { try { // 首先尝试从缓存加载 final cachedContent = await SubtitleCacheManager.getCachedContent(url); if (cachedContent != null) { AppLogger.debug('从缓存加载字幕: $url'); return _parseSubtitleContent(cachedContent); } // 缓存未命中,从网络加载 AppLogger.debug('从网络加载字幕: $url'); final response = await _dio.get(url); AppLogger.debug('字幕文件下载状态: ${response.statusCode}'); if (response.statusCode == 200) { final content = response.data as String; // 保存到缓存 await SubtitleCacheManager.cacheContent(url, content); return _parseSubtitleContent(content); } else { throw Exception('字幕下载失败: ${response.statusCode}'); } } catch (e) { AppLogger.debug('字幕加载失败: $e'); rethrow; } } // 新增: 解析字幕内容的私有方法 SubtitleList? _parseSubtitleContent(String content) { AppLogger.debug('字幕文件内容预览: ${content.substring(0, content.length > 100 ? 100 : content.length)}...'); final parser = SubtitleParserFactory.getParser(content); if (parser == null) { throw Exception('不支持的字幕格式'); } final subtitleList = parser.parse(content); AppLogger.debug('字幕解析完成,字幕数量: ${subtitleList.subtitles.length}'); return subtitleList; } } ================================================ FILE: lib/core/subtitle/subtitle_service.dart ================================================ import 'dart:async'; import 'package:asmrapp/utils/logger.dart'; import 'package:asmrapp/core/audio/models/subtitle.dart'; import 'package:asmrapp/core/subtitle/i_subtitle_service.dart'; import 'package:get_it/get_it.dart'; import 'package:asmrapp/core/subtitle/subtitle_loader.dart'; import 'package:asmrapp/core/subtitle/managers/subtitle_state_manager.dart'; class SubtitleService implements ISubtitleService { final _subtitleLoader = GetIt.I(); final _stateManager = SubtitleStateManager(); @override Stream get subtitleStream => _stateManager.subtitleStream; @override Stream get currentSubtitleStream => _stateManager.currentSubtitleStream; @override Subtitle? get currentSubtitle => _stateManager.currentSubtitle; @override Future loadSubtitle(String url) async { try { clearSubtitle(); final subtitleList = await _subtitleLoader.loadSubtitleContent(url); _stateManager.setSubtitleList(subtitleList); } catch (e) { AppLogger.debug('字幕加载失败: $e'); clearSubtitle(); rethrow; } } @override void updatePosition(Duration position) { _stateManager.updatePosition(position); } @override void dispose() { _stateManager.dispose(); } @override SubtitleList? get subtitleList => _stateManager.subtitleList; @override void clearSubtitle() { _stateManager.clear(); } @override Stream get currentSubtitleWithStateStream => _stateManager.currentSubtitleWithStateStream; @override SubtitleWithState? get currentSubtitleWithState => _stateManager.currentSubtitleWithState; } ================================================ FILE: lib/core/subtitle/utils/subtitle_matcher.dart ================================================ import 'package:asmrapp/data/models/files/child.dart'; class SubtitleMatcher { // 支持的字幕格式 static const supportedFormats = ['.vtt', '.lrc']; // 检查文件是否为字幕文件 static bool isSubtitleFile(String? fileName) { if (fileName == null) return false; return supportedFormats.any((format) => fileName.toLowerCase().endsWith(format)); } // 获取音频文件的可能的字幕文件名列表 static List getPossibleSubtitleNames(String audioFileName) { final names = []; final baseName = _getBaseName(audioFileName); // 生成可能的字幕文件名 for (final format in supportedFormats) { // 1. 直接替换扩展名: aaa.mp3 -> aaa.vtt names.add('$baseName$format'); // 2. 保留原扩展名: aaa.mp3 -> aaa.mp3.vtt names.add('$audioFileName$format'); } return names; } // 查找匹配的字幕文件 static Child? findMatchingSubtitle(String audioFileName, List siblings) { final possibleNames = getPossibleSubtitleNames(audioFileName); // 遍历所有可能的字幕文件名 for (final subtitleName in possibleNames) { try { final subtitleFile = siblings.firstWhere( (file) => file.title?.toLowerCase() == subtitleName.toLowerCase() ); return subtitleFile; } catch (_) { // 继续查找下一个可能的文件名 continue; } } return null; } // 获取不带扩展名的文件名 static String _getBaseName(String fileName) { final lastDot = fileName.lastIndexOf('.'); if (lastDot == -1) return fileName; return fileName.substring(0, lastDot); } } ================================================ FILE: lib/core/theme/app_colors.dart ================================================ import 'package:flutter/material.dart'; /// 应用颜色配置 class AppColors { // 禁止实例化 const AppColors._(); // 亮色主题颜色 static const ColorScheme lightColorScheme = ColorScheme.light( // 基础色调 primary: Color(0xFF6750A4), onPrimary: Colors.white, // 表面颜色 surface: Colors.white, surfaceVariant: Color(0xFFF4F4F4), onSurface: Colors.black87, surfaceContainerHighest: Color(0xFFE6E6E6), // 背景颜色 background: Colors.white, onBackground: Colors.black87, // 错误状态颜色 error: Color(0xFFB3261E), errorContainer: Color(0xFFF9DEDC), onError: Colors.white, ); // 暗色主题颜色 static const ColorScheme darkColorScheme = ColorScheme.dark( // 基础色调 primary: Color(0xFFD0BCFF), onPrimary: Color(0xFF381E72), // 表面颜色 surface: Color(0xFF1C1B1F), surfaceVariant: Color(0xFF2B2930), onSurface: Colors.white, surfaceContainerHighest: Color(0xFF2B2B2B), // 背景颜色 background: Color(0xFF1C1B1F), onBackground: Colors.white, // 错误状态颜色 error: Color(0xFFF2B8B5), errorContainer: Color(0xFF8C1D18), onError: Color(0xFF601410), ); } ================================================ FILE: lib/core/theme/app_theme.dart ================================================ import 'package:flutter/material.dart'; import 'app_colors.dart'; /// 应用主题配置 class AppTheme { // 禁止实例化 const AppTheme._(); // 亮色主题 static ThemeData get light => ThemeData( useMaterial3: true, brightness: Brightness.light, colorScheme: AppColors.lightColorScheme, // Card主题 cardTheme: const CardTheme( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(12)), ), ), // AppBar主题 appBarTheme: const AppBarTheme( centerTitle: true, elevation: 0, scrolledUnderElevation: 0, ), ); // 暗色主题 static ThemeData get dark => ThemeData( useMaterial3: true, brightness: Brightness.dark, colorScheme: AppColors.darkColorScheme, // Card主题 cardTheme: const CardTheme( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(12)), ), ), // AppBar主题 appBarTheme: const AppBarTheme( centerTitle: true, elevation: 0, scrolledUnderElevation: 0, ), ); } ================================================ FILE: lib/core/theme/theme_controller.dart ================================================ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; class ThemeController extends ChangeNotifier { static const String _themeKey = 'theme_mode'; final SharedPreferences _prefs; ThemeController(this._prefs) { // 从持久化存储加载主题模式 final savedThemeMode = _prefs.getString(_themeKey); if (savedThemeMode != null) { _themeMode = ThemeMode.values.firstWhere( (mode) => mode.toString() == savedThemeMode, orElse: () => ThemeMode.system, ); } } ThemeMode _themeMode = ThemeMode.system; ThemeMode get themeMode => _themeMode; // 切换主题模式 Future setThemeMode(ThemeMode mode) async { if (_themeMode == mode) return; _themeMode = mode; notifyListeners(); // 保存到持久化存储 await _prefs.setString(_themeKey, mode.toString()); } // 切换到下一个主题模式 Future toggleThemeMode() async { final modes = ThemeMode.values; final currentIndex = modes.indexOf(_themeMode); final nextIndex = (currentIndex + 1) % modes.length; await setThemeMode(modes[nextIndex]); } } ================================================ FILE: lib/data/models/audio/README.md ================================================ # 音频数据模型 此目录包含所有音频相关的数据模型定义。 ## 文件结构 - `audio_track.dart` - 音频轨道模型 - `playlist.dart` - 播放列表模型 - `audio_metadata.dart` - 音频元数据模型 ## 说明 这些模型用于: - 音频文件信息的封装 - 播放列表数据的组织 - 音频元数据的管理 ================================================ FILE: lib/data/models/auth/auth_resp/auth_resp.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'user.dart'; part 'auth_resp.freezed.dart'; part 'auth_resp.g.dart'; @freezed class AuthResp with _$AuthResp { factory AuthResp({ User? user, String? token, }) = _AuthResp; factory AuthResp.fromJson(Map json) => _$AuthRespFromJson(json); } ================================================ FILE: lib/data/models/auth/auth_resp/auth_resp.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'auth_resp.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); AuthResp _$AuthRespFromJson(Map json) { return _AuthResp.fromJson(json); } /// @nodoc mixin _$AuthResp { User? get user => throw _privateConstructorUsedError; String? get token => throw _privateConstructorUsedError; /// Serializes this AuthResp to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of AuthResp /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $AuthRespCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $AuthRespCopyWith<$Res> { factory $AuthRespCopyWith(AuthResp value, $Res Function(AuthResp) then) = _$AuthRespCopyWithImpl<$Res, AuthResp>; @useResult $Res call({User? user, String? token}); $UserCopyWith<$Res>? get user; } /// @nodoc class _$AuthRespCopyWithImpl<$Res, $Val extends AuthResp> implements $AuthRespCopyWith<$Res> { _$AuthRespCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of AuthResp /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? user = freezed, Object? token = freezed, }) { return _then(_value.copyWith( user: freezed == user ? _value.user : user // ignore: cast_nullable_to_non_nullable as User?, token: freezed == token ? _value.token : token // ignore: cast_nullable_to_non_nullable as String?, ) as $Val); } /// Create a copy of AuthResp /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $UserCopyWith<$Res>? get user { if (_value.user == null) { return null; } return $UserCopyWith<$Res>(_value.user!, (value) { return _then(_value.copyWith(user: value) as $Val); }); } } /// @nodoc abstract class _$$AuthRespImplCopyWith<$Res> implements $AuthRespCopyWith<$Res> { factory _$$AuthRespImplCopyWith( _$AuthRespImpl value, $Res Function(_$AuthRespImpl) then) = __$$AuthRespImplCopyWithImpl<$Res>; @override @useResult $Res call({User? user, String? token}); @override $UserCopyWith<$Res>? get user; } /// @nodoc class __$$AuthRespImplCopyWithImpl<$Res> extends _$AuthRespCopyWithImpl<$Res, _$AuthRespImpl> implements _$$AuthRespImplCopyWith<$Res> { __$$AuthRespImplCopyWithImpl( _$AuthRespImpl _value, $Res Function(_$AuthRespImpl) _then) : super(_value, _then); /// Create a copy of AuthResp /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? user = freezed, Object? token = freezed, }) { return _then(_$AuthRespImpl( user: freezed == user ? _value.user : user // ignore: cast_nullable_to_non_nullable as User?, token: freezed == token ? _value.token : token // ignore: cast_nullable_to_non_nullable as String?, )); } } /// @nodoc @JsonSerializable() class _$AuthRespImpl implements _AuthResp { _$AuthRespImpl({this.user, this.token}); factory _$AuthRespImpl.fromJson(Map json) => _$$AuthRespImplFromJson(json); @override final User? user; @override final String? token; @override String toString() { return 'AuthResp(user: $user, token: $token)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$AuthRespImpl && (identical(other.user, user) || other.user == user) && (identical(other.token, token) || other.token == token)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, user, token); /// Create a copy of AuthResp /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$AuthRespImplCopyWith<_$AuthRespImpl> get copyWith => __$$AuthRespImplCopyWithImpl<_$AuthRespImpl>(this, _$identity); @override Map toJson() { return _$$AuthRespImplToJson( this, ); } } abstract class _AuthResp implements AuthResp { factory _AuthResp({final User? user, final String? token}) = _$AuthRespImpl; factory _AuthResp.fromJson(Map json) = _$AuthRespImpl.fromJson; @override User? get user; @override String? get token; /// Create a copy of AuthResp /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$AuthRespImplCopyWith<_$AuthRespImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/auth/auth_resp/auth_resp.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'auth_resp.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$AuthRespImpl _$$AuthRespImplFromJson(Map json) => _$AuthRespImpl( user: json['user'] == null ? null : User.fromJson(json['user'] as Map), token: json['token'] as String?, ); Map _$$AuthRespImplToJson(_$AuthRespImpl instance) => { 'user': instance.user, 'token': instance.token, }; ================================================ FILE: lib/data/models/auth/auth_resp/user.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'user.freezed.dart'; part 'user.g.dart'; @freezed class User with _$User { factory User({ bool? loggedIn, String? name, String? group, dynamic email, String? recommenderUuid, }) = _User; factory User.fromJson(Map json) => _$UserFromJson(json); } ================================================ FILE: lib/data/models/auth/auth_resp/user.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'user.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); User _$UserFromJson(Map json) { return _User.fromJson(json); } /// @nodoc mixin _$User { bool? get loggedIn => throw _privateConstructorUsedError; String? get name => throw _privateConstructorUsedError; String? get group => throw _privateConstructorUsedError; dynamic get email => throw _privateConstructorUsedError; String? get recommenderUuid => throw _privateConstructorUsedError; /// Serializes this User to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of User /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $UserCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $UserCopyWith<$Res> { factory $UserCopyWith(User value, $Res Function(User) then) = _$UserCopyWithImpl<$Res, User>; @useResult $Res call( {bool? loggedIn, String? name, String? group, dynamic email, String? recommenderUuid}); } /// @nodoc class _$UserCopyWithImpl<$Res, $Val extends User> implements $UserCopyWith<$Res> { _$UserCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of User /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? loggedIn = freezed, Object? name = freezed, Object? group = freezed, Object? email = freezed, Object? recommenderUuid = freezed, }) { return _then(_value.copyWith( loggedIn: freezed == loggedIn ? _value.loggedIn : loggedIn // ignore: cast_nullable_to_non_nullable as bool?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, group: freezed == group ? _value.group : group // ignore: cast_nullable_to_non_nullable as String?, email: freezed == email ? _value.email : email // ignore: cast_nullable_to_non_nullable as dynamic, recommenderUuid: freezed == recommenderUuid ? _value.recommenderUuid : recommenderUuid // ignore: cast_nullable_to_non_nullable as String?, ) as $Val); } } /// @nodoc abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> { factory _$$UserImplCopyWith( _$UserImpl value, $Res Function(_$UserImpl) then) = __$$UserImplCopyWithImpl<$Res>; @override @useResult $Res call( {bool? loggedIn, String? name, String? group, dynamic email, String? recommenderUuid}); } /// @nodoc class __$$UserImplCopyWithImpl<$Res> extends _$UserCopyWithImpl<$Res, _$UserImpl> implements _$$UserImplCopyWith<$Res> { __$$UserImplCopyWithImpl(_$UserImpl _value, $Res Function(_$UserImpl) _then) : super(_value, _then); /// Create a copy of User /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? loggedIn = freezed, Object? name = freezed, Object? group = freezed, Object? email = freezed, Object? recommenderUuid = freezed, }) { return _then(_$UserImpl( loggedIn: freezed == loggedIn ? _value.loggedIn : loggedIn // ignore: cast_nullable_to_non_nullable as bool?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, group: freezed == group ? _value.group : group // ignore: cast_nullable_to_non_nullable as String?, email: freezed == email ? _value.email : email // ignore: cast_nullable_to_non_nullable as dynamic, recommenderUuid: freezed == recommenderUuid ? _value.recommenderUuid : recommenderUuid // ignore: cast_nullable_to_non_nullable as String?, )); } } /// @nodoc @JsonSerializable() class _$UserImpl implements _User { _$UserImpl( {this.loggedIn, this.name, this.group, this.email, this.recommenderUuid}); factory _$UserImpl.fromJson(Map json) => _$$UserImplFromJson(json); @override final bool? loggedIn; @override final String? name; @override final String? group; @override final dynamic email; @override final String? recommenderUuid; @override String toString() { return 'User(loggedIn: $loggedIn, name: $name, group: $group, email: $email, recommenderUuid: $recommenderUuid)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$UserImpl && (identical(other.loggedIn, loggedIn) || other.loggedIn == loggedIn) && (identical(other.name, name) || other.name == name) && (identical(other.group, group) || other.group == group) && const DeepCollectionEquality().equals(other.email, email) && (identical(other.recommenderUuid, recommenderUuid) || other.recommenderUuid == recommenderUuid)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, loggedIn, name, group, const DeepCollectionEquality().hash(email), recommenderUuid); /// Create a copy of User /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$UserImplCopyWith<_$UserImpl> get copyWith => __$$UserImplCopyWithImpl<_$UserImpl>(this, _$identity); @override Map toJson() { return _$$UserImplToJson( this, ); } } abstract class _User implements User { factory _User( {final bool? loggedIn, final String? name, final String? group, final dynamic email, final String? recommenderUuid}) = _$UserImpl; factory _User.fromJson(Map json) = _$UserImpl.fromJson; @override bool? get loggedIn; @override String? get name; @override String? get group; @override dynamic get email; @override String? get recommenderUuid; /// Create a copy of User /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$UserImplCopyWith<_$UserImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/auth/auth_resp/user.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'user.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$UserImpl _$$UserImplFromJson(Map json) => _$UserImpl( loggedIn: json['loggedIn'] as bool?, name: json['name'] as String?, group: json['group'] as String?, email: json['email'], recommenderUuid: json['recommenderUuid'] as String?, ); Map _$$UserImplToJson(_$UserImpl instance) => { 'loggedIn': instance.loggedIn, 'name': instance.name, 'group': instance.group, 'email': instance.email, 'recommenderUuid': instance.recommenderUuid, }; ================================================ FILE: lib/data/models/files/child.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'work.dart'; part 'child.freezed.dart'; part 'child.g.dart'; @freezed class Child with _$Child { factory Child({ String? type, String? title, List? children, String? hash, Work? work, String? workTitle, String? mediaStreamUrl, String? mediaDownloadUrl, int? size, }) = _Child; factory Child.fromJson(Map json) => _$ChildFromJson(json); } ================================================ FILE: lib/data/models/files/child.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'child.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Child _$ChildFromJson(Map json) { return _Child.fromJson(json); } /// @nodoc mixin _$Child { String? get type => throw _privateConstructorUsedError; String? get title => throw _privateConstructorUsedError; List? get children => throw _privateConstructorUsedError; String? get hash => throw _privateConstructorUsedError; Work? get work => throw _privateConstructorUsedError; String? get workTitle => throw _privateConstructorUsedError; String? get mediaStreamUrl => throw _privateConstructorUsedError; String? get mediaDownloadUrl => throw _privateConstructorUsedError; int? get size => throw _privateConstructorUsedError; /// Serializes this Child to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of Child /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $ChildCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $ChildCopyWith<$Res> { factory $ChildCopyWith(Child value, $Res Function(Child) then) = _$ChildCopyWithImpl<$Res, Child>; @useResult $Res call( {String? type, String? title, List? children, String? hash, Work? work, String? workTitle, String? mediaStreamUrl, String? mediaDownloadUrl, int? size}); $WorkCopyWith<$Res>? get work; } /// @nodoc class _$ChildCopyWithImpl<$Res, $Val extends Child> implements $ChildCopyWith<$Res> { _$ChildCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of Child /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? type = freezed, Object? title = freezed, Object? children = freezed, Object? hash = freezed, Object? work = freezed, Object? workTitle = freezed, Object? mediaStreamUrl = freezed, Object? mediaDownloadUrl = freezed, Object? size = freezed, }) { return _then(_value.copyWith( type: freezed == type ? _value.type : type // ignore: cast_nullable_to_non_nullable as String?, title: freezed == title ? _value.title : title // ignore: cast_nullable_to_non_nullable as String?, children: freezed == children ? _value.children : children // ignore: cast_nullable_to_non_nullable as List?, hash: freezed == hash ? _value.hash : hash // ignore: cast_nullable_to_non_nullable as String?, work: freezed == work ? _value.work : work // ignore: cast_nullable_to_non_nullable as Work?, workTitle: freezed == workTitle ? _value.workTitle : workTitle // ignore: cast_nullable_to_non_nullable as String?, mediaStreamUrl: freezed == mediaStreamUrl ? _value.mediaStreamUrl : mediaStreamUrl // ignore: cast_nullable_to_non_nullable as String?, mediaDownloadUrl: freezed == mediaDownloadUrl ? _value.mediaDownloadUrl : mediaDownloadUrl // ignore: cast_nullable_to_non_nullable as String?, size: freezed == size ? _value.size : size // ignore: cast_nullable_to_non_nullable as int?, ) as $Val); } /// Create a copy of Child /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $WorkCopyWith<$Res>? get work { if (_value.work == null) { return null; } return $WorkCopyWith<$Res>(_value.work!, (value) { return _then(_value.copyWith(work: value) as $Val); }); } } /// @nodoc abstract class _$$ChildImplCopyWith<$Res> implements $ChildCopyWith<$Res> { factory _$$ChildImplCopyWith( _$ChildImpl value, $Res Function(_$ChildImpl) then) = __$$ChildImplCopyWithImpl<$Res>; @override @useResult $Res call( {String? type, String? title, List? children, String? hash, Work? work, String? workTitle, String? mediaStreamUrl, String? mediaDownloadUrl, int? size}); @override $WorkCopyWith<$Res>? get work; } /// @nodoc class __$$ChildImplCopyWithImpl<$Res> extends _$ChildCopyWithImpl<$Res, _$ChildImpl> implements _$$ChildImplCopyWith<$Res> { __$$ChildImplCopyWithImpl( _$ChildImpl _value, $Res Function(_$ChildImpl) _then) : super(_value, _then); /// Create a copy of Child /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? type = freezed, Object? title = freezed, Object? children = freezed, Object? hash = freezed, Object? work = freezed, Object? workTitle = freezed, Object? mediaStreamUrl = freezed, Object? mediaDownloadUrl = freezed, Object? size = freezed, }) { return _then(_$ChildImpl( type: freezed == type ? _value.type : type // ignore: cast_nullable_to_non_nullable as String?, title: freezed == title ? _value.title : title // ignore: cast_nullable_to_non_nullable as String?, children: freezed == children ? _value._children : children // ignore: cast_nullable_to_non_nullable as List?, hash: freezed == hash ? _value.hash : hash // ignore: cast_nullable_to_non_nullable as String?, work: freezed == work ? _value.work : work // ignore: cast_nullable_to_non_nullable as Work?, workTitle: freezed == workTitle ? _value.workTitle : workTitle // ignore: cast_nullable_to_non_nullable as String?, mediaStreamUrl: freezed == mediaStreamUrl ? _value.mediaStreamUrl : mediaStreamUrl // ignore: cast_nullable_to_non_nullable as String?, mediaDownloadUrl: freezed == mediaDownloadUrl ? _value.mediaDownloadUrl : mediaDownloadUrl // ignore: cast_nullable_to_non_nullable as String?, size: freezed == size ? _value.size : size // ignore: cast_nullable_to_non_nullable as int?, )); } } /// @nodoc @JsonSerializable() class _$ChildImpl implements _Child { _$ChildImpl( {this.type, this.title, final List? children, this.hash, this.work, this.workTitle, this.mediaStreamUrl, this.mediaDownloadUrl, this.size}) : _children = children; factory _$ChildImpl.fromJson(Map json) => _$$ChildImplFromJson(json); @override final String? type; @override final String? title; final List? _children; @override List? get children { final value = _children; if (value == null) return null; if (_children is EqualUnmodifiableListView) return _children; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(value); } @override final String? hash; @override final Work? work; @override final String? workTitle; @override final String? mediaStreamUrl; @override final String? mediaDownloadUrl; @override final int? size; @override String toString() { return 'Child(type: $type, title: $title, children: $children, hash: $hash, work: $work, workTitle: $workTitle, mediaStreamUrl: $mediaStreamUrl, mediaDownloadUrl: $mediaDownloadUrl, size: $size)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$ChildImpl && (identical(other.type, type) || other.type == type) && (identical(other.title, title) || other.title == title) && const DeepCollectionEquality().equals(other._children, _children) && (identical(other.hash, hash) || other.hash == hash) && (identical(other.work, work) || other.work == work) && (identical(other.workTitle, workTitle) || other.workTitle == workTitle) && (identical(other.mediaStreamUrl, mediaStreamUrl) || other.mediaStreamUrl == mediaStreamUrl) && (identical(other.mediaDownloadUrl, mediaDownloadUrl) || other.mediaDownloadUrl == mediaDownloadUrl) && (identical(other.size, size) || other.size == size)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, type, title, const DeepCollectionEquality().hash(_children), hash, work, workTitle, mediaStreamUrl, mediaDownloadUrl, size); /// Create a copy of Child /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ChildImplCopyWith<_$ChildImpl> get copyWith => __$$ChildImplCopyWithImpl<_$ChildImpl>(this, _$identity); @override Map toJson() { return _$$ChildImplToJson( this, ); } } abstract class _Child implements Child { factory _Child( {final String? type, final String? title, final List? children, final String? hash, final Work? work, final String? workTitle, final String? mediaStreamUrl, final String? mediaDownloadUrl, final int? size}) = _$ChildImpl; factory _Child.fromJson(Map json) = _$ChildImpl.fromJson; @override String? get type; @override String? get title; @override List? get children; @override String? get hash; @override Work? get work; @override String? get workTitle; @override String? get mediaStreamUrl; @override String? get mediaDownloadUrl; @override int? get size; /// Create a copy of Child /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$ChildImplCopyWith<_$ChildImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/files/child.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'child.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$ChildImpl _$$ChildImplFromJson(Map json) => _$ChildImpl( type: json['type'] as String?, title: json['title'] as String?, children: (json['children'] as List?) ?.map((e) => Child.fromJson(e as Map)) .toList(), hash: json['hash'] as String?, work: json['work'] == null ? null : Work.fromJson(json['work'] as Map), workTitle: json['workTitle'] as String?, mediaStreamUrl: json['mediaStreamUrl'] as String?, mediaDownloadUrl: json['mediaDownloadUrl'] as String?, size: (json['size'] as num?)?.toInt(), ); Map _$$ChildImplToJson(_$ChildImpl instance) => { 'type': instance.type, 'title': instance.title, 'children': instance.children, 'hash': instance.hash, 'work': instance.work, 'workTitle': instance.workTitle, 'mediaStreamUrl': instance.mediaStreamUrl, 'mediaDownloadUrl': instance.mediaDownloadUrl, 'size': instance.size, }; ================================================ FILE: lib/data/models/files/files.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'child.dart'; part 'files.freezed.dart'; part 'files.g.dart'; @freezed class Files with _$Files { factory Files({ String? type, String? title, List? children, }) = _Files; factory Files.fromJson(Map json) => _$FilesFromJson(json); } ================================================ FILE: lib/data/models/files/files.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'files.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Files _$FilesFromJson(Map json) { return _Files.fromJson(json); } /// @nodoc mixin _$Files { String? get type => throw _privateConstructorUsedError; String? get title => throw _privateConstructorUsedError; List? get children => throw _privateConstructorUsedError; /// Serializes this Files to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of Files /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $FilesCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $FilesCopyWith<$Res> { factory $FilesCopyWith(Files value, $Res Function(Files) then) = _$FilesCopyWithImpl<$Res, Files>; @useResult $Res call({String? type, String? title, List? children}); } /// @nodoc class _$FilesCopyWithImpl<$Res, $Val extends Files> implements $FilesCopyWith<$Res> { _$FilesCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of Files /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? type = freezed, Object? title = freezed, Object? children = freezed, }) { return _then(_value.copyWith( type: freezed == type ? _value.type : type // ignore: cast_nullable_to_non_nullable as String?, title: freezed == title ? _value.title : title // ignore: cast_nullable_to_non_nullable as String?, children: freezed == children ? _value.children : children // ignore: cast_nullable_to_non_nullable as List?, ) as $Val); } } /// @nodoc abstract class _$$FilesImplCopyWith<$Res> implements $FilesCopyWith<$Res> { factory _$$FilesImplCopyWith( _$FilesImpl value, $Res Function(_$FilesImpl) then) = __$$FilesImplCopyWithImpl<$Res>; @override @useResult $Res call({String? type, String? title, List? children}); } /// @nodoc class __$$FilesImplCopyWithImpl<$Res> extends _$FilesCopyWithImpl<$Res, _$FilesImpl> implements _$$FilesImplCopyWith<$Res> { __$$FilesImplCopyWithImpl( _$FilesImpl _value, $Res Function(_$FilesImpl) _then) : super(_value, _then); /// Create a copy of Files /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? type = freezed, Object? title = freezed, Object? children = freezed, }) { return _then(_$FilesImpl( type: freezed == type ? _value.type : type // ignore: cast_nullable_to_non_nullable as String?, title: freezed == title ? _value.title : title // ignore: cast_nullable_to_non_nullable as String?, children: freezed == children ? _value._children : children // ignore: cast_nullable_to_non_nullable as List?, )); } } /// @nodoc @JsonSerializable() class _$FilesImpl implements _Files { _$FilesImpl({this.type, this.title, final List? children}) : _children = children; factory _$FilesImpl.fromJson(Map json) => _$$FilesImplFromJson(json); @override final String? type; @override final String? title; final List? _children; @override List? get children { final value = _children; if (value == null) return null; if (_children is EqualUnmodifiableListView) return _children; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(value); } @override String toString() { return 'Files(type: $type, title: $title, children: $children)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$FilesImpl && (identical(other.type, type) || other.type == type) && (identical(other.title, title) || other.title == title) && const DeepCollectionEquality().equals(other._children, _children)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, type, title, const DeepCollectionEquality().hash(_children)); /// Create a copy of Files /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$FilesImplCopyWith<_$FilesImpl> get copyWith => __$$FilesImplCopyWithImpl<_$FilesImpl>(this, _$identity); @override Map toJson() { return _$$FilesImplToJson( this, ); } } abstract class _Files implements Files { factory _Files( {final String? type, final String? title, final List? children}) = _$FilesImpl; factory _Files.fromJson(Map json) = _$FilesImpl.fromJson; @override String? get type; @override String? get title; @override List? get children; /// Create a copy of Files /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$FilesImplCopyWith<_$FilesImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/files/files.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'files.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$FilesImpl _$$FilesImplFromJson(Map json) => _$FilesImpl( type: json['type'] as String?, title: json['title'] as String?, children: (json['children'] as List?) ?.map((e) => Child.fromJson(e as Map)) .toList(), ); Map _$$FilesImplToJson(_$FilesImpl instance) => { 'type': instance.type, 'title': instance.title, 'children': instance.children, }; ================================================ FILE: lib/data/models/files/work.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'work.freezed.dart'; part 'work.g.dart'; @freezed class Work with _$Work { factory Work({ int? id, @JsonKey(name: 'source_id') String? sourceId, @JsonKey(name: 'source_type') String? sourceType, }) = _Work; factory Work.fromJson(Map json) => _$WorkFromJson(json); } ================================================ FILE: lib/data/models/files/work.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'work.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Work _$WorkFromJson(Map json) { return _Work.fromJson(json); } /// @nodoc mixin _$Work { int? get id => throw _privateConstructorUsedError; @JsonKey(name: 'source_id') String? get sourceId => throw _privateConstructorUsedError; @JsonKey(name: 'source_type') String? get sourceType => throw _privateConstructorUsedError; /// Serializes this Work to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of Work /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $WorkCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $WorkCopyWith<$Res> { factory $WorkCopyWith(Work value, $Res Function(Work) then) = _$WorkCopyWithImpl<$Res, Work>; @useResult $Res call( {int? id, @JsonKey(name: 'source_id') String? sourceId, @JsonKey(name: 'source_type') String? sourceType}); } /// @nodoc class _$WorkCopyWithImpl<$Res, $Val extends Work> implements $WorkCopyWith<$Res> { _$WorkCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of Work /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? sourceId = freezed, Object? sourceType = freezed, }) { return _then(_value.copyWith( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as int?, sourceId: freezed == sourceId ? _value.sourceId : sourceId // ignore: cast_nullable_to_non_nullable as String?, sourceType: freezed == sourceType ? _value.sourceType : sourceType // ignore: cast_nullable_to_non_nullable as String?, ) as $Val); } } /// @nodoc abstract class _$$WorkImplCopyWith<$Res> implements $WorkCopyWith<$Res> { factory _$$WorkImplCopyWith( _$WorkImpl value, $Res Function(_$WorkImpl) then) = __$$WorkImplCopyWithImpl<$Res>; @override @useResult $Res call( {int? id, @JsonKey(name: 'source_id') String? sourceId, @JsonKey(name: 'source_type') String? sourceType}); } /// @nodoc class __$$WorkImplCopyWithImpl<$Res> extends _$WorkCopyWithImpl<$Res, _$WorkImpl> implements _$$WorkImplCopyWith<$Res> { __$$WorkImplCopyWithImpl(_$WorkImpl _value, $Res Function(_$WorkImpl) _then) : super(_value, _then); /// Create a copy of Work /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? sourceId = freezed, Object? sourceType = freezed, }) { return _then(_$WorkImpl( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as int?, sourceId: freezed == sourceId ? _value.sourceId : sourceId // ignore: cast_nullable_to_non_nullable as String?, sourceType: freezed == sourceType ? _value.sourceType : sourceType // ignore: cast_nullable_to_non_nullable as String?, )); } } /// @nodoc @JsonSerializable() class _$WorkImpl implements _Work { _$WorkImpl( {this.id, @JsonKey(name: 'source_id') this.sourceId, @JsonKey(name: 'source_type') this.sourceType}); factory _$WorkImpl.fromJson(Map json) => _$$WorkImplFromJson(json); @override final int? id; @override @JsonKey(name: 'source_id') final String? sourceId; @override @JsonKey(name: 'source_type') final String? sourceType; @override String toString() { return 'Work(id: $id, sourceId: $sourceId, sourceType: $sourceType)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$WorkImpl && (identical(other.id, id) || other.id == id) && (identical(other.sourceId, sourceId) || other.sourceId == sourceId) && (identical(other.sourceType, sourceType) || other.sourceType == sourceType)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, id, sourceId, sourceType); /// Create a copy of Work /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$WorkImplCopyWith<_$WorkImpl> get copyWith => __$$WorkImplCopyWithImpl<_$WorkImpl>(this, _$identity); @override Map toJson() { return _$$WorkImplToJson( this, ); } } abstract class _Work implements Work { factory _Work( {final int? id, @JsonKey(name: 'source_id') final String? sourceId, @JsonKey(name: 'source_type') final String? sourceType}) = _$WorkImpl; factory _Work.fromJson(Map json) = _$WorkImpl.fromJson; @override int? get id; @override @JsonKey(name: 'source_id') String? get sourceId; @override @JsonKey(name: 'source_type') String? get sourceType; /// Create a copy of Work /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$WorkImplCopyWith<_$WorkImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/files/work.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'work.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$WorkImpl _$$WorkImplFromJson(Map json) => _$WorkImpl( id: (json['id'] as num?)?.toInt(), sourceId: json['source_id'] as String?, sourceType: json['source_type'] as String?, ); Map _$$WorkImplToJson(_$WorkImpl instance) => { 'id': instance.id, 'source_id': instance.sourceId, 'source_type': instance.sourceType, }; ================================================ FILE: lib/data/models/mark_lists/mark_lists.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'pagination.dart'; import 'playlist.dart'; part 'mark_lists.freezed.dart'; part 'mark_lists.g.dart'; @freezed class MarkLists with _$MarkLists { factory MarkLists({ List? playlists, Pagination? pagination, }) = _MarkLists; factory MarkLists.fromJson(Map json) => _$MarkListsFromJson(json); } ================================================ FILE: lib/data/models/mark_lists/mark_lists.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'mark_lists.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); MarkLists _$MarkListsFromJson(Map json) { return _MarkLists.fromJson(json); } /// @nodoc mixin _$MarkLists { List? get playlists => throw _privateConstructorUsedError; Pagination? get pagination => throw _privateConstructorUsedError; /// Serializes this MarkLists to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of MarkLists /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $MarkListsCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $MarkListsCopyWith<$Res> { factory $MarkListsCopyWith(MarkLists value, $Res Function(MarkLists) then) = _$MarkListsCopyWithImpl<$Res, MarkLists>; @useResult $Res call({List? playlists, Pagination? pagination}); $PaginationCopyWith<$Res>? get pagination; } /// @nodoc class _$MarkListsCopyWithImpl<$Res, $Val extends MarkLists> implements $MarkListsCopyWith<$Res> { _$MarkListsCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of MarkLists /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? playlists = freezed, Object? pagination = freezed, }) { return _then(_value.copyWith( playlists: freezed == playlists ? _value.playlists : playlists // ignore: cast_nullable_to_non_nullable as List?, pagination: freezed == pagination ? _value.pagination : pagination // ignore: cast_nullable_to_non_nullable as Pagination?, ) as $Val); } /// Create a copy of MarkLists /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $PaginationCopyWith<$Res>? get pagination { if (_value.pagination == null) { return null; } return $PaginationCopyWith<$Res>(_value.pagination!, (value) { return _then(_value.copyWith(pagination: value) as $Val); }); } } /// @nodoc abstract class _$$MarkListsImplCopyWith<$Res> implements $MarkListsCopyWith<$Res> { factory _$$MarkListsImplCopyWith( _$MarkListsImpl value, $Res Function(_$MarkListsImpl) then) = __$$MarkListsImplCopyWithImpl<$Res>; @override @useResult $Res call({List? playlists, Pagination? pagination}); @override $PaginationCopyWith<$Res>? get pagination; } /// @nodoc class __$$MarkListsImplCopyWithImpl<$Res> extends _$MarkListsCopyWithImpl<$Res, _$MarkListsImpl> implements _$$MarkListsImplCopyWith<$Res> { __$$MarkListsImplCopyWithImpl( _$MarkListsImpl _value, $Res Function(_$MarkListsImpl) _then) : super(_value, _then); /// Create a copy of MarkLists /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? playlists = freezed, Object? pagination = freezed, }) { return _then(_$MarkListsImpl( playlists: freezed == playlists ? _value._playlists : playlists // ignore: cast_nullable_to_non_nullable as List?, pagination: freezed == pagination ? _value.pagination : pagination // ignore: cast_nullable_to_non_nullable as Pagination?, )); } } /// @nodoc @JsonSerializable() class _$MarkListsImpl implements _MarkLists { _$MarkListsImpl({final List? playlists, this.pagination}) : _playlists = playlists; factory _$MarkListsImpl.fromJson(Map json) => _$$MarkListsImplFromJson(json); final List? _playlists; @override List? get playlists { final value = _playlists; if (value == null) return null; if (_playlists is EqualUnmodifiableListView) return _playlists; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(value); } @override final Pagination? pagination; @override String toString() { return 'MarkLists(playlists: $playlists, pagination: $pagination)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$MarkListsImpl && const DeepCollectionEquality() .equals(other._playlists, _playlists) && (identical(other.pagination, pagination) || other.pagination == pagination)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, const DeepCollectionEquality().hash(_playlists), pagination); /// Create a copy of MarkLists /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$MarkListsImplCopyWith<_$MarkListsImpl> get copyWith => __$$MarkListsImplCopyWithImpl<_$MarkListsImpl>(this, _$identity); @override Map toJson() { return _$$MarkListsImplToJson( this, ); } } abstract class _MarkLists implements MarkLists { factory _MarkLists( {final List? playlists, final Pagination? pagination}) = _$MarkListsImpl; factory _MarkLists.fromJson(Map json) = _$MarkListsImpl.fromJson; @override List? get playlists; @override Pagination? get pagination; /// Create a copy of MarkLists /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$MarkListsImplCopyWith<_$MarkListsImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/mark_lists/mark_lists.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'mark_lists.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$MarkListsImpl _$$MarkListsImplFromJson(Map json) => _$MarkListsImpl( playlists: (json['playlists'] as List?) ?.map((e) => Playlist.fromJson(e as Map)) .toList(), pagination: json['pagination'] == null ? null : Pagination.fromJson(json['pagination'] as Map), ); Map _$$MarkListsImplToJson(_$MarkListsImpl instance) => { 'playlists': instance.playlists, 'pagination': instance.pagination, }; ================================================ FILE: lib/data/models/mark_lists/pagination.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'pagination.freezed.dart'; part 'pagination.g.dart'; @freezed class Pagination with _$Pagination { factory Pagination({ int? page, int? pageSize, int? totalCount, }) = _Pagination; factory Pagination.fromJson(Map json) => _$PaginationFromJson(json); } ================================================ FILE: lib/data/models/mark_lists/pagination.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'pagination.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Pagination _$PaginationFromJson(Map json) { return _Pagination.fromJson(json); } /// @nodoc mixin _$Pagination { int? get page => throw _privateConstructorUsedError; int? get pageSize => throw _privateConstructorUsedError; int? get totalCount => throw _privateConstructorUsedError; /// Serializes this Pagination to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $PaginationCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $PaginationCopyWith<$Res> { factory $PaginationCopyWith( Pagination value, $Res Function(Pagination) then) = _$PaginationCopyWithImpl<$Res, Pagination>; @useResult $Res call({int? page, int? pageSize, int? totalCount}); } /// @nodoc class _$PaginationCopyWithImpl<$Res, $Val extends Pagination> implements $PaginationCopyWith<$Res> { _$PaginationCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? page = freezed, Object? pageSize = freezed, Object? totalCount = freezed, }) { return _then(_value.copyWith( page: freezed == page ? _value.page : page // ignore: cast_nullable_to_non_nullable as int?, pageSize: freezed == pageSize ? _value.pageSize : pageSize // ignore: cast_nullable_to_non_nullable as int?, totalCount: freezed == totalCount ? _value.totalCount : totalCount // ignore: cast_nullable_to_non_nullable as int?, ) as $Val); } } /// @nodoc abstract class _$$PaginationImplCopyWith<$Res> implements $PaginationCopyWith<$Res> { factory _$$PaginationImplCopyWith( _$PaginationImpl value, $Res Function(_$PaginationImpl) then) = __$$PaginationImplCopyWithImpl<$Res>; @override @useResult $Res call({int? page, int? pageSize, int? totalCount}); } /// @nodoc class __$$PaginationImplCopyWithImpl<$Res> extends _$PaginationCopyWithImpl<$Res, _$PaginationImpl> implements _$$PaginationImplCopyWith<$Res> { __$$PaginationImplCopyWithImpl( _$PaginationImpl _value, $Res Function(_$PaginationImpl) _then) : super(_value, _then); /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? page = freezed, Object? pageSize = freezed, Object? totalCount = freezed, }) { return _then(_$PaginationImpl( page: freezed == page ? _value.page : page // ignore: cast_nullable_to_non_nullable as int?, pageSize: freezed == pageSize ? _value.pageSize : pageSize // ignore: cast_nullable_to_non_nullable as int?, totalCount: freezed == totalCount ? _value.totalCount : totalCount // ignore: cast_nullable_to_non_nullable as int?, )); } } /// @nodoc @JsonSerializable() class _$PaginationImpl implements _Pagination { _$PaginationImpl({this.page, this.pageSize, this.totalCount}); factory _$PaginationImpl.fromJson(Map json) => _$$PaginationImplFromJson(json); @override final int? page; @override final int? pageSize; @override final int? totalCount; @override String toString() { return 'Pagination(page: $page, pageSize: $pageSize, totalCount: $totalCount)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PaginationImpl && (identical(other.page, page) || other.page == page) && (identical(other.pageSize, pageSize) || other.pageSize == pageSize) && (identical(other.totalCount, totalCount) || other.totalCount == totalCount)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, page, pageSize, totalCount); /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PaginationImplCopyWith<_$PaginationImpl> get copyWith => __$$PaginationImplCopyWithImpl<_$PaginationImpl>(this, _$identity); @override Map toJson() { return _$$PaginationImplToJson( this, ); } } abstract class _Pagination implements Pagination { factory _Pagination( {final int? page, final int? pageSize, final int? totalCount}) = _$PaginationImpl; factory _Pagination.fromJson(Map json) = _$PaginationImpl.fromJson; @override int? get page; @override int? get pageSize; @override int? get totalCount; /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$PaginationImplCopyWith<_$PaginationImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/mark_lists/pagination.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'pagination.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$PaginationImpl _$$PaginationImplFromJson(Map json) => _$PaginationImpl( page: (json['page'] as num?)?.toInt(), pageSize: (json['pageSize'] as num?)?.toInt(), totalCount: (json['totalCount'] as num?)?.toInt(), ); Map _$$PaginationImplToJson(_$PaginationImpl instance) => { 'page': instance.page, 'pageSize': instance.pageSize, 'totalCount': instance.totalCount, }; ================================================ FILE: lib/data/models/mark_lists/playlist.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'playlist.freezed.dart'; part 'playlist.g.dart'; @freezed class Playlist with _$Playlist { factory Playlist({ String? id, @JsonKey(name: 'user_name') String? userName, int? privacy, String? locale, @JsonKey(name: 'playback_count') int? playbackCount, String? name, String? description, @JsonKey(name: 'created_at') String? createdAt, @JsonKey(name: 'updated_at') String? updatedAt, @JsonKey(name: 'works_count') int? worksCount, @JsonKey(name: 'latestWorkID') dynamic latestWorkId, String? mainCoverUrl, }) = _Playlist; factory Playlist.fromJson(Map json) => _$PlaylistFromJson(json); } ================================================ FILE: lib/data/models/mark_lists/playlist.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'playlist.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Playlist _$PlaylistFromJson(Map json) { return _Playlist.fromJson(json); } /// @nodoc mixin _$Playlist { String? get id => throw _privateConstructorUsedError; @JsonKey(name: 'user_name') String? get userName => throw _privateConstructorUsedError; int? get privacy => throw _privateConstructorUsedError; String? get locale => throw _privateConstructorUsedError; @JsonKey(name: 'playback_count') int? get playbackCount => throw _privateConstructorUsedError; String? get name => throw _privateConstructorUsedError; String? get description => throw _privateConstructorUsedError; @JsonKey(name: 'created_at') String? get createdAt => throw _privateConstructorUsedError; @JsonKey(name: 'updated_at') String? get updatedAt => throw _privateConstructorUsedError; @JsonKey(name: 'works_count') int? get worksCount => throw _privateConstructorUsedError; @JsonKey(name: 'latestWorkID') dynamic get latestWorkId => throw _privateConstructorUsedError; String? get mainCoverUrl => throw _privateConstructorUsedError; /// Serializes this Playlist to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $PlaylistCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $PlaylistCopyWith<$Res> { factory $PlaylistCopyWith(Playlist value, $Res Function(Playlist) then) = _$PlaylistCopyWithImpl<$Res, Playlist>; @useResult $Res call( {String? id, @JsonKey(name: 'user_name') String? userName, int? privacy, String? locale, @JsonKey(name: 'playback_count') int? playbackCount, String? name, String? description, @JsonKey(name: 'created_at') String? createdAt, @JsonKey(name: 'updated_at') String? updatedAt, @JsonKey(name: 'works_count') int? worksCount, @JsonKey(name: 'latestWorkID') dynamic latestWorkId, String? mainCoverUrl}); } /// @nodoc class _$PlaylistCopyWithImpl<$Res, $Val extends Playlist> implements $PlaylistCopyWith<$Res> { _$PlaylistCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? userName = freezed, Object? privacy = freezed, Object? locale = freezed, Object? playbackCount = freezed, Object? name = freezed, Object? description = freezed, Object? createdAt = freezed, Object? updatedAt = freezed, Object? worksCount = freezed, Object? latestWorkId = freezed, Object? mainCoverUrl = freezed, }) { return _then(_value.copyWith( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as String?, userName: freezed == userName ? _value.userName : userName // ignore: cast_nullable_to_non_nullable as String?, privacy: freezed == privacy ? _value.privacy : privacy // ignore: cast_nullable_to_non_nullable as int?, locale: freezed == locale ? _value.locale : locale // ignore: cast_nullable_to_non_nullable as String?, playbackCount: freezed == playbackCount ? _value.playbackCount : playbackCount // ignore: cast_nullable_to_non_nullable as int?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, description: freezed == description ? _value.description : description // ignore: cast_nullable_to_non_nullable as String?, createdAt: freezed == createdAt ? _value.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as String?, updatedAt: freezed == updatedAt ? _value.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as String?, worksCount: freezed == worksCount ? _value.worksCount : worksCount // ignore: cast_nullable_to_non_nullable as int?, latestWorkId: freezed == latestWorkId ? _value.latestWorkId : latestWorkId // ignore: cast_nullable_to_non_nullable as dynamic, mainCoverUrl: freezed == mainCoverUrl ? _value.mainCoverUrl : mainCoverUrl // ignore: cast_nullable_to_non_nullable as String?, ) as $Val); } } /// @nodoc abstract class _$$PlaylistImplCopyWith<$Res> implements $PlaylistCopyWith<$Res> { factory _$$PlaylistImplCopyWith( _$PlaylistImpl value, $Res Function(_$PlaylistImpl) then) = __$$PlaylistImplCopyWithImpl<$Res>; @override @useResult $Res call( {String? id, @JsonKey(name: 'user_name') String? userName, int? privacy, String? locale, @JsonKey(name: 'playback_count') int? playbackCount, String? name, String? description, @JsonKey(name: 'created_at') String? createdAt, @JsonKey(name: 'updated_at') String? updatedAt, @JsonKey(name: 'works_count') int? worksCount, @JsonKey(name: 'latestWorkID') dynamic latestWorkId, String? mainCoverUrl}); } /// @nodoc class __$$PlaylistImplCopyWithImpl<$Res> extends _$PlaylistCopyWithImpl<$Res, _$PlaylistImpl> implements _$$PlaylistImplCopyWith<$Res> { __$$PlaylistImplCopyWithImpl( _$PlaylistImpl _value, $Res Function(_$PlaylistImpl) _then) : super(_value, _then); /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? userName = freezed, Object? privacy = freezed, Object? locale = freezed, Object? playbackCount = freezed, Object? name = freezed, Object? description = freezed, Object? createdAt = freezed, Object? updatedAt = freezed, Object? worksCount = freezed, Object? latestWorkId = freezed, Object? mainCoverUrl = freezed, }) { return _then(_$PlaylistImpl( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as String?, userName: freezed == userName ? _value.userName : userName // ignore: cast_nullable_to_non_nullable as String?, privacy: freezed == privacy ? _value.privacy : privacy // ignore: cast_nullable_to_non_nullable as int?, locale: freezed == locale ? _value.locale : locale // ignore: cast_nullable_to_non_nullable as String?, playbackCount: freezed == playbackCount ? _value.playbackCount : playbackCount // ignore: cast_nullable_to_non_nullable as int?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, description: freezed == description ? _value.description : description // ignore: cast_nullable_to_non_nullable as String?, createdAt: freezed == createdAt ? _value.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as String?, updatedAt: freezed == updatedAt ? _value.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as String?, worksCount: freezed == worksCount ? _value.worksCount : worksCount // ignore: cast_nullable_to_non_nullable as int?, latestWorkId: freezed == latestWorkId ? _value.latestWorkId : latestWorkId // ignore: cast_nullable_to_non_nullable as dynamic, mainCoverUrl: freezed == mainCoverUrl ? _value.mainCoverUrl : mainCoverUrl // ignore: cast_nullable_to_non_nullable as String?, )); } } /// @nodoc @JsonSerializable() class _$PlaylistImpl implements _Playlist { _$PlaylistImpl( {this.id, @JsonKey(name: 'user_name') this.userName, this.privacy, this.locale, @JsonKey(name: 'playback_count') this.playbackCount, this.name, this.description, @JsonKey(name: 'created_at') this.createdAt, @JsonKey(name: 'updated_at') this.updatedAt, @JsonKey(name: 'works_count') this.worksCount, @JsonKey(name: 'latestWorkID') this.latestWorkId, this.mainCoverUrl}); factory _$PlaylistImpl.fromJson(Map json) => _$$PlaylistImplFromJson(json); @override final String? id; @override @JsonKey(name: 'user_name') final String? userName; @override final int? privacy; @override final String? locale; @override @JsonKey(name: 'playback_count') final int? playbackCount; @override final String? name; @override final String? description; @override @JsonKey(name: 'created_at') final String? createdAt; @override @JsonKey(name: 'updated_at') final String? updatedAt; @override @JsonKey(name: 'works_count') final int? worksCount; @override @JsonKey(name: 'latestWorkID') final dynamic latestWorkId; @override final String? mainCoverUrl; @override String toString() { return 'Playlist(id: $id, userName: $userName, privacy: $privacy, locale: $locale, playbackCount: $playbackCount, name: $name, description: $description, createdAt: $createdAt, updatedAt: $updatedAt, worksCount: $worksCount, latestWorkId: $latestWorkId, mainCoverUrl: $mainCoverUrl)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PlaylistImpl && (identical(other.id, id) || other.id == id) && (identical(other.userName, userName) || other.userName == userName) && (identical(other.privacy, privacy) || other.privacy == privacy) && (identical(other.locale, locale) || other.locale == locale) && (identical(other.playbackCount, playbackCount) || other.playbackCount == playbackCount) && (identical(other.name, name) || other.name == name) && (identical(other.description, description) || other.description == description) && (identical(other.createdAt, createdAt) || other.createdAt == createdAt) && (identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt) && (identical(other.worksCount, worksCount) || other.worksCount == worksCount) && const DeepCollectionEquality() .equals(other.latestWorkId, latestWorkId) && (identical(other.mainCoverUrl, mainCoverUrl) || other.mainCoverUrl == mainCoverUrl)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, id, userName, privacy, locale, playbackCount, name, description, createdAt, updatedAt, worksCount, const DeepCollectionEquality().hash(latestWorkId), mainCoverUrl); /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PlaylistImplCopyWith<_$PlaylistImpl> get copyWith => __$$PlaylistImplCopyWithImpl<_$PlaylistImpl>(this, _$identity); @override Map toJson() { return _$$PlaylistImplToJson( this, ); } } abstract class _Playlist implements Playlist { factory _Playlist( {final String? id, @JsonKey(name: 'user_name') final String? userName, final int? privacy, final String? locale, @JsonKey(name: 'playback_count') final int? playbackCount, final String? name, final String? description, @JsonKey(name: 'created_at') final String? createdAt, @JsonKey(name: 'updated_at') final String? updatedAt, @JsonKey(name: 'works_count') final int? worksCount, @JsonKey(name: 'latestWorkID') final dynamic latestWorkId, final String? mainCoverUrl}) = _$PlaylistImpl; factory _Playlist.fromJson(Map json) = _$PlaylistImpl.fromJson; @override String? get id; @override @JsonKey(name: 'user_name') String? get userName; @override int? get privacy; @override String? get locale; @override @JsonKey(name: 'playback_count') int? get playbackCount; @override String? get name; @override String? get description; @override @JsonKey(name: 'created_at') String? get createdAt; @override @JsonKey(name: 'updated_at') String? get updatedAt; @override @JsonKey(name: 'works_count') int? get worksCount; @override @JsonKey(name: 'latestWorkID') dynamic get latestWorkId; @override String? get mainCoverUrl; /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$PlaylistImplCopyWith<_$PlaylistImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/mark_lists/playlist.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'playlist.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$PlaylistImpl _$$PlaylistImplFromJson(Map json) => _$PlaylistImpl( id: json['id'] as String?, userName: json['user_name'] as String?, privacy: (json['privacy'] as num?)?.toInt(), locale: json['locale'] as String?, playbackCount: (json['playback_count'] as num?)?.toInt(), name: json['name'] as String?, description: json['description'] as String?, createdAt: json['created_at'] as String?, updatedAt: json['updated_at'] as String?, worksCount: (json['works_count'] as num?)?.toInt(), latestWorkId: json['latestWorkID'], mainCoverUrl: json['mainCoverUrl'] as String?, ); Map _$$PlaylistImplToJson(_$PlaylistImpl instance) => { 'id': instance.id, 'user_name': instance.userName, 'privacy': instance.privacy, 'locale': instance.locale, 'playback_count': instance.playbackCount, 'name': instance.name, 'description': instance.description, 'created_at': instance.createdAt, 'updated_at': instance.updatedAt, 'works_count': instance.worksCount, 'latestWorkID': instance.latestWorkId, 'mainCoverUrl': instance.mainCoverUrl, }; ================================================ FILE: lib/data/models/mark_status.dart ================================================ enum MarkStatus { wantToListen('想听'), listening('在听'), listened('听过'), relistening('重听'), onHold('搁置'); final String label; const MarkStatus(this.label); } ================================================ FILE: lib/data/models/my_lists/README.md ================================================ 虽然已有相似结构,但为了方便管理,还是单独创建一个文件夹,专门用来处理“播放清单”这个页面的东西。 ================================================ FILE: lib/data/models/my_lists/my_playlists/my_playlists.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'pagination.dart'; import 'playlist.dart'; part 'my_playlists.freezed.dart'; part 'my_playlists.g.dart'; @freezed class MyPlaylists with _$MyPlaylists { factory MyPlaylists({ List? playlists, Pagination? pagination, }) = _MyPlaylists; factory MyPlaylists.fromJson(Map json) => _$MyPlaylistsFromJson(json); } ================================================ FILE: lib/data/models/my_lists/my_playlists/my_playlists.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'my_playlists.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); MyPlaylists _$MyPlaylistsFromJson(Map json) { return _MyPlaylists.fromJson(json); } /// @nodoc mixin _$MyPlaylists { List? get playlists => throw _privateConstructorUsedError; Pagination? get pagination => throw _privateConstructorUsedError; /// Serializes this MyPlaylists to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of MyPlaylists /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $MyPlaylistsCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $MyPlaylistsCopyWith<$Res> { factory $MyPlaylistsCopyWith( MyPlaylists value, $Res Function(MyPlaylists) then) = _$MyPlaylistsCopyWithImpl<$Res, MyPlaylists>; @useResult $Res call({List? playlists, Pagination? pagination}); $PaginationCopyWith<$Res>? get pagination; } /// @nodoc class _$MyPlaylistsCopyWithImpl<$Res, $Val extends MyPlaylists> implements $MyPlaylistsCopyWith<$Res> { _$MyPlaylistsCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of MyPlaylists /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? playlists = freezed, Object? pagination = freezed, }) { return _then(_value.copyWith( playlists: freezed == playlists ? _value.playlists : playlists // ignore: cast_nullable_to_non_nullable as List?, pagination: freezed == pagination ? _value.pagination : pagination // ignore: cast_nullable_to_non_nullable as Pagination?, ) as $Val); } /// Create a copy of MyPlaylists /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $PaginationCopyWith<$Res>? get pagination { if (_value.pagination == null) { return null; } return $PaginationCopyWith<$Res>(_value.pagination!, (value) { return _then(_value.copyWith(pagination: value) as $Val); }); } } /// @nodoc abstract class _$$MyPlaylistsImplCopyWith<$Res> implements $MyPlaylistsCopyWith<$Res> { factory _$$MyPlaylistsImplCopyWith( _$MyPlaylistsImpl value, $Res Function(_$MyPlaylistsImpl) then) = __$$MyPlaylistsImplCopyWithImpl<$Res>; @override @useResult $Res call({List? playlists, Pagination? pagination}); @override $PaginationCopyWith<$Res>? get pagination; } /// @nodoc class __$$MyPlaylistsImplCopyWithImpl<$Res> extends _$MyPlaylistsCopyWithImpl<$Res, _$MyPlaylistsImpl> implements _$$MyPlaylistsImplCopyWith<$Res> { __$$MyPlaylistsImplCopyWithImpl( _$MyPlaylistsImpl _value, $Res Function(_$MyPlaylistsImpl) _then) : super(_value, _then); /// Create a copy of MyPlaylists /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? playlists = freezed, Object? pagination = freezed, }) { return _then(_$MyPlaylistsImpl( playlists: freezed == playlists ? _value._playlists : playlists // ignore: cast_nullable_to_non_nullable as List?, pagination: freezed == pagination ? _value.pagination : pagination // ignore: cast_nullable_to_non_nullable as Pagination?, )); } } /// @nodoc @JsonSerializable() class _$MyPlaylistsImpl implements _MyPlaylists { _$MyPlaylistsImpl({final List? playlists, this.pagination}) : _playlists = playlists; factory _$MyPlaylistsImpl.fromJson(Map json) => _$$MyPlaylistsImplFromJson(json); final List? _playlists; @override List? get playlists { final value = _playlists; if (value == null) return null; if (_playlists is EqualUnmodifiableListView) return _playlists; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(value); } @override final Pagination? pagination; @override String toString() { return 'MyPlaylists(playlists: $playlists, pagination: $pagination)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$MyPlaylistsImpl && const DeepCollectionEquality() .equals(other._playlists, _playlists) && (identical(other.pagination, pagination) || other.pagination == pagination)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, const DeepCollectionEquality().hash(_playlists), pagination); /// Create a copy of MyPlaylists /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$MyPlaylistsImplCopyWith<_$MyPlaylistsImpl> get copyWith => __$$MyPlaylistsImplCopyWithImpl<_$MyPlaylistsImpl>(this, _$identity); @override Map toJson() { return _$$MyPlaylistsImplToJson( this, ); } } abstract class _MyPlaylists implements MyPlaylists { factory _MyPlaylists( {final List? playlists, final Pagination? pagination}) = _$MyPlaylistsImpl; factory _MyPlaylists.fromJson(Map json) = _$MyPlaylistsImpl.fromJson; @override List? get playlists; @override Pagination? get pagination; /// Create a copy of MyPlaylists /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$MyPlaylistsImplCopyWith<_$MyPlaylistsImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/my_lists/my_playlists/my_playlists.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'my_playlists.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$MyPlaylistsImpl _$$MyPlaylistsImplFromJson(Map json) => _$MyPlaylistsImpl( playlists: (json['playlists'] as List?) ?.map((e) => Playlist.fromJson(e as Map)) .toList(), pagination: json['pagination'] == null ? null : Pagination.fromJson(json['pagination'] as Map), ); Map _$$MyPlaylistsImplToJson(_$MyPlaylistsImpl instance) => { 'playlists': instance.playlists, 'pagination': instance.pagination, }; ================================================ FILE: lib/data/models/my_lists/my_playlists/pagination.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'pagination.freezed.dart'; part 'pagination.g.dart'; @freezed class Pagination with _$Pagination { factory Pagination({ int? page, int? pageSize, int? totalCount, }) = _Pagination; factory Pagination.fromJson(Map json) => _$PaginationFromJson(json); } ================================================ FILE: lib/data/models/my_lists/my_playlists/pagination.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'pagination.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Pagination _$PaginationFromJson(Map json) { return _Pagination.fromJson(json); } /// @nodoc mixin _$Pagination { int? get page => throw _privateConstructorUsedError; int? get pageSize => throw _privateConstructorUsedError; int? get totalCount => throw _privateConstructorUsedError; /// Serializes this Pagination to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $PaginationCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $PaginationCopyWith<$Res> { factory $PaginationCopyWith( Pagination value, $Res Function(Pagination) then) = _$PaginationCopyWithImpl<$Res, Pagination>; @useResult $Res call({int? page, int? pageSize, int? totalCount}); } /// @nodoc class _$PaginationCopyWithImpl<$Res, $Val extends Pagination> implements $PaginationCopyWith<$Res> { _$PaginationCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? page = freezed, Object? pageSize = freezed, Object? totalCount = freezed, }) { return _then(_value.copyWith( page: freezed == page ? _value.page : page // ignore: cast_nullable_to_non_nullable as int?, pageSize: freezed == pageSize ? _value.pageSize : pageSize // ignore: cast_nullable_to_non_nullable as int?, totalCount: freezed == totalCount ? _value.totalCount : totalCount // ignore: cast_nullable_to_non_nullable as int?, ) as $Val); } } /// @nodoc abstract class _$$PaginationImplCopyWith<$Res> implements $PaginationCopyWith<$Res> { factory _$$PaginationImplCopyWith( _$PaginationImpl value, $Res Function(_$PaginationImpl) then) = __$$PaginationImplCopyWithImpl<$Res>; @override @useResult $Res call({int? page, int? pageSize, int? totalCount}); } /// @nodoc class __$$PaginationImplCopyWithImpl<$Res> extends _$PaginationCopyWithImpl<$Res, _$PaginationImpl> implements _$$PaginationImplCopyWith<$Res> { __$$PaginationImplCopyWithImpl( _$PaginationImpl _value, $Res Function(_$PaginationImpl) _then) : super(_value, _then); /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? page = freezed, Object? pageSize = freezed, Object? totalCount = freezed, }) { return _then(_$PaginationImpl( page: freezed == page ? _value.page : page // ignore: cast_nullable_to_non_nullable as int?, pageSize: freezed == pageSize ? _value.pageSize : pageSize // ignore: cast_nullable_to_non_nullable as int?, totalCount: freezed == totalCount ? _value.totalCount : totalCount // ignore: cast_nullable_to_non_nullable as int?, )); } } /// @nodoc @JsonSerializable() class _$PaginationImpl implements _Pagination { _$PaginationImpl({this.page, this.pageSize, this.totalCount}); factory _$PaginationImpl.fromJson(Map json) => _$$PaginationImplFromJson(json); @override final int? page; @override final int? pageSize; @override final int? totalCount; @override String toString() { return 'Pagination(page: $page, pageSize: $pageSize, totalCount: $totalCount)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PaginationImpl && (identical(other.page, page) || other.page == page) && (identical(other.pageSize, pageSize) || other.pageSize == pageSize) && (identical(other.totalCount, totalCount) || other.totalCount == totalCount)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, page, pageSize, totalCount); /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PaginationImplCopyWith<_$PaginationImpl> get copyWith => __$$PaginationImplCopyWithImpl<_$PaginationImpl>(this, _$identity); @override Map toJson() { return _$$PaginationImplToJson( this, ); } } abstract class _Pagination implements Pagination { factory _Pagination( {final int? page, final int? pageSize, final int? totalCount}) = _$PaginationImpl; factory _Pagination.fromJson(Map json) = _$PaginationImpl.fromJson; @override int? get page; @override int? get pageSize; @override int? get totalCount; /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$PaginationImplCopyWith<_$PaginationImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/my_lists/my_playlists/pagination.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'pagination.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$PaginationImpl _$$PaginationImplFromJson(Map json) => _$PaginationImpl( page: (json['page'] as num?)?.toInt(), pageSize: (json['pageSize'] as num?)?.toInt(), totalCount: (json['totalCount'] as num?)?.toInt(), ); Map _$$PaginationImplToJson(_$PaginationImpl instance) => { 'page': instance.page, 'pageSize': instance.pageSize, 'totalCount': instance.totalCount, }; ================================================ FILE: lib/data/models/my_lists/my_playlists/playlist.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'playlist.freezed.dart'; part 'playlist.g.dart'; @freezed class Playlist with _$Playlist { factory Playlist({ String? id, @JsonKey(name: 'user_name') String? userName, int? privacy, String? locale, @JsonKey(name: 'playback_count') int? playbackCount, String? name, String? description, @JsonKey(name: 'created_at') String? createdAt, @JsonKey(name: 'updated_at') String? updatedAt, @JsonKey(name: 'works_count') int? worksCount, @JsonKey(name: 'latestWorkID') dynamic latestWorkId, String? mainCoverUrl, }) = _Playlist; factory Playlist.fromJson(Map json) => _$PlaylistFromJson(json); } ================================================ FILE: lib/data/models/my_lists/my_playlists/playlist.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'playlist.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Playlist _$PlaylistFromJson(Map json) { return _Playlist.fromJson(json); } /// @nodoc mixin _$Playlist { String? get id => throw _privateConstructorUsedError; @JsonKey(name: 'user_name') String? get userName => throw _privateConstructorUsedError; int? get privacy => throw _privateConstructorUsedError; String? get locale => throw _privateConstructorUsedError; @JsonKey(name: 'playback_count') int? get playbackCount => throw _privateConstructorUsedError; String? get name => throw _privateConstructorUsedError; String? get description => throw _privateConstructorUsedError; @JsonKey(name: 'created_at') String? get createdAt => throw _privateConstructorUsedError; @JsonKey(name: 'updated_at') String? get updatedAt => throw _privateConstructorUsedError; @JsonKey(name: 'works_count') int? get worksCount => throw _privateConstructorUsedError; @JsonKey(name: 'latestWorkID') dynamic get latestWorkId => throw _privateConstructorUsedError; String? get mainCoverUrl => throw _privateConstructorUsedError; /// Serializes this Playlist to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $PlaylistCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $PlaylistCopyWith<$Res> { factory $PlaylistCopyWith(Playlist value, $Res Function(Playlist) then) = _$PlaylistCopyWithImpl<$Res, Playlist>; @useResult $Res call( {String? id, @JsonKey(name: 'user_name') String? userName, int? privacy, String? locale, @JsonKey(name: 'playback_count') int? playbackCount, String? name, String? description, @JsonKey(name: 'created_at') String? createdAt, @JsonKey(name: 'updated_at') String? updatedAt, @JsonKey(name: 'works_count') int? worksCount, @JsonKey(name: 'latestWorkID') dynamic latestWorkId, String? mainCoverUrl}); } /// @nodoc class _$PlaylistCopyWithImpl<$Res, $Val extends Playlist> implements $PlaylistCopyWith<$Res> { _$PlaylistCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? userName = freezed, Object? privacy = freezed, Object? locale = freezed, Object? playbackCount = freezed, Object? name = freezed, Object? description = freezed, Object? createdAt = freezed, Object? updatedAt = freezed, Object? worksCount = freezed, Object? latestWorkId = freezed, Object? mainCoverUrl = freezed, }) { return _then(_value.copyWith( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as String?, userName: freezed == userName ? _value.userName : userName // ignore: cast_nullable_to_non_nullable as String?, privacy: freezed == privacy ? _value.privacy : privacy // ignore: cast_nullable_to_non_nullable as int?, locale: freezed == locale ? _value.locale : locale // ignore: cast_nullable_to_non_nullable as String?, playbackCount: freezed == playbackCount ? _value.playbackCount : playbackCount // ignore: cast_nullable_to_non_nullable as int?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, description: freezed == description ? _value.description : description // ignore: cast_nullable_to_non_nullable as String?, createdAt: freezed == createdAt ? _value.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as String?, updatedAt: freezed == updatedAt ? _value.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as String?, worksCount: freezed == worksCount ? _value.worksCount : worksCount // ignore: cast_nullable_to_non_nullable as int?, latestWorkId: freezed == latestWorkId ? _value.latestWorkId : latestWorkId // ignore: cast_nullable_to_non_nullable as dynamic, mainCoverUrl: freezed == mainCoverUrl ? _value.mainCoverUrl : mainCoverUrl // ignore: cast_nullable_to_non_nullable as String?, ) as $Val); } } /// @nodoc abstract class _$$PlaylistImplCopyWith<$Res> implements $PlaylistCopyWith<$Res> { factory _$$PlaylistImplCopyWith( _$PlaylistImpl value, $Res Function(_$PlaylistImpl) then) = __$$PlaylistImplCopyWithImpl<$Res>; @override @useResult $Res call( {String? id, @JsonKey(name: 'user_name') String? userName, int? privacy, String? locale, @JsonKey(name: 'playback_count') int? playbackCount, String? name, String? description, @JsonKey(name: 'created_at') String? createdAt, @JsonKey(name: 'updated_at') String? updatedAt, @JsonKey(name: 'works_count') int? worksCount, @JsonKey(name: 'latestWorkID') dynamic latestWorkId, String? mainCoverUrl}); } /// @nodoc class __$$PlaylistImplCopyWithImpl<$Res> extends _$PlaylistCopyWithImpl<$Res, _$PlaylistImpl> implements _$$PlaylistImplCopyWith<$Res> { __$$PlaylistImplCopyWithImpl( _$PlaylistImpl _value, $Res Function(_$PlaylistImpl) _then) : super(_value, _then); /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? userName = freezed, Object? privacy = freezed, Object? locale = freezed, Object? playbackCount = freezed, Object? name = freezed, Object? description = freezed, Object? createdAt = freezed, Object? updatedAt = freezed, Object? worksCount = freezed, Object? latestWorkId = freezed, Object? mainCoverUrl = freezed, }) { return _then(_$PlaylistImpl( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as String?, userName: freezed == userName ? _value.userName : userName // ignore: cast_nullable_to_non_nullable as String?, privacy: freezed == privacy ? _value.privacy : privacy // ignore: cast_nullable_to_non_nullable as int?, locale: freezed == locale ? _value.locale : locale // ignore: cast_nullable_to_non_nullable as String?, playbackCount: freezed == playbackCount ? _value.playbackCount : playbackCount // ignore: cast_nullable_to_non_nullable as int?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, description: freezed == description ? _value.description : description // ignore: cast_nullable_to_non_nullable as String?, createdAt: freezed == createdAt ? _value.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as String?, updatedAt: freezed == updatedAt ? _value.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as String?, worksCount: freezed == worksCount ? _value.worksCount : worksCount // ignore: cast_nullable_to_non_nullable as int?, latestWorkId: freezed == latestWorkId ? _value.latestWorkId : latestWorkId // ignore: cast_nullable_to_non_nullable as dynamic, mainCoverUrl: freezed == mainCoverUrl ? _value.mainCoverUrl : mainCoverUrl // ignore: cast_nullable_to_non_nullable as String?, )); } } /// @nodoc @JsonSerializable() class _$PlaylistImpl implements _Playlist { _$PlaylistImpl( {this.id, @JsonKey(name: 'user_name') this.userName, this.privacy, this.locale, @JsonKey(name: 'playback_count') this.playbackCount, this.name, this.description, @JsonKey(name: 'created_at') this.createdAt, @JsonKey(name: 'updated_at') this.updatedAt, @JsonKey(name: 'works_count') this.worksCount, @JsonKey(name: 'latestWorkID') this.latestWorkId, this.mainCoverUrl}); factory _$PlaylistImpl.fromJson(Map json) => _$$PlaylistImplFromJson(json); @override final String? id; @override @JsonKey(name: 'user_name') final String? userName; @override final int? privacy; @override final String? locale; @override @JsonKey(name: 'playback_count') final int? playbackCount; @override final String? name; @override final String? description; @override @JsonKey(name: 'created_at') final String? createdAt; @override @JsonKey(name: 'updated_at') final String? updatedAt; @override @JsonKey(name: 'works_count') final int? worksCount; @override @JsonKey(name: 'latestWorkID') final dynamic latestWorkId; @override final String? mainCoverUrl; @override String toString() { return 'Playlist(id: $id, userName: $userName, privacy: $privacy, locale: $locale, playbackCount: $playbackCount, name: $name, description: $description, createdAt: $createdAt, updatedAt: $updatedAt, worksCount: $worksCount, latestWorkId: $latestWorkId, mainCoverUrl: $mainCoverUrl)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PlaylistImpl && (identical(other.id, id) || other.id == id) && (identical(other.userName, userName) || other.userName == userName) && (identical(other.privacy, privacy) || other.privacy == privacy) && (identical(other.locale, locale) || other.locale == locale) && (identical(other.playbackCount, playbackCount) || other.playbackCount == playbackCount) && (identical(other.name, name) || other.name == name) && (identical(other.description, description) || other.description == description) && (identical(other.createdAt, createdAt) || other.createdAt == createdAt) && (identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt) && (identical(other.worksCount, worksCount) || other.worksCount == worksCount) && const DeepCollectionEquality() .equals(other.latestWorkId, latestWorkId) && (identical(other.mainCoverUrl, mainCoverUrl) || other.mainCoverUrl == mainCoverUrl)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, id, userName, privacy, locale, playbackCount, name, description, createdAt, updatedAt, worksCount, const DeepCollectionEquality().hash(latestWorkId), mainCoverUrl); /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PlaylistImplCopyWith<_$PlaylistImpl> get copyWith => __$$PlaylistImplCopyWithImpl<_$PlaylistImpl>(this, _$identity); @override Map toJson() { return _$$PlaylistImplToJson( this, ); } } abstract class _Playlist implements Playlist { factory _Playlist( {final String? id, @JsonKey(name: 'user_name') final String? userName, final int? privacy, final String? locale, @JsonKey(name: 'playback_count') final int? playbackCount, final String? name, final String? description, @JsonKey(name: 'created_at') final String? createdAt, @JsonKey(name: 'updated_at') final String? updatedAt, @JsonKey(name: 'works_count') final int? worksCount, @JsonKey(name: 'latestWorkID') final dynamic latestWorkId, final String? mainCoverUrl}) = _$PlaylistImpl; factory _Playlist.fromJson(Map json) = _$PlaylistImpl.fromJson; @override String? get id; @override @JsonKey(name: 'user_name') String? get userName; @override int? get privacy; @override String? get locale; @override @JsonKey(name: 'playback_count') int? get playbackCount; @override String? get name; @override String? get description; @override @JsonKey(name: 'created_at') String? get createdAt; @override @JsonKey(name: 'updated_at') String? get updatedAt; @override @JsonKey(name: 'works_count') int? get worksCount; @override @JsonKey(name: 'latestWorkID') dynamic get latestWorkId; @override String? get mainCoverUrl; /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$PlaylistImplCopyWith<_$PlaylistImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/my_lists/my_playlists/playlist.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'playlist.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$PlaylistImpl _$$PlaylistImplFromJson(Map json) => _$PlaylistImpl( id: json['id'] as String?, userName: json['user_name'] as String?, privacy: (json['privacy'] as num?)?.toInt(), locale: json['locale'] as String?, playbackCount: (json['playback_count'] as num?)?.toInt(), name: json['name'] as String?, description: json['description'] as String?, createdAt: json['created_at'] as String?, updatedAt: json['updated_at'] as String?, worksCount: (json['works_count'] as num?)?.toInt(), latestWorkId: json['latestWorkID'], mainCoverUrl: json['mainCoverUrl'] as String?, ); Map _$$PlaylistImplToJson(_$PlaylistImpl instance) => { 'id': instance.id, 'user_name': instance.userName, 'privacy': instance.privacy, 'locale': instance.locale, 'playback_count': instance.playbackCount, 'name': instance.name, 'description': instance.description, 'created_at': instance.createdAt, 'updated_at': instance.updatedAt, 'works_count': instance.worksCount, 'latestWorkID': instance.latestWorkId, 'mainCoverUrl': instance.mainCoverUrl, }; ================================================ FILE: lib/data/models/playback/playback_state.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/data/models/files/files.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/core/audio/models/play_mode.dart'; part 'playback_state.freezed.dart'; part 'playback_state.g.dart'; @freezed class PlaybackState with _$PlaybackState { const factory PlaybackState({ required Work work, required Files files, required Child currentFile, required List playlist, required int currentIndex, required PlayMode playMode, required int position, // 使用毫秒存储 required String timestamp, // ISO8601 格式 }) = _PlaybackState; factory PlaybackState.fromJson(Map json) => _$PlaybackStateFromJson(json); } ================================================ FILE: lib/data/models/playback/playback_state.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'playback_state.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); PlaybackState _$PlaybackStateFromJson(Map json) { return _PlaybackState.fromJson(json); } /// @nodoc mixin _$PlaybackState { Work get work => throw _privateConstructorUsedError; Files get files => throw _privateConstructorUsedError; Child get currentFile => throw _privateConstructorUsedError; List get playlist => throw _privateConstructorUsedError; int get currentIndex => throw _privateConstructorUsedError; PlayMode get playMode => throw _privateConstructorUsedError; int get position => throw _privateConstructorUsedError; // 使用毫秒存储 String get timestamp => throw _privateConstructorUsedError; /// Serializes this PlaybackState to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of PlaybackState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $PlaybackStateCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $PlaybackStateCopyWith<$Res> { factory $PlaybackStateCopyWith( PlaybackState value, $Res Function(PlaybackState) then) = _$PlaybackStateCopyWithImpl<$Res, PlaybackState>; @useResult $Res call( {Work work, Files files, Child currentFile, List playlist, int currentIndex, PlayMode playMode, int position, String timestamp}); $WorkCopyWith<$Res> get work; $FilesCopyWith<$Res> get files; $ChildCopyWith<$Res> get currentFile; } /// @nodoc class _$PlaybackStateCopyWithImpl<$Res, $Val extends PlaybackState> implements $PlaybackStateCopyWith<$Res> { _$PlaybackStateCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of PlaybackState /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? work = null, Object? files = null, Object? currentFile = null, Object? playlist = null, Object? currentIndex = null, Object? playMode = null, Object? position = null, Object? timestamp = null, }) { return _then(_value.copyWith( work: null == work ? _value.work : work // ignore: cast_nullable_to_non_nullable as Work, files: null == files ? _value.files : files // ignore: cast_nullable_to_non_nullable as Files, currentFile: null == currentFile ? _value.currentFile : currentFile // ignore: cast_nullable_to_non_nullable as Child, playlist: null == playlist ? _value.playlist : playlist // ignore: cast_nullable_to_non_nullable as List, currentIndex: null == currentIndex ? _value.currentIndex : currentIndex // ignore: cast_nullable_to_non_nullable as int, playMode: null == playMode ? _value.playMode : playMode // ignore: cast_nullable_to_non_nullable as PlayMode, position: null == position ? _value.position : position // ignore: cast_nullable_to_non_nullable as int, timestamp: null == timestamp ? _value.timestamp : timestamp // ignore: cast_nullable_to_non_nullable as String, ) as $Val); } /// Create a copy of PlaybackState /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $WorkCopyWith<$Res> get work { return $WorkCopyWith<$Res>(_value.work, (value) { return _then(_value.copyWith(work: value) as $Val); }); } /// Create a copy of PlaybackState /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $FilesCopyWith<$Res> get files { return $FilesCopyWith<$Res>(_value.files, (value) { return _then(_value.copyWith(files: value) as $Val); }); } /// Create a copy of PlaybackState /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $ChildCopyWith<$Res> get currentFile { return $ChildCopyWith<$Res>(_value.currentFile, (value) { return _then(_value.copyWith(currentFile: value) as $Val); }); } } /// @nodoc abstract class _$$PlaybackStateImplCopyWith<$Res> implements $PlaybackStateCopyWith<$Res> { factory _$$PlaybackStateImplCopyWith( _$PlaybackStateImpl value, $Res Function(_$PlaybackStateImpl) then) = __$$PlaybackStateImplCopyWithImpl<$Res>; @override @useResult $Res call( {Work work, Files files, Child currentFile, List playlist, int currentIndex, PlayMode playMode, int position, String timestamp}); @override $WorkCopyWith<$Res> get work; @override $FilesCopyWith<$Res> get files; @override $ChildCopyWith<$Res> get currentFile; } /// @nodoc class __$$PlaybackStateImplCopyWithImpl<$Res> extends _$PlaybackStateCopyWithImpl<$Res, _$PlaybackStateImpl> implements _$$PlaybackStateImplCopyWith<$Res> { __$$PlaybackStateImplCopyWithImpl( _$PlaybackStateImpl _value, $Res Function(_$PlaybackStateImpl) _then) : super(_value, _then); /// Create a copy of PlaybackState /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? work = null, Object? files = null, Object? currentFile = null, Object? playlist = null, Object? currentIndex = null, Object? playMode = null, Object? position = null, Object? timestamp = null, }) { return _then(_$PlaybackStateImpl( work: null == work ? _value.work : work // ignore: cast_nullable_to_non_nullable as Work, files: null == files ? _value.files : files // ignore: cast_nullable_to_non_nullable as Files, currentFile: null == currentFile ? _value.currentFile : currentFile // ignore: cast_nullable_to_non_nullable as Child, playlist: null == playlist ? _value._playlist : playlist // ignore: cast_nullable_to_non_nullable as List, currentIndex: null == currentIndex ? _value.currentIndex : currentIndex // ignore: cast_nullable_to_non_nullable as int, playMode: null == playMode ? _value.playMode : playMode // ignore: cast_nullable_to_non_nullable as PlayMode, position: null == position ? _value.position : position // ignore: cast_nullable_to_non_nullable as int, timestamp: null == timestamp ? _value.timestamp : timestamp // ignore: cast_nullable_to_non_nullable as String, )); } } /// @nodoc @JsonSerializable() class _$PlaybackStateImpl implements _PlaybackState { const _$PlaybackStateImpl( {required this.work, required this.files, required this.currentFile, required final List playlist, required this.currentIndex, required this.playMode, required this.position, required this.timestamp}) : _playlist = playlist; factory _$PlaybackStateImpl.fromJson(Map json) => _$$PlaybackStateImplFromJson(json); @override final Work work; @override final Files files; @override final Child currentFile; final List _playlist; @override List get playlist { if (_playlist is EqualUnmodifiableListView) return _playlist; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_playlist); } @override final int currentIndex; @override final PlayMode playMode; @override final int position; // 使用毫秒存储 @override final String timestamp; @override String toString() { return 'PlaybackState(work: $work, files: $files, currentFile: $currentFile, playlist: $playlist, currentIndex: $currentIndex, playMode: $playMode, position: $position, timestamp: $timestamp)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PlaybackStateImpl && (identical(other.work, work) || other.work == work) && (identical(other.files, files) || other.files == files) && (identical(other.currentFile, currentFile) || other.currentFile == currentFile) && const DeepCollectionEquality().equals(other._playlist, _playlist) && (identical(other.currentIndex, currentIndex) || other.currentIndex == currentIndex) && (identical(other.playMode, playMode) || other.playMode == playMode) && (identical(other.position, position) || other.position == position) && (identical(other.timestamp, timestamp) || other.timestamp == timestamp)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, work, files, currentFile, const DeepCollectionEquality().hash(_playlist), currentIndex, playMode, position, timestamp); /// Create a copy of PlaybackState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PlaybackStateImplCopyWith<_$PlaybackStateImpl> get copyWith => __$$PlaybackStateImplCopyWithImpl<_$PlaybackStateImpl>(this, _$identity); @override Map toJson() { return _$$PlaybackStateImplToJson( this, ); } } abstract class _PlaybackState implements PlaybackState { const factory _PlaybackState( {required final Work work, required final Files files, required final Child currentFile, required final List playlist, required final int currentIndex, required final PlayMode playMode, required final int position, required final String timestamp}) = _$PlaybackStateImpl; factory _PlaybackState.fromJson(Map json) = _$PlaybackStateImpl.fromJson; @override Work get work; @override Files get files; @override Child get currentFile; @override List get playlist; @override int get currentIndex; @override PlayMode get playMode; @override int get position; // 使用毫秒存储 @override String get timestamp; /// Create a copy of PlaybackState /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$PlaybackStateImplCopyWith<_$PlaybackStateImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/playback/playback_state.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'playback_state.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$PlaybackStateImpl _$$PlaybackStateImplFromJson(Map json) => _$PlaybackStateImpl( work: Work.fromJson(json['work'] as Map), files: Files.fromJson(json['files'] as Map), currentFile: Child.fromJson(json['currentFile'] as Map), playlist: (json['playlist'] as List) .map((e) => Child.fromJson(e as Map)) .toList(), currentIndex: (json['currentIndex'] as num).toInt(), playMode: $enumDecode(_$PlayModeEnumMap, json['playMode']), position: (json['position'] as num).toInt(), timestamp: json['timestamp'] as String, ); Map _$$PlaybackStateImplToJson(_$PlaybackStateImpl instance) => { 'work': instance.work, 'files': instance.files, 'currentFile': instance.currentFile, 'playlist': instance.playlist, 'currentIndex': instance.currentIndex, 'playMode': _$PlayModeEnumMap[instance.playMode]!, 'position': instance.position, 'timestamp': instance.timestamp, }; const _$PlayModeEnumMap = { PlayMode.single: 'single', PlayMode.loop: 'loop', PlayMode.sequence: 'sequence', }; ================================================ FILE: lib/data/models/playlists_with_exist_statu/pagination.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'pagination.freezed.dart'; part 'pagination.g.dart'; @freezed class Pagination with _$Pagination { factory Pagination({ int? page, int? pageSize, int? totalCount, }) = _Pagination; factory Pagination.fromJson(Map json) => _$PaginationFromJson(json); } ================================================ FILE: lib/data/models/playlists_with_exist_statu/pagination.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'pagination.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Pagination _$PaginationFromJson(Map json) { return _Pagination.fromJson(json); } /// @nodoc mixin _$Pagination { int? get page => throw _privateConstructorUsedError; int? get pageSize => throw _privateConstructorUsedError; int? get totalCount => throw _privateConstructorUsedError; /// Serializes this Pagination to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $PaginationCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $PaginationCopyWith<$Res> { factory $PaginationCopyWith( Pagination value, $Res Function(Pagination) then) = _$PaginationCopyWithImpl<$Res, Pagination>; @useResult $Res call({int? page, int? pageSize, int? totalCount}); } /// @nodoc class _$PaginationCopyWithImpl<$Res, $Val extends Pagination> implements $PaginationCopyWith<$Res> { _$PaginationCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? page = freezed, Object? pageSize = freezed, Object? totalCount = freezed, }) { return _then(_value.copyWith( page: freezed == page ? _value.page : page // ignore: cast_nullable_to_non_nullable as int?, pageSize: freezed == pageSize ? _value.pageSize : pageSize // ignore: cast_nullable_to_non_nullable as int?, totalCount: freezed == totalCount ? _value.totalCount : totalCount // ignore: cast_nullable_to_non_nullable as int?, ) as $Val); } } /// @nodoc abstract class _$$PaginationImplCopyWith<$Res> implements $PaginationCopyWith<$Res> { factory _$$PaginationImplCopyWith( _$PaginationImpl value, $Res Function(_$PaginationImpl) then) = __$$PaginationImplCopyWithImpl<$Res>; @override @useResult $Res call({int? page, int? pageSize, int? totalCount}); } /// @nodoc class __$$PaginationImplCopyWithImpl<$Res> extends _$PaginationCopyWithImpl<$Res, _$PaginationImpl> implements _$$PaginationImplCopyWith<$Res> { __$$PaginationImplCopyWithImpl( _$PaginationImpl _value, $Res Function(_$PaginationImpl) _then) : super(_value, _then); /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? page = freezed, Object? pageSize = freezed, Object? totalCount = freezed, }) { return _then(_$PaginationImpl( page: freezed == page ? _value.page : page // ignore: cast_nullable_to_non_nullable as int?, pageSize: freezed == pageSize ? _value.pageSize : pageSize // ignore: cast_nullable_to_non_nullable as int?, totalCount: freezed == totalCount ? _value.totalCount : totalCount // ignore: cast_nullable_to_non_nullable as int?, )); } } /// @nodoc @JsonSerializable() class _$PaginationImpl implements _Pagination { _$PaginationImpl({this.page, this.pageSize, this.totalCount}); factory _$PaginationImpl.fromJson(Map json) => _$$PaginationImplFromJson(json); @override final int? page; @override final int? pageSize; @override final int? totalCount; @override String toString() { return 'Pagination(page: $page, pageSize: $pageSize, totalCount: $totalCount)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PaginationImpl && (identical(other.page, page) || other.page == page) && (identical(other.pageSize, pageSize) || other.pageSize == pageSize) && (identical(other.totalCount, totalCount) || other.totalCount == totalCount)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, page, pageSize, totalCount); /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PaginationImplCopyWith<_$PaginationImpl> get copyWith => __$$PaginationImplCopyWithImpl<_$PaginationImpl>(this, _$identity); @override Map toJson() { return _$$PaginationImplToJson( this, ); } } abstract class _Pagination implements Pagination { factory _Pagination( {final int? page, final int? pageSize, final int? totalCount}) = _$PaginationImpl; factory _Pagination.fromJson(Map json) = _$PaginationImpl.fromJson; @override int? get page; @override int? get pageSize; @override int? get totalCount; /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$PaginationImplCopyWith<_$PaginationImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/playlists_with_exist_statu/pagination.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'pagination.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$PaginationImpl _$$PaginationImplFromJson(Map json) => _$PaginationImpl( page: (json['page'] as num?)?.toInt(), pageSize: (json['pageSize'] as num?)?.toInt(), totalCount: (json['totalCount'] as num?)?.toInt(), ); Map _$$PaginationImplToJson(_$PaginationImpl instance) => { 'page': instance.page, 'pageSize': instance.pageSize, 'totalCount': instance.totalCount, }; ================================================ FILE: lib/data/models/playlists_with_exist_statu/playlist.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'playlist.freezed.dart'; part 'playlist.g.dart'; @freezed class Playlist with _$Playlist { factory Playlist({ String? id, @JsonKey(name: 'user_name') String? userName, int? privacy, String? locale, @JsonKey(name: 'playback_count') int? playbackCount, String? name, String? description, @JsonKey(name: 'created_at') String? createdAt, @JsonKey(name: 'updated_at') String? updatedAt, @JsonKey(name: 'works_count') int? worksCount, bool? exist, }) = _Playlist; factory Playlist.fromJson(Map json) => _$PlaylistFromJson(json); } ================================================ FILE: lib/data/models/playlists_with_exist_statu/playlist.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'playlist.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Playlist _$PlaylistFromJson(Map json) { return _Playlist.fromJson(json); } /// @nodoc mixin _$Playlist { String? get id => throw _privateConstructorUsedError; @JsonKey(name: 'user_name') String? get userName => throw _privateConstructorUsedError; int? get privacy => throw _privateConstructorUsedError; String? get locale => throw _privateConstructorUsedError; @JsonKey(name: 'playback_count') int? get playbackCount => throw _privateConstructorUsedError; String? get name => throw _privateConstructorUsedError; String? get description => throw _privateConstructorUsedError; @JsonKey(name: 'created_at') String? get createdAt => throw _privateConstructorUsedError; @JsonKey(name: 'updated_at') String? get updatedAt => throw _privateConstructorUsedError; @JsonKey(name: 'works_count') int? get worksCount => throw _privateConstructorUsedError; bool? get exist => throw _privateConstructorUsedError; /// Serializes this Playlist to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $PlaylistCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $PlaylistCopyWith<$Res> { factory $PlaylistCopyWith(Playlist value, $Res Function(Playlist) then) = _$PlaylistCopyWithImpl<$Res, Playlist>; @useResult $Res call( {String? id, @JsonKey(name: 'user_name') String? userName, int? privacy, String? locale, @JsonKey(name: 'playback_count') int? playbackCount, String? name, String? description, @JsonKey(name: 'created_at') String? createdAt, @JsonKey(name: 'updated_at') String? updatedAt, @JsonKey(name: 'works_count') int? worksCount, bool? exist}); } /// @nodoc class _$PlaylistCopyWithImpl<$Res, $Val extends Playlist> implements $PlaylistCopyWith<$Res> { _$PlaylistCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? userName = freezed, Object? privacy = freezed, Object? locale = freezed, Object? playbackCount = freezed, Object? name = freezed, Object? description = freezed, Object? createdAt = freezed, Object? updatedAt = freezed, Object? worksCount = freezed, Object? exist = freezed, }) { return _then(_value.copyWith( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as String?, userName: freezed == userName ? _value.userName : userName // ignore: cast_nullable_to_non_nullable as String?, privacy: freezed == privacy ? _value.privacy : privacy // ignore: cast_nullable_to_non_nullable as int?, locale: freezed == locale ? _value.locale : locale // ignore: cast_nullable_to_non_nullable as String?, playbackCount: freezed == playbackCount ? _value.playbackCount : playbackCount // ignore: cast_nullable_to_non_nullable as int?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, description: freezed == description ? _value.description : description // ignore: cast_nullable_to_non_nullable as String?, createdAt: freezed == createdAt ? _value.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as String?, updatedAt: freezed == updatedAt ? _value.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as String?, worksCount: freezed == worksCount ? _value.worksCount : worksCount // ignore: cast_nullable_to_non_nullable as int?, exist: freezed == exist ? _value.exist : exist // ignore: cast_nullable_to_non_nullable as bool?, ) as $Val); } } /// @nodoc abstract class _$$PlaylistImplCopyWith<$Res> implements $PlaylistCopyWith<$Res> { factory _$$PlaylistImplCopyWith( _$PlaylistImpl value, $Res Function(_$PlaylistImpl) then) = __$$PlaylistImplCopyWithImpl<$Res>; @override @useResult $Res call( {String? id, @JsonKey(name: 'user_name') String? userName, int? privacy, String? locale, @JsonKey(name: 'playback_count') int? playbackCount, String? name, String? description, @JsonKey(name: 'created_at') String? createdAt, @JsonKey(name: 'updated_at') String? updatedAt, @JsonKey(name: 'works_count') int? worksCount, bool? exist}); } /// @nodoc class __$$PlaylistImplCopyWithImpl<$Res> extends _$PlaylistCopyWithImpl<$Res, _$PlaylistImpl> implements _$$PlaylistImplCopyWith<$Res> { __$$PlaylistImplCopyWithImpl( _$PlaylistImpl _value, $Res Function(_$PlaylistImpl) _then) : super(_value, _then); /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? userName = freezed, Object? privacy = freezed, Object? locale = freezed, Object? playbackCount = freezed, Object? name = freezed, Object? description = freezed, Object? createdAt = freezed, Object? updatedAt = freezed, Object? worksCount = freezed, Object? exist = freezed, }) { return _then(_$PlaylistImpl( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as String?, userName: freezed == userName ? _value.userName : userName // ignore: cast_nullable_to_non_nullable as String?, privacy: freezed == privacy ? _value.privacy : privacy // ignore: cast_nullable_to_non_nullable as int?, locale: freezed == locale ? _value.locale : locale // ignore: cast_nullable_to_non_nullable as String?, playbackCount: freezed == playbackCount ? _value.playbackCount : playbackCount // ignore: cast_nullable_to_non_nullable as int?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, description: freezed == description ? _value.description : description // ignore: cast_nullable_to_non_nullable as String?, createdAt: freezed == createdAt ? _value.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as String?, updatedAt: freezed == updatedAt ? _value.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as String?, worksCount: freezed == worksCount ? _value.worksCount : worksCount // ignore: cast_nullable_to_non_nullable as int?, exist: freezed == exist ? _value.exist : exist // ignore: cast_nullable_to_non_nullable as bool?, )); } } /// @nodoc @JsonSerializable() class _$PlaylistImpl implements _Playlist { _$PlaylistImpl( {this.id, @JsonKey(name: 'user_name') this.userName, this.privacy, this.locale, @JsonKey(name: 'playback_count') this.playbackCount, this.name, this.description, @JsonKey(name: 'created_at') this.createdAt, @JsonKey(name: 'updated_at') this.updatedAt, @JsonKey(name: 'works_count') this.worksCount, this.exist}); factory _$PlaylistImpl.fromJson(Map json) => _$$PlaylistImplFromJson(json); @override final String? id; @override @JsonKey(name: 'user_name') final String? userName; @override final int? privacy; @override final String? locale; @override @JsonKey(name: 'playback_count') final int? playbackCount; @override final String? name; @override final String? description; @override @JsonKey(name: 'created_at') final String? createdAt; @override @JsonKey(name: 'updated_at') final String? updatedAt; @override @JsonKey(name: 'works_count') final int? worksCount; @override final bool? exist; @override String toString() { return 'Playlist(id: $id, userName: $userName, privacy: $privacy, locale: $locale, playbackCount: $playbackCount, name: $name, description: $description, createdAt: $createdAt, updatedAt: $updatedAt, worksCount: $worksCount, exist: $exist)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PlaylistImpl && (identical(other.id, id) || other.id == id) && (identical(other.userName, userName) || other.userName == userName) && (identical(other.privacy, privacy) || other.privacy == privacy) && (identical(other.locale, locale) || other.locale == locale) && (identical(other.playbackCount, playbackCount) || other.playbackCount == playbackCount) && (identical(other.name, name) || other.name == name) && (identical(other.description, description) || other.description == description) && (identical(other.createdAt, createdAt) || other.createdAt == createdAt) && (identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt) && (identical(other.worksCount, worksCount) || other.worksCount == worksCount) && (identical(other.exist, exist) || other.exist == exist)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, id, userName, privacy, locale, playbackCount, name, description, createdAt, updatedAt, worksCount, exist); /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PlaylistImplCopyWith<_$PlaylistImpl> get copyWith => __$$PlaylistImplCopyWithImpl<_$PlaylistImpl>(this, _$identity); @override Map toJson() { return _$$PlaylistImplToJson( this, ); } } abstract class _Playlist implements Playlist { factory _Playlist( {final String? id, @JsonKey(name: 'user_name') final String? userName, final int? privacy, final String? locale, @JsonKey(name: 'playback_count') final int? playbackCount, final String? name, final String? description, @JsonKey(name: 'created_at') final String? createdAt, @JsonKey(name: 'updated_at') final String? updatedAt, @JsonKey(name: 'works_count') final int? worksCount, final bool? exist}) = _$PlaylistImpl; factory _Playlist.fromJson(Map json) = _$PlaylistImpl.fromJson; @override String? get id; @override @JsonKey(name: 'user_name') String? get userName; @override int? get privacy; @override String? get locale; @override @JsonKey(name: 'playback_count') int? get playbackCount; @override String? get name; @override String? get description; @override @JsonKey(name: 'created_at') String? get createdAt; @override @JsonKey(name: 'updated_at') String? get updatedAt; @override @JsonKey(name: 'works_count') int? get worksCount; @override bool? get exist; /// Create a copy of Playlist /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$PlaylistImplCopyWith<_$PlaylistImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/playlists_with_exist_statu/playlist.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'playlist.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$PlaylistImpl _$$PlaylistImplFromJson(Map json) => _$PlaylistImpl( id: json['id'] as String?, userName: json['user_name'] as String?, privacy: (json['privacy'] as num?)?.toInt(), locale: json['locale'] as String?, playbackCount: (json['playback_count'] as num?)?.toInt(), name: json['name'] as String?, description: json['description'] as String?, createdAt: json['created_at'] as String?, updatedAt: json['updated_at'] as String?, worksCount: (json['works_count'] as num?)?.toInt(), exist: json['exist'] as bool?, ); Map _$$PlaylistImplToJson(_$PlaylistImpl instance) => { 'id': instance.id, 'user_name': instance.userName, 'privacy': instance.privacy, 'locale': instance.locale, 'playback_count': instance.playbackCount, 'name': instance.name, 'description': instance.description, 'created_at': instance.createdAt, 'updated_at': instance.updatedAt, 'works_count': instance.worksCount, 'exist': instance.exist, }; ================================================ FILE: lib/data/models/playlists_with_exist_statu/playlists_with_exist_statu.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'pagination.dart'; import 'playlist.dart'; part 'playlists_with_exist_statu.freezed.dart'; part 'playlists_with_exist_statu.g.dart'; @freezed class PlaylistsWithExistStatu with _$PlaylistsWithExistStatu { factory PlaylistsWithExistStatu({ List? playlists, Pagination? pagination, }) = _PlaylistsWithExistStatu; factory PlaylistsWithExistStatu.fromJson(Map json) => _$PlaylistsWithExistStatuFromJson(json); } ================================================ FILE: lib/data/models/playlists_with_exist_statu/playlists_with_exist_statu.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'playlists_with_exist_statu.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); PlaylistsWithExistStatu _$PlaylistsWithExistStatuFromJson( Map json) { return _PlaylistsWithExistStatu.fromJson(json); } /// @nodoc mixin _$PlaylistsWithExistStatu { List? get playlists => throw _privateConstructorUsedError; Pagination? get pagination => throw _privateConstructorUsedError; /// Serializes this PlaylistsWithExistStatu to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of PlaylistsWithExistStatu /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $PlaylistsWithExistStatuCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $PlaylistsWithExistStatuCopyWith<$Res> { factory $PlaylistsWithExistStatuCopyWith(PlaylistsWithExistStatu value, $Res Function(PlaylistsWithExistStatu) then) = _$PlaylistsWithExistStatuCopyWithImpl<$Res, PlaylistsWithExistStatu>; @useResult $Res call({List? playlists, Pagination? pagination}); $PaginationCopyWith<$Res>? get pagination; } /// @nodoc class _$PlaylistsWithExistStatuCopyWithImpl<$Res, $Val extends PlaylistsWithExistStatu> implements $PlaylistsWithExistStatuCopyWith<$Res> { _$PlaylistsWithExistStatuCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of PlaylistsWithExistStatu /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? playlists = freezed, Object? pagination = freezed, }) { return _then(_value.copyWith( playlists: freezed == playlists ? _value.playlists : playlists // ignore: cast_nullable_to_non_nullable as List?, pagination: freezed == pagination ? _value.pagination : pagination // ignore: cast_nullable_to_non_nullable as Pagination?, ) as $Val); } /// Create a copy of PlaylistsWithExistStatu /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $PaginationCopyWith<$Res>? get pagination { if (_value.pagination == null) { return null; } return $PaginationCopyWith<$Res>(_value.pagination!, (value) { return _then(_value.copyWith(pagination: value) as $Val); }); } } /// @nodoc abstract class _$$PlaylistsWithExistStatuImplCopyWith<$Res> implements $PlaylistsWithExistStatuCopyWith<$Res> { factory _$$PlaylistsWithExistStatuImplCopyWith( _$PlaylistsWithExistStatuImpl value, $Res Function(_$PlaylistsWithExistStatuImpl) then) = __$$PlaylistsWithExistStatuImplCopyWithImpl<$Res>; @override @useResult $Res call({List? playlists, Pagination? pagination}); @override $PaginationCopyWith<$Res>? get pagination; } /// @nodoc class __$$PlaylistsWithExistStatuImplCopyWithImpl<$Res> extends _$PlaylistsWithExistStatuCopyWithImpl<$Res, _$PlaylistsWithExistStatuImpl> implements _$$PlaylistsWithExistStatuImplCopyWith<$Res> { __$$PlaylistsWithExistStatuImplCopyWithImpl( _$PlaylistsWithExistStatuImpl _value, $Res Function(_$PlaylistsWithExistStatuImpl) _then) : super(_value, _then); /// Create a copy of PlaylistsWithExistStatu /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? playlists = freezed, Object? pagination = freezed, }) { return _then(_$PlaylistsWithExistStatuImpl( playlists: freezed == playlists ? _value._playlists : playlists // ignore: cast_nullable_to_non_nullable as List?, pagination: freezed == pagination ? _value.pagination : pagination // ignore: cast_nullable_to_non_nullable as Pagination?, )); } } /// @nodoc @JsonSerializable() class _$PlaylistsWithExistStatuImpl implements _PlaylistsWithExistStatu { _$PlaylistsWithExistStatuImpl( {final List? playlists, this.pagination}) : _playlists = playlists; factory _$PlaylistsWithExistStatuImpl.fromJson(Map json) => _$$PlaylistsWithExistStatuImplFromJson(json); final List? _playlists; @override List? get playlists { final value = _playlists; if (value == null) return null; if (_playlists is EqualUnmodifiableListView) return _playlists; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(value); } @override final Pagination? pagination; @override String toString() { return 'PlaylistsWithExistStatu(playlists: $playlists, pagination: $pagination)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PlaylistsWithExistStatuImpl && const DeepCollectionEquality() .equals(other._playlists, _playlists) && (identical(other.pagination, pagination) || other.pagination == pagination)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, const DeepCollectionEquality().hash(_playlists), pagination); /// Create a copy of PlaylistsWithExistStatu /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PlaylistsWithExistStatuImplCopyWith<_$PlaylistsWithExistStatuImpl> get copyWith => __$$PlaylistsWithExistStatuImplCopyWithImpl< _$PlaylistsWithExistStatuImpl>(this, _$identity); @override Map toJson() { return _$$PlaylistsWithExistStatuImplToJson( this, ); } } abstract class _PlaylistsWithExistStatu implements PlaylistsWithExistStatu { factory _PlaylistsWithExistStatu( {final List? playlists, final Pagination? pagination}) = _$PlaylistsWithExistStatuImpl; factory _PlaylistsWithExistStatu.fromJson(Map json) = _$PlaylistsWithExistStatuImpl.fromJson; @override List? get playlists; @override Pagination? get pagination; /// Create a copy of PlaylistsWithExistStatu /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$PlaylistsWithExistStatuImplCopyWith<_$PlaylistsWithExistStatuImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/playlists_with_exist_statu/playlists_with_exist_statu.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'playlists_with_exist_statu.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$PlaylistsWithExistStatuImpl _$$PlaylistsWithExistStatuImplFromJson( Map json) => _$PlaylistsWithExistStatuImpl( playlists: (json['playlists'] as List?) ?.map((e) => Playlist.fromJson(e as Map)) .toList(), pagination: json['pagination'] == null ? null : Pagination.fromJson(json['pagination'] as Map), ); Map _$$PlaylistsWithExistStatuImplToJson( _$PlaylistsWithExistStatuImpl instance) => { 'playlists': instance.playlists, 'pagination': instance.pagination, }; ================================================ FILE: lib/data/models/works/circle.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'circle.freezed.dart'; part 'circle.g.dart'; @freezed class Circle with _$Circle { factory Circle({ int? id, String? name, @JsonKey(name: 'source_id') String? sourceId, @JsonKey(name: 'source_type') String? sourceType, }) = _Circle; factory Circle.fromJson(Map json) => _$CircleFromJson(json); } ================================================ FILE: lib/data/models/works/circle.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'circle.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Circle _$CircleFromJson(Map json) { return _Circle.fromJson(json); } /// @nodoc mixin _$Circle { int? get id => throw _privateConstructorUsedError; String? get name => throw _privateConstructorUsedError; @JsonKey(name: 'source_id') String? get sourceId => throw _privateConstructorUsedError; @JsonKey(name: 'source_type') String? get sourceType => throw _privateConstructorUsedError; /// Serializes this Circle to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of Circle /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $CircleCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $CircleCopyWith<$Res> { factory $CircleCopyWith(Circle value, $Res Function(Circle) then) = _$CircleCopyWithImpl<$Res, Circle>; @useResult $Res call( {int? id, String? name, @JsonKey(name: 'source_id') String? sourceId, @JsonKey(name: 'source_type') String? sourceType}); } /// @nodoc class _$CircleCopyWithImpl<$Res, $Val extends Circle> implements $CircleCopyWith<$Res> { _$CircleCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of Circle /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? name = freezed, Object? sourceId = freezed, Object? sourceType = freezed, }) { return _then(_value.copyWith( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as int?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, sourceId: freezed == sourceId ? _value.sourceId : sourceId // ignore: cast_nullable_to_non_nullable as String?, sourceType: freezed == sourceType ? _value.sourceType : sourceType // ignore: cast_nullable_to_non_nullable as String?, ) as $Val); } } /// @nodoc abstract class _$$CircleImplCopyWith<$Res> implements $CircleCopyWith<$Res> { factory _$$CircleImplCopyWith( _$CircleImpl value, $Res Function(_$CircleImpl) then) = __$$CircleImplCopyWithImpl<$Res>; @override @useResult $Res call( {int? id, String? name, @JsonKey(name: 'source_id') String? sourceId, @JsonKey(name: 'source_type') String? sourceType}); } /// @nodoc class __$$CircleImplCopyWithImpl<$Res> extends _$CircleCopyWithImpl<$Res, _$CircleImpl> implements _$$CircleImplCopyWith<$Res> { __$$CircleImplCopyWithImpl( _$CircleImpl _value, $Res Function(_$CircleImpl) _then) : super(_value, _then); /// Create a copy of Circle /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? name = freezed, Object? sourceId = freezed, Object? sourceType = freezed, }) { return _then(_$CircleImpl( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as int?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, sourceId: freezed == sourceId ? _value.sourceId : sourceId // ignore: cast_nullable_to_non_nullable as String?, sourceType: freezed == sourceType ? _value.sourceType : sourceType // ignore: cast_nullable_to_non_nullable as String?, )); } } /// @nodoc @JsonSerializable() class _$CircleImpl implements _Circle { _$CircleImpl( {this.id, this.name, @JsonKey(name: 'source_id') this.sourceId, @JsonKey(name: 'source_type') this.sourceType}); factory _$CircleImpl.fromJson(Map json) => _$$CircleImplFromJson(json); @override final int? id; @override final String? name; @override @JsonKey(name: 'source_id') final String? sourceId; @override @JsonKey(name: 'source_type') final String? sourceType; @override String toString() { return 'Circle(id: $id, name: $name, sourceId: $sourceId, sourceType: $sourceType)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$CircleImpl && (identical(other.id, id) || other.id == id) && (identical(other.name, name) || other.name == name) && (identical(other.sourceId, sourceId) || other.sourceId == sourceId) && (identical(other.sourceType, sourceType) || other.sourceType == sourceType)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, id, name, sourceId, sourceType); /// Create a copy of Circle /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$CircleImplCopyWith<_$CircleImpl> get copyWith => __$$CircleImplCopyWithImpl<_$CircleImpl>(this, _$identity); @override Map toJson() { return _$$CircleImplToJson( this, ); } } abstract class _Circle implements Circle { factory _Circle( {final int? id, final String? name, @JsonKey(name: 'source_id') final String? sourceId, @JsonKey(name: 'source_type') final String? sourceType}) = _$CircleImpl; factory _Circle.fromJson(Map json) = _$CircleImpl.fromJson; @override int? get id; @override String? get name; @override @JsonKey(name: 'source_id') String? get sourceId; @override @JsonKey(name: 'source_type') String? get sourceType; /// Create a copy of Circle /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$CircleImplCopyWith<_$CircleImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/works/circle.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'circle.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$CircleImpl _$$CircleImplFromJson(Map json) => _$CircleImpl( id: (json['id'] as num?)?.toInt(), name: json['name'] as String?, sourceId: json['source_id'] as String?, sourceType: json['source_type'] as String?, ); Map _$$CircleImplToJson(_$CircleImpl instance) => { 'id': instance.id, 'name': instance.name, 'source_id': instance.sourceId, 'source_type': instance.sourceType, }; ================================================ FILE: lib/data/models/works/en_us.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'en_us.freezed.dart'; part 'en_us.g.dart'; @freezed class EnUs with _$EnUs { factory EnUs({ String? name, List? history, }) = _EnUs; factory EnUs.fromJson(Map json) => _$EnUsFromJson(json); } ================================================ FILE: lib/data/models/works/en_us.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'en_us.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); EnUs _$EnUsFromJson(Map json) { return _EnUs.fromJson(json); } /// @nodoc mixin _$EnUs { String? get name => throw _privateConstructorUsedError; List? get history => throw _privateConstructorUsedError; /// Serializes this EnUs to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of EnUs /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $EnUsCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $EnUsCopyWith<$Res> { factory $EnUsCopyWith(EnUs value, $Res Function(EnUs) then) = _$EnUsCopyWithImpl<$Res, EnUs>; @useResult $Res call({String? name, List? history}); } /// @nodoc class _$EnUsCopyWithImpl<$Res, $Val extends EnUs> implements $EnUsCopyWith<$Res> { _$EnUsCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of EnUs /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? name = freezed, Object? history = freezed, }) { return _then(_value.copyWith( name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, history: freezed == history ? _value.history : history // ignore: cast_nullable_to_non_nullable as List?, ) as $Val); } } /// @nodoc abstract class _$$EnUsImplCopyWith<$Res> implements $EnUsCopyWith<$Res> { factory _$$EnUsImplCopyWith( _$EnUsImpl value, $Res Function(_$EnUsImpl) then) = __$$EnUsImplCopyWithImpl<$Res>; @override @useResult $Res call({String? name, List? history}); } /// @nodoc class __$$EnUsImplCopyWithImpl<$Res> extends _$EnUsCopyWithImpl<$Res, _$EnUsImpl> implements _$$EnUsImplCopyWith<$Res> { __$$EnUsImplCopyWithImpl(_$EnUsImpl _value, $Res Function(_$EnUsImpl) _then) : super(_value, _then); /// Create a copy of EnUs /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? name = freezed, Object? history = freezed, }) { return _then(_$EnUsImpl( name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, history: freezed == history ? _value._history : history // ignore: cast_nullable_to_non_nullable as List?, )); } } /// @nodoc @JsonSerializable() class _$EnUsImpl implements _EnUs { _$EnUsImpl({this.name, final List? history}) : _history = history; factory _$EnUsImpl.fromJson(Map json) => _$$EnUsImplFromJson(json); @override final String? name; final List? _history; @override List? get history { final value = _history; if (value == null) return null; if (_history is EqualUnmodifiableListView) return _history; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(value); } @override String toString() { return 'EnUs(name: $name, history: $history)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$EnUsImpl && (identical(other.name, name) || other.name == name) && const DeepCollectionEquality().equals(other._history, _history)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, name, const DeepCollectionEquality().hash(_history)); /// Create a copy of EnUs /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$EnUsImplCopyWith<_$EnUsImpl> get copyWith => __$$EnUsImplCopyWithImpl<_$EnUsImpl>(this, _$identity); @override Map toJson() { return _$$EnUsImplToJson( this, ); } } abstract class _EnUs implements EnUs { factory _EnUs({final String? name, final List? history}) = _$EnUsImpl; factory _EnUs.fromJson(Map json) = _$EnUsImpl.fromJson; @override String? get name; @override List? get history; /// Create a copy of EnUs /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$EnUsImplCopyWith<_$EnUsImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/works/en_us.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'en_us.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$EnUsImpl _$$EnUsImplFromJson(Map json) => _$EnUsImpl( name: json['name'] as String?, history: json['history'] as List?, ); Map _$$EnUsImplToJson(_$EnUsImpl instance) => { 'name': instance.name, 'history': instance.history, }; ================================================ FILE: lib/data/models/works/i18n.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'en_us.dart'; import 'ja_jp.dart'; import 'zh_cn.dart'; part 'i18n.freezed.dart'; part 'i18n.g.dart'; @freezed class I18n with _$I18n { factory I18n({ @JsonKey(name: 'en-us') EnUs? enUs, @JsonKey(name: 'ja-jp') JaJp? jaJp, @JsonKey(name: 'zh-cn') ZhCn? zhCn, }) = _I18n; factory I18n.fromJson(Map json) => _$I18nFromJson(json); } ================================================ FILE: lib/data/models/works/i18n.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'i18n.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); I18n _$I18nFromJson(Map json) { return _I18n.fromJson(json); } /// @nodoc mixin _$I18n { @JsonKey(name: 'en-us') EnUs? get enUs => throw _privateConstructorUsedError; @JsonKey(name: 'ja-jp') JaJp? get jaJp => throw _privateConstructorUsedError; @JsonKey(name: 'zh-cn') ZhCn? get zhCn => throw _privateConstructorUsedError; /// Serializes this I18n to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of I18n /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $I18nCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $I18nCopyWith<$Res> { factory $I18nCopyWith(I18n value, $Res Function(I18n) then) = _$I18nCopyWithImpl<$Res, I18n>; @useResult $Res call( {@JsonKey(name: 'en-us') EnUs? enUs, @JsonKey(name: 'ja-jp') JaJp? jaJp, @JsonKey(name: 'zh-cn') ZhCn? zhCn}); $EnUsCopyWith<$Res>? get enUs; $JaJpCopyWith<$Res>? get jaJp; $ZhCnCopyWith<$Res>? get zhCn; } /// @nodoc class _$I18nCopyWithImpl<$Res, $Val extends I18n> implements $I18nCopyWith<$Res> { _$I18nCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of I18n /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? enUs = freezed, Object? jaJp = freezed, Object? zhCn = freezed, }) { return _then(_value.copyWith( enUs: freezed == enUs ? _value.enUs : enUs // ignore: cast_nullable_to_non_nullable as EnUs?, jaJp: freezed == jaJp ? _value.jaJp : jaJp // ignore: cast_nullable_to_non_nullable as JaJp?, zhCn: freezed == zhCn ? _value.zhCn : zhCn // ignore: cast_nullable_to_non_nullable as ZhCn?, ) as $Val); } /// Create a copy of I18n /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $EnUsCopyWith<$Res>? get enUs { if (_value.enUs == null) { return null; } return $EnUsCopyWith<$Res>(_value.enUs!, (value) { return _then(_value.copyWith(enUs: value) as $Val); }); } /// Create a copy of I18n /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $JaJpCopyWith<$Res>? get jaJp { if (_value.jaJp == null) { return null; } return $JaJpCopyWith<$Res>(_value.jaJp!, (value) { return _then(_value.copyWith(jaJp: value) as $Val); }); } /// Create a copy of I18n /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $ZhCnCopyWith<$Res>? get zhCn { if (_value.zhCn == null) { return null; } return $ZhCnCopyWith<$Res>(_value.zhCn!, (value) { return _then(_value.copyWith(zhCn: value) as $Val); }); } } /// @nodoc abstract class _$$I18nImplCopyWith<$Res> implements $I18nCopyWith<$Res> { factory _$$I18nImplCopyWith( _$I18nImpl value, $Res Function(_$I18nImpl) then) = __$$I18nImplCopyWithImpl<$Res>; @override @useResult $Res call( {@JsonKey(name: 'en-us') EnUs? enUs, @JsonKey(name: 'ja-jp') JaJp? jaJp, @JsonKey(name: 'zh-cn') ZhCn? zhCn}); @override $EnUsCopyWith<$Res>? get enUs; @override $JaJpCopyWith<$Res>? get jaJp; @override $ZhCnCopyWith<$Res>? get zhCn; } /// @nodoc class __$$I18nImplCopyWithImpl<$Res> extends _$I18nCopyWithImpl<$Res, _$I18nImpl> implements _$$I18nImplCopyWith<$Res> { __$$I18nImplCopyWithImpl(_$I18nImpl _value, $Res Function(_$I18nImpl) _then) : super(_value, _then); /// Create a copy of I18n /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? enUs = freezed, Object? jaJp = freezed, Object? zhCn = freezed, }) { return _then(_$I18nImpl( enUs: freezed == enUs ? _value.enUs : enUs // ignore: cast_nullable_to_non_nullable as EnUs?, jaJp: freezed == jaJp ? _value.jaJp : jaJp // ignore: cast_nullable_to_non_nullable as JaJp?, zhCn: freezed == zhCn ? _value.zhCn : zhCn // ignore: cast_nullable_to_non_nullable as ZhCn?, )); } } /// @nodoc @JsonSerializable() class _$I18nImpl implements _I18n { _$I18nImpl( {@JsonKey(name: 'en-us') this.enUs, @JsonKey(name: 'ja-jp') this.jaJp, @JsonKey(name: 'zh-cn') this.zhCn}); factory _$I18nImpl.fromJson(Map json) => _$$I18nImplFromJson(json); @override @JsonKey(name: 'en-us') final EnUs? enUs; @override @JsonKey(name: 'ja-jp') final JaJp? jaJp; @override @JsonKey(name: 'zh-cn') final ZhCn? zhCn; @override String toString() { return 'I18n(enUs: $enUs, jaJp: $jaJp, zhCn: $zhCn)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$I18nImpl && (identical(other.enUs, enUs) || other.enUs == enUs) && (identical(other.jaJp, jaJp) || other.jaJp == jaJp) && (identical(other.zhCn, zhCn) || other.zhCn == zhCn)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, enUs, jaJp, zhCn); /// Create a copy of I18n /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$I18nImplCopyWith<_$I18nImpl> get copyWith => __$$I18nImplCopyWithImpl<_$I18nImpl>(this, _$identity); @override Map toJson() { return _$$I18nImplToJson( this, ); } } abstract class _I18n implements I18n { factory _I18n( {@JsonKey(name: 'en-us') final EnUs? enUs, @JsonKey(name: 'ja-jp') final JaJp? jaJp, @JsonKey(name: 'zh-cn') final ZhCn? zhCn}) = _$I18nImpl; factory _I18n.fromJson(Map json) = _$I18nImpl.fromJson; @override @JsonKey(name: 'en-us') EnUs? get enUs; @override @JsonKey(name: 'ja-jp') JaJp? get jaJp; @override @JsonKey(name: 'zh-cn') ZhCn? get zhCn; /// Create a copy of I18n /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$I18nImplCopyWith<_$I18nImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/works/i18n.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'i18n.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$I18nImpl _$$I18nImplFromJson(Map json) => _$I18nImpl( enUs: json['en-us'] == null ? null : EnUs.fromJson(json['en-us'] as Map), jaJp: json['ja-jp'] == null ? null : JaJp.fromJson(json['ja-jp'] as Map), zhCn: json['zh-cn'] == null ? null : ZhCn.fromJson(json['zh-cn'] as Map), ); Map _$$I18nImplToJson(_$I18nImpl instance) => { 'en-us': instance.enUs, 'ja-jp': instance.jaJp, 'zh-cn': instance.zhCn, }; ================================================ FILE: lib/data/models/works/ja_jp.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'ja_jp.freezed.dart'; part 'ja_jp.g.dart'; @freezed class JaJp with _$JaJp { factory JaJp({ String? name, }) = _JaJp; factory JaJp.fromJson(Map json) => _$JaJpFromJson(json); } ================================================ FILE: lib/data/models/works/ja_jp.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'ja_jp.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); JaJp _$JaJpFromJson(Map json) { return _JaJp.fromJson(json); } /// @nodoc mixin _$JaJp { String? get name => throw _privateConstructorUsedError; /// Serializes this JaJp to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of JaJp /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $JaJpCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $JaJpCopyWith<$Res> { factory $JaJpCopyWith(JaJp value, $Res Function(JaJp) then) = _$JaJpCopyWithImpl<$Res, JaJp>; @useResult $Res call({String? name}); } /// @nodoc class _$JaJpCopyWithImpl<$Res, $Val extends JaJp> implements $JaJpCopyWith<$Res> { _$JaJpCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of JaJp /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? name = freezed, }) { return _then(_value.copyWith( name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, ) as $Val); } } /// @nodoc abstract class _$$JaJpImplCopyWith<$Res> implements $JaJpCopyWith<$Res> { factory _$$JaJpImplCopyWith( _$JaJpImpl value, $Res Function(_$JaJpImpl) then) = __$$JaJpImplCopyWithImpl<$Res>; @override @useResult $Res call({String? name}); } /// @nodoc class __$$JaJpImplCopyWithImpl<$Res> extends _$JaJpCopyWithImpl<$Res, _$JaJpImpl> implements _$$JaJpImplCopyWith<$Res> { __$$JaJpImplCopyWithImpl(_$JaJpImpl _value, $Res Function(_$JaJpImpl) _then) : super(_value, _then); /// Create a copy of JaJp /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? name = freezed, }) { return _then(_$JaJpImpl( name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, )); } } /// @nodoc @JsonSerializable() class _$JaJpImpl implements _JaJp { _$JaJpImpl({this.name}); factory _$JaJpImpl.fromJson(Map json) => _$$JaJpImplFromJson(json); @override final String? name; @override String toString() { return 'JaJp(name: $name)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$JaJpImpl && (identical(other.name, name) || other.name == name)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, name); /// Create a copy of JaJp /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$JaJpImplCopyWith<_$JaJpImpl> get copyWith => __$$JaJpImplCopyWithImpl<_$JaJpImpl>(this, _$identity); @override Map toJson() { return _$$JaJpImplToJson( this, ); } } abstract class _JaJp implements JaJp { factory _JaJp({final String? name}) = _$JaJpImpl; factory _JaJp.fromJson(Map json) = _$JaJpImpl.fromJson; @override String? get name; /// Create a copy of JaJp /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$JaJpImplCopyWith<_$JaJpImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/works/ja_jp.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'ja_jp.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$JaJpImpl _$$JaJpImplFromJson(Map json) => _$JaJpImpl( name: json['name'] as String?, ); Map _$$JaJpImplToJson(_$JaJpImpl instance) => { 'name': instance.name, }; ================================================ FILE: lib/data/models/works/language_edition.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'language_edition.freezed.dart'; part 'language_edition.g.dart'; @freezed class LanguageEdition with _$LanguageEdition { factory LanguageEdition({ String? lang, String? label, String? workno, @JsonKey(name: 'edition_id') int? editionId, @JsonKey(name: 'edition_type') String? editionType, @JsonKey(name: 'display_order') int? displayOrder, }) = _LanguageEdition; factory LanguageEdition.fromJson(Map json) => _$LanguageEditionFromJson(json); } ================================================ FILE: lib/data/models/works/language_edition.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'language_edition.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); LanguageEdition _$LanguageEditionFromJson(Map json) { return _LanguageEdition.fromJson(json); } /// @nodoc mixin _$LanguageEdition { String? get lang => throw _privateConstructorUsedError; String? get label => throw _privateConstructorUsedError; String? get workno => throw _privateConstructorUsedError; @JsonKey(name: 'edition_id') int? get editionId => throw _privateConstructorUsedError; @JsonKey(name: 'edition_type') String? get editionType => throw _privateConstructorUsedError; @JsonKey(name: 'display_order') int? get displayOrder => throw _privateConstructorUsedError; /// Serializes this LanguageEdition to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of LanguageEdition /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $LanguageEditionCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $LanguageEditionCopyWith<$Res> { factory $LanguageEditionCopyWith( LanguageEdition value, $Res Function(LanguageEdition) then) = _$LanguageEditionCopyWithImpl<$Res, LanguageEdition>; @useResult $Res call( {String? lang, String? label, String? workno, @JsonKey(name: 'edition_id') int? editionId, @JsonKey(name: 'edition_type') String? editionType, @JsonKey(name: 'display_order') int? displayOrder}); } /// @nodoc class _$LanguageEditionCopyWithImpl<$Res, $Val extends LanguageEdition> implements $LanguageEditionCopyWith<$Res> { _$LanguageEditionCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of LanguageEdition /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? lang = freezed, Object? label = freezed, Object? workno = freezed, Object? editionId = freezed, Object? editionType = freezed, Object? displayOrder = freezed, }) { return _then(_value.copyWith( lang: freezed == lang ? _value.lang : lang // ignore: cast_nullable_to_non_nullable as String?, label: freezed == label ? _value.label : label // ignore: cast_nullable_to_non_nullable as String?, workno: freezed == workno ? _value.workno : workno // ignore: cast_nullable_to_non_nullable as String?, editionId: freezed == editionId ? _value.editionId : editionId // ignore: cast_nullable_to_non_nullable as int?, editionType: freezed == editionType ? _value.editionType : editionType // ignore: cast_nullable_to_non_nullable as String?, displayOrder: freezed == displayOrder ? _value.displayOrder : displayOrder // ignore: cast_nullable_to_non_nullable as int?, ) as $Val); } } /// @nodoc abstract class _$$LanguageEditionImplCopyWith<$Res> implements $LanguageEditionCopyWith<$Res> { factory _$$LanguageEditionImplCopyWith(_$LanguageEditionImpl value, $Res Function(_$LanguageEditionImpl) then) = __$$LanguageEditionImplCopyWithImpl<$Res>; @override @useResult $Res call( {String? lang, String? label, String? workno, @JsonKey(name: 'edition_id') int? editionId, @JsonKey(name: 'edition_type') String? editionType, @JsonKey(name: 'display_order') int? displayOrder}); } /// @nodoc class __$$LanguageEditionImplCopyWithImpl<$Res> extends _$LanguageEditionCopyWithImpl<$Res, _$LanguageEditionImpl> implements _$$LanguageEditionImplCopyWith<$Res> { __$$LanguageEditionImplCopyWithImpl( _$LanguageEditionImpl _value, $Res Function(_$LanguageEditionImpl) _then) : super(_value, _then); /// Create a copy of LanguageEdition /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? lang = freezed, Object? label = freezed, Object? workno = freezed, Object? editionId = freezed, Object? editionType = freezed, Object? displayOrder = freezed, }) { return _then(_$LanguageEditionImpl( lang: freezed == lang ? _value.lang : lang // ignore: cast_nullable_to_non_nullable as String?, label: freezed == label ? _value.label : label // ignore: cast_nullable_to_non_nullable as String?, workno: freezed == workno ? _value.workno : workno // ignore: cast_nullable_to_non_nullable as String?, editionId: freezed == editionId ? _value.editionId : editionId // ignore: cast_nullable_to_non_nullable as int?, editionType: freezed == editionType ? _value.editionType : editionType // ignore: cast_nullable_to_non_nullable as String?, displayOrder: freezed == displayOrder ? _value.displayOrder : displayOrder // ignore: cast_nullable_to_non_nullable as int?, )); } } /// @nodoc @JsonSerializable() class _$LanguageEditionImpl implements _LanguageEdition { _$LanguageEditionImpl( {this.lang, this.label, this.workno, @JsonKey(name: 'edition_id') this.editionId, @JsonKey(name: 'edition_type') this.editionType, @JsonKey(name: 'display_order') this.displayOrder}); factory _$LanguageEditionImpl.fromJson(Map json) => _$$LanguageEditionImplFromJson(json); @override final String? lang; @override final String? label; @override final String? workno; @override @JsonKey(name: 'edition_id') final int? editionId; @override @JsonKey(name: 'edition_type') final String? editionType; @override @JsonKey(name: 'display_order') final int? displayOrder; @override String toString() { return 'LanguageEdition(lang: $lang, label: $label, workno: $workno, editionId: $editionId, editionType: $editionType, displayOrder: $displayOrder)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$LanguageEditionImpl && (identical(other.lang, lang) || other.lang == lang) && (identical(other.label, label) || other.label == label) && (identical(other.workno, workno) || other.workno == workno) && (identical(other.editionId, editionId) || other.editionId == editionId) && (identical(other.editionType, editionType) || other.editionType == editionType) && (identical(other.displayOrder, displayOrder) || other.displayOrder == displayOrder)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, lang, label, workno, editionId, editionType, displayOrder); /// Create a copy of LanguageEdition /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$LanguageEditionImplCopyWith<_$LanguageEditionImpl> get copyWith => __$$LanguageEditionImplCopyWithImpl<_$LanguageEditionImpl>( this, _$identity); @override Map toJson() { return _$$LanguageEditionImplToJson( this, ); } } abstract class _LanguageEdition implements LanguageEdition { factory _LanguageEdition( {final String? lang, final String? label, final String? workno, @JsonKey(name: 'edition_id') final int? editionId, @JsonKey(name: 'edition_type') final String? editionType, @JsonKey(name: 'display_order') final int? displayOrder}) = _$LanguageEditionImpl; factory _LanguageEdition.fromJson(Map json) = _$LanguageEditionImpl.fromJson; @override String? get lang; @override String? get label; @override String? get workno; @override @JsonKey(name: 'edition_id') int? get editionId; @override @JsonKey(name: 'edition_type') String? get editionType; @override @JsonKey(name: 'display_order') int? get displayOrder; /// Create a copy of LanguageEdition /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$LanguageEditionImplCopyWith<_$LanguageEditionImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/works/language_edition.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'language_edition.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$LanguageEditionImpl _$$LanguageEditionImplFromJson( Map json) => _$LanguageEditionImpl( lang: json['lang'] as String?, label: json['label'] as String?, workno: json['workno'] as String?, editionId: (json['edition_id'] as num?)?.toInt(), editionType: json['edition_type'] as String?, displayOrder: (json['display_order'] as num?)?.toInt(), ); Map _$$LanguageEditionImplToJson( _$LanguageEditionImpl instance) => { 'lang': instance.lang, 'label': instance.label, 'workno': instance.workno, 'edition_id': instance.editionId, 'edition_type': instance.editionType, 'display_order': instance.displayOrder, }; ================================================ FILE: lib/data/models/works/other_language_editions_in_db.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'other_language_editions_in_db.freezed.dart'; part 'other_language_editions_in_db.g.dart'; @freezed class OtherLanguageEditionsInDb with _$OtherLanguageEditionsInDb { factory OtherLanguageEditionsInDb({ int? id, String? lang, String? title, @JsonKey(name: 'source_id') String? sourceId, @JsonKey(name: 'is_original') bool? isOriginal, @JsonKey(name: 'source_type') String? sourceType, }) = _OtherLanguageEditionsInDb; factory OtherLanguageEditionsInDb.fromJson(Map json) => _$OtherLanguageEditionsInDbFromJson(json); } ================================================ FILE: lib/data/models/works/other_language_editions_in_db.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'other_language_editions_in_db.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); OtherLanguageEditionsInDb _$OtherLanguageEditionsInDbFromJson( Map json) { return _OtherLanguageEditionsInDb.fromJson(json); } /// @nodoc mixin _$OtherLanguageEditionsInDb { int? get id => throw _privateConstructorUsedError; String? get lang => throw _privateConstructorUsedError; String? get title => throw _privateConstructorUsedError; @JsonKey(name: 'source_id') String? get sourceId => throw _privateConstructorUsedError; @JsonKey(name: 'is_original') bool? get isOriginal => throw _privateConstructorUsedError; @JsonKey(name: 'source_type') String? get sourceType => throw _privateConstructorUsedError; /// Serializes this OtherLanguageEditionsInDb to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of OtherLanguageEditionsInDb /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $OtherLanguageEditionsInDbCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $OtherLanguageEditionsInDbCopyWith<$Res> { factory $OtherLanguageEditionsInDbCopyWith(OtherLanguageEditionsInDb value, $Res Function(OtherLanguageEditionsInDb) then) = _$OtherLanguageEditionsInDbCopyWithImpl<$Res, OtherLanguageEditionsInDb>; @useResult $Res call( {int? id, String? lang, String? title, @JsonKey(name: 'source_id') String? sourceId, @JsonKey(name: 'is_original') bool? isOriginal, @JsonKey(name: 'source_type') String? sourceType}); } /// @nodoc class _$OtherLanguageEditionsInDbCopyWithImpl<$Res, $Val extends OtherLanguageEditionsInDb> implements $OtherLanguageEditionsInDbCopyWith<$Res> { _$OtherLanguageEditionsInDbCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of OtherLanguageEditionsInDb /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? lang = freezed, Object? title = freezed, Object? sourceId = freezed, Object? isOriginal = freezed, Object? sourceType = freezed, }) { return _then(_value.copyWith( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as int?, lang: freezed == lang ? _value.lang : lang // ignore: cast_nullable_to_non_nullable as String?, title: freezed == title ? _value.title : title // ignore: cast_nullable_to_non_nullable as String?, sourceId: freezed == sourceId ? _value.sourceId : sourceId // ignore: cast_nullable_to_non_nullable as String?, isOriginal: freezed == isOriginal ? _value.isOriginal : isOriginal // ignore: cast_nullable_to_non_nullable as bool?, sourceType: freezed == sourceType ? _value.sourceType : sourceType // ignore: cast_nullable_to_non_nullable as String?, ) as $Val); } } /// @nodoc abstract class _$$OtherLanguageEditionsInDbImplCopyWith<$Res> implements $OtherLanguageEditionsInDbCopyWith<$Res> { factory _$$OtherLanguageEditionsInDbImplCopyWith( _$OtherLanguageEditionsInDbImpl value, $Res Function(_$OtherLanguageEditionsInDbImpl) then) = __$$OtherLanguageEditionsInDbImplCopyWithImpl<$Res>; @override @useResult $Res call( {int? id, String? lang, String? title, @JsonKey(name: 'source_id') String? sourceId, @JsonKey(name: 'is_original') bool? isOriginal, @JsonKey(name: 'source_type') String? sourceType}); } /// @nodoc class __$$OtherLanguageEditionsInDbImplCopyWithImpl<$Res> extends _$OtherLanguageEditionsInDbCopyWithImpl<$Res, _$OtherLanguageEditionsInDbImpl> implements _$$OtherLanguageEditionsInDbImplCopyWith<$Res> { __$$OtherLanguageEditionsInDbImplCopyWithImpl( _$OtherLanguageEditionsInDbImpl _value, $Res Function(_$OtherLanguageEditionsInDbImpl) _then) : super(_value, _then); /// Create a copy of OtherLanguageEditionsInDb /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? lang = freezed, Object? title = freezed, Object? sourceId = freezed, Object? isOriginal = freezed, Object? sourceType = freezed, }) { return _then(_$OtherLanguageEditionsInDbImpl( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as int?, lang: freezed == lang ? _value.lang : lang // ignore: cast_nullable_to_non_nullable as String?, title: freezed == title ? _value.title : title // ignore: cast_nullable_to_non_nullable as String?, sourceId: freezed == sourceId ? _value.sourceId : sourceId // ignore: cast_nullable_to_non_nullable as String?, isOriginal: freezed == isOriginal ? _value.isOriginal : isOriginal // ignore: cast_nullable_to_non_nullable as bool?, sourceType: freezed == sourceType ? _value.sourceType : sourceType // ignore: cast_nullable_to_non_nullable as String?, )); } } /// @nodoc @JsonSerializable() class _$OtherLanguageEditionsInDbImpl implements _OtherLanguageEditionsInDb { _$OtherLanguageEditionsInDbImpl( {this.id, this.lang, this.title, @JsonKey(name: 'source_id') this.sourceId, @JsonKey(name: 'is_original') this.isOriginal, @JsonKey(name: 'source_type') this.sourceType}); factory _$OtherLanguageEditionsInDbImpl.fromJson(Map json) => _$$OtherLanguageEditionsInDbImplFromJson(json); @override final int? id; @override final String? lang; @override final String? title; @override @JsonKey(name: 'source_id') final String? sourceId; @override @JsonKey(name: 'is_original') final bool? isOriginal; @override @JsonKey(name: 'source_type') final String? sourceType; @override String toString() { return 'OtherLanguageEditionsInDb(id: $id, lang: $lang, title: $title, sourceId: $sourceId, isOriginal: $isOriginal, sourceType: $sourceType)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$OtherLanguageEditionsInDbImpl && (identical(other.id, id) || other.id == id) && (identical(other.lang, lang) || other.lang == lang) && (identical(other.title, title) || other.title == title) && (identical(other.sourceId, sourceId) || other.sourceId == sourceId) && (identical(other.isOriginal, isOriginal) || other.isOriginal == isOriginal) && (identical(other.sourceType, sourceType) || other.sourceType == sourceType)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, id, lang, title, sourceId, isOriginal, sourceType); /// Create a copy of OtherLanguageEditionsInDb /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$OtherLanguageEditionsInDbImplCopyWith<_$OtherLanguageEditionsInDbImpl> get copyWith => __$$OtherLanguageEditionsInDbImplCopyWithImpl< _$OtherLanguageEditionsInDbImpl>(this, _$identity); @override Map toJson() { return _$$OtherLanguageEditionsInDbImplToJson( this, ); } } abstract class _OtherLanguageEditionsInDb implements OtherLanguageEditionsInDb { factory _OtherLanguageEditionsInDb( {final int? id, final String? lang, final String? title, @JsonKey(name: 'source_id') final String? sourceId, @JsonKey(name: 'is_original') final bool? isOriginal, @JsonKey(name: 'source_type') final String? sourceType}) = _$OtherLanguageEditionsInDbImpl; factory _OtherLanguageEditionsInDb.fromJson(Map json) = _$OtherLanguageEditionsInDbImpl.fromJson; @override int? get id; @override String? get lang; @override String? get title; @override @JsonKey(name: 'source_id') String? get sourceId; @override @JsonKey(name: 'is_original') bool? get isOriginal; @override @JsonKey(name: 'source_type') String? get sourceType; /// Create a copy of OtherLanguageEditionsInDb /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$OtherLanguageEditionsInDbImplCopyWith<_$OtherLanguageEditionsInDbImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/works/other_language_editions_in_db.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'other_language_editions_in_db.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$OtherLanguageEditionsInDbImpl _$$OtherLanguageEditionsInDbImplFromJson( Map json) => _$OtherLanguageEditionsInDbImpl( id: (json['id'] as num?)?.toInt(), lang: json['lang'] as String?, title: json['title'] as String?, sourceId: json['source_id'] as String?, isOriginal: json['is_original'] as bool?, sourceType: json['source_type'] as String?, ); Map _$$OtherLanguageEditionsInDbImplToJson( _$OtherLanguageEditionsInDbImpl instance) => { 'id': instance.id, 'lang': instance.lang, 'title': instance.title, 'source_id': instance.sourceId, 'is_original': instance.isOriginal, 'source_type': instance.sourceType, }; ================================================ FILE: lib/data/models/works/pagination.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'pagination.freezed.dart'; part 'pagination.g.dart'; @freezed class Pagination with _$Pagination { factory Pagination({ int? currentPage, int? pageSize, int? totalCount, }) = _Pagination; factory Pagination.fromJson(Map json) => _$PaginationFromJson(json); } ================================================ FILE: lib/data/models/works/pagination.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'pagination.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Pagination _$PaginationFromJson(Map json) { return _Pagination.fromJson(json); } /// @nodoc mixin _$Pagination { int? get currentPage => throw _privateConstructorUsedError; int? get pageSize => throw _privateConstructorUsedError; int? get totalCount => throw _privateConstructorUsedError; /// Serializes this Pagination to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $PaginationCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $PaginationCopyWith<$Res> { factory $PaginationCopyWith( Pagination value, $Res Function(Pagination) then) = _$PaginationCopyWithImpl<$Res, Pagination>; @useResult $Res call({int? currentPage, int? pageSize, int? totalCount}); } /// @nodoc class _$PaginationCopyWithImpl<$Res, $Val extends Pagination> implements $PaginationCopyWith<$Res> { _$PaginationCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? currentPage = freezed, Object? pageSize = freezed, Object? totalCount = freezed, }) { return _then(_value.copyWith( currentPage: freezed == currentPage ? _value.currentPage : currentPage // ignore: cast_nullable_to_non_nullable as int?, pageSize: freezed == pageSize ? _value.pageSize : pageSize // ignore: cast_nullable_to_non_nullable as int?, totalCount: freezed == totalCount ? _value.totalCount : totalCount // ignore: cast_nullable_to_non_nullable as int?, ) as $Val); } } /// @nodoc abstract class _$$PaginationImplCopyWith<$Res> implements $PaginationCopyWith<$Res> { factory _$$PaginationImplCopyWith( _$PaginationImpl value, $Res Function(_$PaginationImpl) then) = __$$PaginationImplCopyWithImpl<$Res>; @override @useResult $Res call({int? currentPage, int? pageSize, int? totalCount}); } /// @nodoc class __$$PaginationImplCopyWithImpl<$Res> extends _$PaginationCopyWithImpl<$Res, _$PaginationImpl> implements _$$PaginationImplCopyWith<$Res> { __$$PaginationImplCopyWithImpl( _$PaginationImpl _value, $Res Function(_$PaginationImpl) _then) : super(_value, _then); /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? currentPage = freezed, Object? pageSize = freezed, Object? totalCount = freezed, }) { return _then(_$PaginationImpl( currentPage: freezed == currentPage ? _value.currentPage : currentPage // ignore: cast_nullable_to_non_nullable as int?, pageSize: freezed == pageSize ? _value.pageSize : pageSize // ignore: cast_nullable_to_non_nullable as int?, totalCount: freezed == totalCount ? _value.totalCount : totalCount // ignore: cast_nullable_to_non_nullable as int?, )); } } /// @nodoc @JsonSerializable() class _$PaginationImpl implements _Pagination { _$PaginationImpl({this.currentPage, this.pageSize, this.totalCount}); factory _$PaginationImpl.fromJson(Map json) => _$$PaginationImplFromJson(json); @override final int? currentPage; @override final int? pageSize; @override final int? totalCount; @override String toString() { return 'Pagination(currentPage: $currentPage, pageSize: $pageSize, totalCount: $totalCount)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PaginationImpl && (identical(other.currentPage, currentPage) || other.currentPage == currentPage) && (identical(other.pageSize, pageSize) || other.pageSize == pageSize) && (identical(other.totalCount, totalCount) || other.totalCount == totalCount)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, currentPage, pageSize, totalCount); /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PaginationImplCopyWith<_$PaginationImpl> get copyWith => __$$PaginationImplCopyWithImpl<_$PaginationImpl>(this, _$identity); @override Map toJson() { return _$$PaginationImplToJson( this, ); } } abstract class _Pagination implements Pagination { factory _Pagination( {final int? currentPage, final int? pageSize, final int? totalCount}) = _$PaginationImpl; factory _Pagination.fromJson(Map json) = _$PaginationImpl.fromJson; @override int? get currentPage; @override int? get pageSize; @override int? get totalCount; /// Create a copy of Pagination /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$PaginationImplCopyWith<_$PaginationImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/works/pagination.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'pagination.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$PaginationImpl _$$PaginationImplFromJson(Map json) => _$PaginationImpl( currentPage: (json['currentPage'] as num?)?.toInt(), pageSize: (json['pageSize'] as num?)?.toInt(), totalCount: (json['totalCount'] as num?)?.toInt(), ); Map _$$PaginationImplToJson(_$PaginationImpl instance) => { 'currentPage': instance.currentPage, 'pageSize': instance.pageSize, 'totalCount': instance.totalCount, }; ================================================ FILE: lib/data/models/works/tag.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'i18n.dart'; part 'tag.freezed.dart'; part 'tag.g.dart'; @freezed class Tag with _$Tag { factory Tag({ int? id, I18n? i18n, String? name, }) = _Tag; factory Tag.fromJson(Map json) => _$TagFromJson(json); } ================================================ FILE: lib/data/models/works/tag.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'tag.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Tag _$TagFromJson(Map json) { return _Tag.fromJson(json); } /// @nodoc mixin _$Tag { int? get id => throw _privateConstructorUsedError; I18n? get i18n => throw _privateConstructorUsedError; String? get name => throw _privateConstructorUsedError; /// Serializes this Tag to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of Tag /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $TagCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $TagCopyWith<$Res> { factory $TagCopyWith(Tag value, $Res Function(Tag) then) = _$TagCopyWithImpl<$Res, Tag>; @useResult $Res call({int? id, I18n? i18n, String? name}); $I18nCopyWith<$Res>? get i18n; } /// @nodoc class _$TagCopyWithImpl<$Res, $Val extends Tag> implements $TagCopyWith<$Res> { _$TagCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of Tag /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? i18n = freezed, Object? name = freezed, }) { return _then(_value.copyWith( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as int?, i18n: freezed == i18n ? _value.i18n : i18n // ignore: cast_nullable_to_non_nullable as I18n?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, ) as $Val); } /// Create a copy of Tag /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $I18nCopyWith<$Res>? get i18n { if (_value.i18n == null) { return null; } return $I18nCopyWith<$Res>(_value.i18n!, (value) { return _then(_value.copyWith(i18n: value) as $Val); }); } } /// @nodoc abstract class _$$TagImplCopyWith<$Res> implements $TagCopyWith<$Res> { factory _$$TagImplCopyWith(_$TagImpl value, $Res Function(_$TagImpl) then) = __$$TagImplCopyWithImpl<$Res>; @override @useResult $Res call({int? id, I18n? i18n, String? name}); @override $I18nCopyWith<$Res>? get i18n; } /// @nodoc class __$$TagImplCopyWithImpl<$Res> extends _$TagCopyWithImpl<$Res, _$TagImpl> implements _$$TagImplCopyWith<$Res> { __$$TagImplCopyWithImpl(_$TagImpl _value, $Res Function(_$TagImpl) _then) : super(_value, _then); /// Create a copy of Tag /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? i18n = freezed, Object? name = freezed, }) { return _then(_$TagImpl( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as int?, i18n: freezed == i18n ? _value.i18n : i18n // ignore: cast_nullable_to_non_nullable as I18n?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, )); } } /// @nodoc @JsonSerializable() class _$TagImpl implements _Tag { _$TagImpl({this.id, this.i18n, this.name}); factory _$TagImpl.fromJson(Map json) => _$$TagImplFromJson(json); @override final int? id; @override final I18n? i18n; @override final String? name; @override String toString() { return 'Tag(id: $id, i18n: $i18n, name: $name)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$TagImpl && (identical(other.id, id) || other.id == id) && (identical(other.i18n, i18n) || other.i18n == i18n) && (identical(other.name, name) || other.name == name)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, id, i18n, name); /// Create a copy of Tag /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$TagImplCopyWith<_$TagImpl> get copyWith => __$$TagImplCopyWithImpl<_$TagImpl>(this, _$identity); @override Map toJson() { return _$$TagImplToJson( this, ); } } abstract class _Tag implements Tag { factory _Tag({final int? id, final I18n? i18n, final String? name}) = _$TagImpl; factory _Tag.fromJson(Map json) = _$TagImpl.fromJson; @override int? get id; @override I18n? get i18n; @override String? get name; /// Create a copy of Tag /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$TagImplCopyWith<_$TagImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/works/tag.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'tag.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$TagImpl _$$TagImplFromJson(Map json) => _$TagImpl( id: (json['id'] as num?)?.toInt(), i18n: json['i18n'] == null ? null : I18n.fromJson(json['i18n'] as Map), name: json['name'] as String?, ); Map _$$TagImplToJson(_$TagImpl instance) => { 'id': instance.id, 'i18n': instance.i18n, 'name': instance.name, }; ================================================ FILE: lib/data/models/works/translation_bonus_lang.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'translation_bonus_lang.freezed.dart'; part 'translation_bonus_lang.g.dart'; @freezed class TranslationBonusLang with _$TranslationBonusLang { factory TranslationBonusLang({ int? price, String? status, @JsonKey(name: 'price_tax') int? priceTax, @JsonKey(name: 'child_count') int? childCount, @JsonKey(name: 'price_in_tax') int? priceInTax, @JsonKey(name: 'recipient_max') int? recipientMax, @JsonKey(name: 'recipient_available_count') int? recipientAvailableCount, }) = _TranslationBonusLang; factory TranslationBonusLang.fromJson(Map json) => _$TranslationBonusLangFromJson(json); } ================================================ FILE: lib/data/models/works/translation_bonus_lang.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'translation_bonus_lang.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); TranslationBonusLang _$TranslationBonusLangFromJson(Map json) { return _TranslationBonusLang.fromJson(json); } /// @nodoc mixin _$TranslationBonusLang { int? get price => throw _privateConstructorUsedError; String? get status => throw _privateConstructorUsedError; @JsonKey(name: 'price_tax') int? get priceTax => throw _privateConstructorUsedError; @JsonKey(name: 'child_count') int? get childCount => throw _privateConstructorUsedError; @JsonKey(name: 'price_in_tax') int? get priceInTax => throw _privateConstructorUsedError; @JsonKey(name: 'recipient_max') int? get recipientMax => throw _privateConstructorUsedError; @JsonKey(name: 'recipient_available_count') int? get recipientAvailableCount => throw _privateConstructorUsedError; /// Serializes this TranslationBonusLang to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of TranslationBonusLang /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $TranslationBonusLangCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $TranslationBonusLangCopyWith<$Res> { factory $TranslationBonusLangCopyWith(TranslationBonusLang value, $Res Function(TranslationBonusLang) then) = _$TranslationBonusLangCopyWithImpl<$Res, TranslationBonusLang>; @useResult $Res call( {int? price, String? status, @JsonKey(name: 'price_tax') int? priceTax, @JsonKey(name: 'child_count') int? childCount, @JsonKey(name: 'price_in_tax') int? priceInTax, @JsonKey(name: 'recipient_max') int? recipientMax, @JsonKey(name: 'recipient_available_count') int? recipientAvailableCount}); } /// @nodoc class _$TranslationBonusLangCopyWithImpl<$Res, $Val extends TranslationBonusLang> implements $TranslationBonusLangCopyWith<$Res> { _$TranslationBonusLangCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of TranslationBonusLang /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? price = freezed, Object? status = freezed, Object? priceTax = freezed, Object? childCount = freezed, Object? priceInTax = freezed, Object? recipientMax = freezed, Object? recipientAvailableCount = freezed, }) { return _then(_value.copyWith( price: freezed == price ? _value.price : price // ignore: cast_nullable_to_non_nullable as int?, status: freezed == status ? _value.status : status // ignore: cast_nullable_to_non_nullable as String?, priceTax: freezed == priceTax ? _value.priceTax : priceTax // ignore: cast_nullable_to_non_nullable as int?, childCount: freezed == childCount ? _value.childCount : childCount // ignore: cast_nullable_to_non_nullable as int?, priceInTax: freezed == priceInTax ? _value.priceInTax : priceInTax // ignore: cast_nullable_to_non_nullable as int?, recipientMax: freezed == recipientMax ? _value.recipientMax : recipientMax // ignore: cast_nullable_to_non_nullable as int?, recipientAvailableCount: freezed == recipientAvailableCount ? _value.recipientAvailableCount : recipientAvailableCount // ignore: cast_nullable_to_non_nullable as int?, ) as $Val); } } /// @nodoc abstract class _$$TranslationBonusLangImplCopyWith<$Res> implements $TranslationBonusLangCopyWith<$Res> { factory _$$TranslationBonusLangImplCopyWith(_$TranslationBonusLangImpl value, $Res Function(_$TranslationBonusLangImpl) then) = __$$TranslationBonusLangImplCopyWithImpl<$Res>; @override @useResult $Res call( {int? price, String? status, @JsonKey(name: 'price_tax') int? priceTax, @JsonKey(name: 'child_count') int? childCount, @JsonKey(name: 'price_in_tax') int? priceInTax, @JsonKey(name: 'recipient_max') int? recipientMax, @JsonKey(name: 'recipient_available_count') int? recipientAvailableCount}); } /// @nodoc class __$$TranslationBonusLangImplCopyWithImpl<$Res> extends _$TranslationBonusLangCopyWithImpl<$Res, _$TranslationBonusLangImpl> implements _$$TranslationBonusLangImplCopyWith<$Res> { __$$TranslationBonusLangImplCopyWithImpl(_$TranslationBonusLangImpl _value, $Res Function(_$TranslationBonusLangImpl) _then) : super(_value, _then); /// Create a copy of TranslationBonusLang /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? price = freezed, Object? status = freezed, Object? priceTax = freezed, Object? childCount = freezed, Object? priceInTax = freezed, Object? recipientMax = freezed, Object? recipientAvailableCount = freezed, }) { return _then(_$TranslationBonusLangImpl( price: freezed == price ? _value.price : price // ignore: cast_nullable_to_non_nullable as int?, status: freezed == status ? _value.status : status // ignore: cast_nullable_to_non_nullable as String?, priceTax: freezed == priceTax ? _value.priceTax : priceTax // ignore: cast_nullable_to_non_nullable as int?, childCount: freezed == childCount ? _value.childCount : childCount // ignore: cast_nullable_to_non_nullable as int?, priceInTax: freezed == priceInTax ? _value.priceInTax : priceInTax // ignore: cast_nullable_to_non_nullable as int?, recipientMax: freezed == recipientMax ? _value.recipientMax : recipientMax // ignore: cast_nullable_to_non_nullable as int?, recipientAvailableCount: freezed == recipientAvailableCount ? _value.recipientAvailableCount : recipientAvailableCount // ignore: cast_nullable_to_non_nullable as int?, )); } } /// @nodoc @JsonSerializable() class _$TranslationBonusLangImpl implements _TranslationBonusLang { _$TranslationBonusLangImpl( {this.price, this.status, @JsonKey(name: 'price_tax') this.priceTax, @JsonKey(name: 'child_count') this.childCount, @JsonKey(name: 'price_in_tax') this.priceInTax, @JsonKey(name: 'recipient_max') this.recipientMax, @JsonKey(name: 'recipient_available_count') this.recipientAvailableCount}); factory _$TranslationBonusLangImpl.fromJson(Map json) => _$$TranslationBonusLangImplFromJson(json); @override final int? price; @override final String? status; @override @JsonKey(name: 'price_tax') final int? priceTax; @override @JsonKey(name: 'child_count') final int? childCount; @override @JsonKey(name: 'price_in_tax') final int? priceInTax; @override @JsonKey(name: 'recipient_max') final int? recipientMax; @override @JsonKey(name: 'recipient_available_count') final int? recipientAvailableCount; @override String toString() { return 'TranslationBonusLang(price: $price, status: $status, priceTax: $priceTax, childCount: $childCount, priceInTax: $priceInTax, recipientMax: $recipientMax, recipientAvailableCount: $recipientAvailableCount)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$TranslationBonusLangImpl && (identical(other.price, price) || other.price == price) && (identical(other.status, status) || other.status == status) && (identical(other.priceTax, priceTax) || other.priceTax == priceTax) && (identical(other.childCount, childCount) || other.childCount == childCount) && (identical(other.priceInTax, priceInTax) || other.priceInTax == priceInTax) && (identical(other.recipientMax, recipientMax) || other.recipientMax == recipientMax) && (identical( other.recipientAvailableCount, recipientAvailableCount) || other.recipientAvailableCount == recipientAvailableCount)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, price, status, priceTax, childCount, priceInTax, recipientMax, recipientAvailableCount); /// Create a copy of TranslationBonusLang /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$TranslationBonusLangImplCopyWith<_$TranslationBonusLangImpl> get copyWith => __$$TranslationBonusLangImplCopyWithImpl<_$TranslationBonusLangImpl>( this, _$identity); @override Map toJson() { return _$$TranslationBonusLangImplToJson( this, ); } } abstract class _TranslationBonusLang implements TranslationBonusLang { factory _TranslationBonusLang( {final int? price, final String? status, @JsonKey(name: 'price_tax') final int? priceTax, @JsonKey(name: 'child_count') final int? childCount, @JsonKey(name: 'price_in_tax') final int? priceInTax, @JsonKey(name: 'recipient_max') final int? recipientMax, @JsonKey(name: 'recipient_available_count') final int? recipientAvailableCount}) = _$TranslationBonusLangImpl; factory _TranslationBonusLang.fromJson(Map json) = _$TranslationBonusLangImpl.fromJson; @override int? get price; @override String? get status; @override @JsonKey(name: 'price_tax') int? get priceTax; @override @JsonKey(name: 'child_count') int? get childCount; @override @JsonKey(name: 'price_in_tax') int? get priceInTax; @override @JsonKey(name: 'recipient_max') int? get recipientMax; @override @JsonKey(name: 'recipient_available_count') int? get recipientAvailableCount; /// Create a copy of TranslationBonusLang /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$TranslationBonusLangImplCopyWith<_$TranslationBonusLangImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/works/translation_bonus_lang.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'translation_bonus_lang.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$TranslationBonusLangImpl _$$TranslationBonusLangImplFromJson( Map json) => _$TranslationBonusLangImpl( price: (json['price'] as num?)?.toInt(), status: json['status'] as String?, priceTax: (json['price_tax'] as num?)?.toInt(), childCount: (json['child_count'] as num?)?.toInt(), priceInTax: (json['price_in_tax'] as num?)?.toInt(), recipientMax: (json['recipient_max'] as num?)?.toInt(), recipientAvailableCount: (json['recipient_available_count'] as num?)?.toInt(), ); Map _$$TranslationBonusLangImplToJson( _$TranslationBonusLangImpl instance) => { 'price': instance.price, 'status': instance.status, 'price_tax': instance.priceTax, 'child_count': instance.childCount, 'price_in_tax': instance.priceInTax, 'recipient_max': instance.recipientMax, 'recipient_available_count': instance.recipientAvailableCount, }; ================================================ FILE: lib/data/models/works/translation_info.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'translation_bonus_lang.dart'; part 'translation_info.freezed.dart'; part 'translation_info.g.dart'; @freezed class TranslationInfo with _$TranslationInfo { factory TranslationInfo({ String? lang, @JsonKey(name: 'is_child') bool? isChild, @JsonKey(name: 'is_parent') bool? isParent, @JsonKey(name: 'is_original') bool? isOriginal, @JsonKey(name: 'is_volunteer') bool? isVolunteer, @JsonKey(name: 'child_worknos') List? childWorknos, @JsonKey(name: 'parent_workno') String? parentWorkno, @JsonKey(name: 'original_workno') String? originalWorkno, @JsonKey(name: 'is_translation_agree') bool? isTranslationAgree, @JsonKey( name: 'translation_bonus_langs', fromJson: _translationBonusLangsFromJson, toJson: _translationBonusLangsToJson, ) Map? translationBonusLangs, @JsonKey(name: 'is_translation_bonus_child') bool? isTranslationBonusChild, @JsonKey(name: 'production_trade_price_rate') int? productionTradePriceRate, }) = _TranslationInfo; factory TranslationInfo.fromJson(Map json) => _$TranslationInfoFromJson(json); } Map? _translationBonusLangsFromJson( dynamic json) { if (json == null) return null; if (json is List && json.isEmpty) return {}; if (json is Map) { return json.map((key, value) => MapEntry( key, TranslationBonusLang.fromJson(value as Map), )); } return {}; } dynamic _translationBonusLangsToJson(Map? map) { if (map == null) return null; if (map.isEmpty) return []; return map.map((key, value) => MapEntry(key, value.toJson())); } ================================================ FILE: lib/data/models/works/translation_info.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'translation_info.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); TranslationInfo _$TranslationInfoFromJson(Map json) { return _TranslationInfo.fromJson(json); } /// @nodoc mixin _$TranslationInfo { String? get lang => throw _privateConstructorUsedError; @JsonKey(name: 'is_child') bool? get isChild => throw _privateConstructorUsedError; @JsonKey(name: 'is_parent') bool? get isParent => throw _privateConstructorUsedError; @JsonKey(name: 'is_original') bool? get isOriginal => throw _privateConstructorUsedError; @JsonKey(name: 'is_volunteer') bool? get isVolunteer => throw _privateConstructorUsedError; @JsonKey(name: 'child_worknos') List? get childWorknos => throw _privateConstructorUsedError; @JsonKey(name: 'parent_workno') String? get parentWorkno => throw _privateConstructorUsedError; @JsonKey(name: 'original_workno') String? get originalWorkno => throw _privateConstructorUsedError; @JsonKey(name: 'is_translation_agree') bool? get isTranslationAgree => throw _privateConstructorUsedError; @JsonKey( name: 'translation_bonus_langs', fromJson: _translationBonusLangsFromJson, toJson: _translationBonusLangsToJson) Map? get translationBonusLangs => throw _privateConstructorUsedError; @JsonKey(name: 'is_translation_bonus_child') bool? get isTranslationBonusChild => throw _privateConstructorUsedError; @JsonKey(name: 'production_trade_price_rate') int? get productionTradePriceRate => throw _privateConstructorUsedError; /// Serializes this TranslationInfo to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of TranslationInfo /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $TranslationInfoCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $TranslationInfoCopyWith<$Res> { factory $TranslationInfoCopyWith( TranslationInfo value, $Res Function(TranslationInfo) then) = _$TranslationInfoCopyWithImpl<$Res, TranslationInfo>; @useResult $Res call( {String? lang, @JsonKey(name: 'is_child') bool? isChild, @JsonKey(name: 'is_parent') bool? isParent, @JsonKey(name: 'is_original') bool? isOriginal, @JsonKey(name: 'is_volunteer') bool? isVolunteer, @JsonKey(name: 'child_worknos') List? childWorknos, @JsonKey(name: 'parent_workno') String? parentWorkno, @JsonKey(name: 'original_workno') String? originalWorkno, @JsonKey(name: 'is_translation_agree') bool? isTranslationAgree, @JsonKey( name: 'translation_bonus_langs', fromJson: _translationBonusLangsFromJson, toJson: _translationBonusLangsToJson) Map? translationBonusLangs, @JsonKey(name: 'is_translation_bonus_child') bool? isTranslationBonusChild, @JsonKey(name: 'production_trade_price_rate') int? productionTradePriceRate}); } /// @nodoc class _$TranslationInfoCopyWithImpl<$Res, $Val extends TranslationInfo> implements $TranslationInfoCopyWith<$Res> { _$TranslationInfoCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of TranslationInfo /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? lang = freezed, Object? isChild = freezed, Object? isParent = freezed, Object? isOriginal = freezed, Object? isVolunteer = freezed, Object? childWorknos = freezed, Object? parentWorkno = freezed, Object? originalWorkno = freezed, Object? isTranslationAgree = freezed, Object? translationBonusLangs = freezed, Object? isTranslationBonusChild = freezed, Object? productionTradePriceRate = freezed, }) { return _then(_value.copyWith( lang: freezed == lang ? _value.lang : lang // ignore: cast_nullable_to_non_nullable as String?, isChild: freezed == isChild ? _value.isChild : isChild // ignore: cast_nullable_to_non_nullable as bool?, isParent: freezed == isParent ? _value.isParent : isParent // ignore: cast_nullable_to_non_nullable as bool?, isOriginal: freezed == isOriginal ? _value.isOriginal : isOriginal // ignore: cast_nullable_to_non_nullable as bool?, isVolunteer: freezed == isVolunteer ? _value.isVolunteer : isVolunteer // ignore: cast_nullable_to_non_nullable as bool?, childWorknos: freezed == childWorknos ? _value.childWorknos : childWorknos // ignore: cast_nullable_to_non_nullable as List?, parentWorkno: freezed == parentWorkno ? _value.parentWorkno : parentWorkno // ignore: cast_nullable_to_non_nullable as String?, originalWorkno: freezed == originalWorkno ? _value.originalWorkno : originalWorkno // ignore: cast_nullable_to_non_nullable as String?, isTranslationAgree: freezed == isTranslationAgree ? _value.isTranslationAgree : isTranslationAgree // ignore: cast_nullable_to_non_nullable as bool?, translationBonusLangs: freezed == translationBonusLangs ? _value.translationBonusLangs : translationBonusLangs // ignore: cast_nullable_to_non_nullable as Map?, isTranslationBonusChild: freezed == isTranslationBonusChild ? _value.isTranslationBonusChild : isTranslationBonusChild // ignore: cast_nullable_to_non_nullable as bool?, productionTradePriceRate: freezed == productionTradePriceRate ? _value.productionTradePriceRate : productionTradePriceRate // ignore: cast_nullable_to_non_nullable as int?, ) as $Val); } } /// @nodoc abstract class _$$TranslationInfoImplCopyWith<$Res> implements $TranslationInfoCopyWith<$Res> { factory _$$TranslationInfoImplCopyWith(_$TranslationInfoImpl value, $Res Function(_$TranslationInfoImpl) then) = __$$TranslationInfoImplCopyWithImpl<$Res>; @override @useResult $Res call( {String? lang, @JsonKey(name: 'is_child') bool? isChild, @JsonKey(name: 'is_parent') bool? isParent, @JsonKey(name: 'is_original') bool? isOriginal, @JsonKey(name: 'is_volunteer') bool? isVolunteer, @JsonKey(name: 'child_worknos') List? childWorknos, @JsonKey(name: 'parent_workno') String? parentWorkno, @JsonKey(name: 'original_workno') String? originalWorkno, @JsonKey(name: 'is_translation_agree') bool? isTranslationAgree, @JsonKey( name: 'translation_bonus_langs', fromJson: _translationBonusLangsFromJson, toJson: _translationBonusLangsToJson) Map? translationBonusLangs, @JsonKey(name: 'is_translation_bonus_child') bool? isTranslationBonusChild, @JsonKey(name: 'production_trade_price_rate') int? productionTradePriceRate}); } /// @nodoc class __$$TranslationInfoImplCopyWithImpl<$Res> extends _$TranslationInfoCopyWithImpl<$Res, _$TranslationInfoImpl> implements _$$TranslationInfoImplCopyWith<$Res> { __$$TranslationInfoImplCopyWithImpl( _$TranslationInfoImpl _value, $Res Function(_$TranslationInfoImpl) _then) : super(_value, _then); /// Create a copy of TranslationInfo /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? lang = freezed, Object? isChild = freezed, Object? isParent = freezed, Object? isOriginal = freezed, Object? isVolunteer = freezed, Object? childWorknos = freezed, Object? parentWorkno = freezed, Object? originalWorkno = freezed, Object? isTranslationAgree = freezed, Object? translationBonusLangs = freezed, Object? isTranslationBonusChild = freezed, Object? productionTradePriceRate = freezed, }) { return _then(_$TranslationInfoImpl( lang: freezed == lang ? _value.lang : lang // ignore: cast_nullable_to_non_nullable as String?, isChild: freezed == isChild ? _value.isChild : isChild // ignore: cast_nullable_to_non_nullable as bool?, isParent: freezed == isParent ? _value.isParent : isParent // ignore: cast_nullable_to_non_nullable as bool?, isOriginal: freezed == isOriginal ? _value.isOriginal : isOriginal // ignore: cast_nullable_to_non_nullable as bool?, isVolunteer: freezed == isVolunteer ? _value.isVolunteer : isVolunteer // ignore: cast_nullable_to_non_nullable as bool?, childWorknos: freezed == childWorknos ? _value._childWorknos : childWorknos // ignore: cast_nullable_to_non_nullable as List?, parentWorkno: freezed == parentWorkno ? _value.parentWorkno : parentWorkno // ignore: cast_nullable_to_non_nullable as String?, originalWorkno: freezed == originalWorkno ? _value.originalWorkno : originalWorkno // ignore: cast_nullable_to_non_nullable as String?, isTranslationAgree: freezed == isTranslationAgree ? _value.isTranslationAgree : isTranslationAgree // ignore: cast_nullable_to_non_nullable as bool?, translationBonusLangs: freezed == translationBonusLangs ? _value._translationBonusLangs : translationBonusLangs // ignore: cast_nullable_to_non_nullable as Map?, isTranslationBonusChild: freezed == isTranslationBonusChild ? _value.isTranslationBonusChild : isTranslationBonusChild // ignore: cast_nullable_to_non_nullable as bool?, productionTradePriceRate: freezed == productionTradePriceRate ? _value.productionTradePriceRate : productionTradePriceRate // ignore: cast_nullable_to_non_nullable as int?, )); } } /// @nodoc @JsonSerializable() class _$TranslationInfoImpl implements _TranslationInfo { _$TranslationInfoImpl( {this.lang, @JsonKey(name: 'is_child') this.isChild, @JsonKey(name: 'is_parent') this.isParent, @JsonKey(name: 'is_original') this.isOriginal, @JsonKey(name: 'is_volunteer') this.isVolunteer, @JsonKey(name: 'child_worknos') final List? childWorknos, @JsonKey(name: 'parent_workno') this.parentWorkno, @JsonKey(name: 'original_workno') this.originalWorkno, @JsonKey(name: 'is_translation_agree') this.isTranslationAgree, @JsonKey( name: 'translation_bonus_langs', fromJson: _translationBonusLangsFromJson, toJson: _translationBonusLangsToJson) final Map? translationBonusLangs, @JsonKey(name: 'is_translation_bonus_child') this.isTranslationBonusChild, @JsonKey(name: 'production_trade_price_rate') this.productionTradePriceRate}) : _childWorknos = childWorknos, _translationBonusLangs = translationBonusLangs; factory _$TranslationInfoImpl.fromJson(Map json) => _$$TranslationInfoImplFromJson(json); @override final String? lang; @override @JsonKey(name: 'is_child') final bool? isChild; @override @JsonKey(name: 'is_parent') final bool? isParent; @override @JsonKey(name: 'is_original') final bool? isOriginal; @override @JsonKey(name: 'is_volunteer') final bool? isVolunteer; final List? _childWorknos; @override @JsonKey(name: 'child_worknos') List? get childWorknos { final value = _childWorknos; if (value == null) return null; if (_childWorknos is EqualUnmodifiableListView) return _childWorknos; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(value); } @override @JsonKey(name: 'parent_workno') final String? parentWorkno; @override @JsonKey(name: 'original_workno') final String? originalWorkno; @override @JsonKey(name: 'is_translation_agree') final bool? isTranslationAgree; final Map? _translationBonusLangs; @override @JsonKey( name: 'translation_bonus_langs', fromJson: _translationBonusLangsFromJson, toJson: _translationBonusLangsToJson) Map? get translationBonusLangs { final value = _translationBonusLangs; if (value == null) return null; if (_translationBonusLangs is EqualUnmodifiableMapView) return _translationBonusLangs; // ignore: implicit_dynamic_type return EqualUnmodifiableMapView(value); } @override @JsonKey(name: 'is_translation_bonus_child') final bool? isTranslationBonusChild; @override @JsonKey(name: 'production_trade_price_rate') final int? productionTradePriceRate; @override String toString() { return 'TranslationInfo(lang: $lang, isChild: $isChild, isParent: $isParent, isOriginal: $isOriginal, isVolunteer: $isVolunteer, childWorknos: $childWorknos, parentWorkno: $parentWorkno, originalWorkno: $originalWorkno, isTranslationAgree: $isTranslationAgree, translationBonusLangs: $translationBonusLangs, isTranslationBonusChild: $isTranslationBonusChild, productionTradePriceRate: $productionTradePriceRate)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$TranslationInfoImpl && (identical(other.lang, lang) || other.lang == lang) && (identical(other.isChild, isChild) || other.isChild == isChild) && (identical(other.isParent, isParent) || other.isParent == isParent) && (identical(other.isOriginal, isOriginal) || other.isOriginal == isOriginal) && (identical(other.isVolunteer, isVolunteer) || other.isVolunteer == isVolunteer) && const DeepCollectionEquality() .equals(other._childWorknos, _childWorknos) && (identical(other.parentWorkno, parentWorkno) || other.parentWorkno == parentWorkno) && (identical(other.originalWorkno, originalWorkno) || other.originalWorkno == originalWorkno) && (identical(other.isTranslationAgree, isTranslationAgree) || other.isTranslationAgree == isTranslationAgree) && const DeepCollectionEquality() .equals(other._translationBonusLangs, _translationBonusLangs) && (identical( other.isTranslationBonusChild, isTranslationBonusChild) || other.isTranslationBonusChild == isTranslationBonusChild) && (identical( other.productionTradePriceRate, productionTradePriceRate) || other.productionTradePriceRate == productionTradePriceRate)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, lang, isChild, isParent, isOriginal, isVolunteer, const DeepCollectionEquality().hash(_childWorknos), parentWorkno, originalWorkno, isTranslationAgree, const DeepCollectionEquality().hash(_translationBonusLangs), isTranslationBonusChild, productionTradePriceRate); /// Create a copy of TranslationInfo /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$TranslationInfoImplCopyWith<_$TranslationInfoImpl> get copyWith => __$$TranslationInfoImplCopyWithImpl<_$TranslationInfoImpl>( this, _$identity); @override Map toJson() { return _$$TranslationInfoImplToJson( this, ); } } abstract class _TranslationInfo implements TranslationInfo { factory _TranslationInfo( {final String? lang, @JsonKey(name: 'is_child') final bool? isChild, @JsonKey(name: 'is_parent') final bool? isParent, @JsonKey(name: 'is_original') final bool? isOriginal, @JsonKey(name: 'is_volunteer') final bool? isVolunteer, @JsonKey(name: 'child_worknos') final List? childWorknos, @JsonKey(name: 'parent_workno') final String? parentWorkno, @JsonKey(name: 'original_workno') final String? originalWorkno, @JsonKey(name: 'is_translation_agree') final bool? isTranslationAgree, @JsonKey( name: 'translation_bonus_langs', fromJson: _translationBonusLangsFromJson, toJson: _translationBonusLangsToJson) final Map? translationBonusLangs, @JsonKey(name: 'is_translation_bonus_child') final bool? isTranslationBonusChild, @JsonKey(name: 'production_trade_price_rate') final int? productionTradePriceRate}) = _$TranslationInfoImpl; factory _TranslationInfo.fromJson(Map json) = _$TranslationInfoImpl.fromJson; @override String? get lang; @override @JsonKey(name: 'is_child') bool? get isChild; @override @JsonKey(name: 'is_parent') bool? get isParent; @override @JsonKey(name: 'is_original') bool? get isOriginal; @override @JsonKey(name: 'is_volunteer') bool? get isVolunteer; @override @JsonKey(name: 'child_worknos') List? get childWorknos; @override @JsonKey(name: 'parent_workno') String? get parentWorkno; @override @JsonKey(name: 'original_workno') String? get originalWorkno; @override @JsonKey(name: 'is_translation_agree') bool? get isTranslationAgree; @override @JsonKey( name: 'translation_bonus_langs', fromJson: _translationBonusLangsFromJson, toJson: _translationBonusLangsToJson) Map? get translationBonusLangs; @override @JsonKey(name: 'is_translation_bonus_child') bool? get isTranslationBonusChild; @override @JsonKey(name: 'production_trade_price_rate') int? get productionTradePriceRate; /// Create a copy of TranslationInfo /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$TranslationInfoImplCopyWith<_$TranslationInfoImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/works/translation_info.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'translation_info.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$TranslationInfoImpl _$$TranslationInfoImplFromJson( Map json) => _$TranslationInfoImpl( lang: json['lang'] as String?, isChild: json['is_child'] as bool?, isParent: json['is_parent'] as bool?, isOriginal: json['is_original'] as bool?, isVolunteer: json['is_volunteer'] as bool?, childWorknos: json['child_worknos'] as List?, parentWorkno: json['parent_workno'] as String?, originalWorkno: json['original_workno'] as String?, isTranslationAgree: json['is_translation_agree'] as bool?, translationBonusLangs: _translationBonusLangsFromJson(json['translation_bonus_langs']), isTranslationBonusChild: json['is_translation_bonus_child'] as bool?, productionTradePriceRate: (json['production_trade_price_rate'] as num?)?.toInt(), ); Map _$$TranslationInfoImplToJson( _$TranslationInfoImpl instance) => { 'lang': instance.lang, 'is_child': instance.isChild, 'is_parent': instance.isParent, 'is_original': instance.isOriginal, 'is_volunteer': instance.isVolunteer, 'child_worknos': instance.childWorknos, 'parent_workno': instance.parentWorkno, 'original_workno': instance.originalWorkno, 'is_translation_agree': instance.isTranslationAgree, 'translation_bonus_langs': _translationBonusLangsToJson(instance.translationBonusLangs), 'is_translation_bonus_child': instance.isTranslationBonusChild, 'production_trade_price_rate': instance.productionTradePriceRate, }; ================================================ FILE: lib/data/models/works/work.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'circle.dart'; import 'language_edition.dart'; import 'other_language_editions_in_db.dart'; import 'tag.dart'; import 'translation_info.dart'; part 'work.freezed.dart'; part 'work.g.dart'; @freezed class Work with _$Work { factory Work({ int? id, String? title, @JsonKey(name: 'circle_id') int? circleId, String? name, bool? nsfw, String? release, @JsonKey(name: 'dl_count') int? dlCount, int? price, @JsonKey(name: 'review_count') int? reviewCount, @JsonKey(name: 'rate_count') int? rateCount, @JsonKey(name: 'rate_average_2dp') int? rateAverage2dp, @JsonKey(name: 'rate_count_detail') List? rateCountDetail, dynamic rank, @JsonKey(name: 'has_subtitle') bool? hasSubtitle, @JsonKey(name: 'create_date') String? createDate, List? vas, List? tags, @JsonKey(name: 'language_editions') List? languageEditions, @JsonKey(name: 'original_workno') String? originalWorkno, @JsonKey(name: 'other_language_editions_in_db') List? otherLanguageEditionsInDb, @JsonKey(name: 'translation_info') TranslationInfo? translationInfo, @JsonKey(name: 'work_attributes') String? workAttributes, @JsonKey(name: 'age_category_string') String? ageCategoryString, int? duration, @JsonKey(name: 'source_type') String? sourceType, @JsonKey(name: 'source_id') String? sourceId, @JsonKey(name: 'source_url') String? sourceUrl, dynamic userRating, Circle? circle, String? samCoverUrl, String? thumbnailCoverUrl, String? mainCoverUrl, }) = _Work; factory Work.fromJson(Map json) => _$WorkFromJson(json); } ================================================ FILE: lib/data/models/works/work.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'work.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Work _$WorkFromJson(Map json) { return _Work.fromJson(json); } /// @nodoc mixin _$Work { int? get id => throw _privateConstructorUsedError; String? get title => throw _privateConstructorUsedError; @JsonKey(name: 'circle_id') int? get circleId => throw _privateConstructorUsedError; String? get name => throw _privateConstructorUsedError; bool? get nsfw => throw _privateConstructorUsedError; String? get release => throw _privateConstructorUsedError; @JsonKey(name: 'dl_count') int? get dlCount => throw _privateConstructorUsedError; int? get price => throw _privateConstructorUsedError; @JsonKey(name: 'review_count') int? get reviewCount => throw _privateConstructorUsedError; @JsonKey(name: 'rate_count') int? get rateCount => throw _privateConstructorUsedError; @JsonKey(name: 'rate_average_2dp') int? get rateAverage2dp => throw _privateConstructorUsedError; @JsonKey(name: 'rate_count_detail') List? get rateCountDetail => throw _privateConstructorUsedError; dynamic get rank => throw _privateConstructorUsedError; @JsonKey(name: 'has_subtitle') bool? get hasSubtitle => throw _privateConstructorUsedError; @JsonKey(name: 'create_date') String? get createDate => throw _privateConstructorUsedError; List? get vas => throw _privateConstructorUsedError; List? get tags => throw _privateConstructorUsedError; @JsonKey(name: 'language_editions') List? get languageEditions => throw _privateConstructorUsedError; @JsonKey(name: 'original_workno') String? get originalWorkno => throw _privateConstructorUsedError; @JsonKey(name: 'other_language_editions_in_db') List? get otherLanguageEditionsInDb => throw _privateConstructorUsedError; @JsonKey(name: 'translation_info') TranslationInfo? get translationInfo => throw _privateConstructorUsedError; @JsonKey(name: 'work_attributes') String? get workAttributes => throw _privateConstructorUsedError; @JsonKey(name: 'age_category_string') String? get ageCategoryString => throw _privateConstructorUsedError; int? get duration => throw _privateConstructorUsedError; @JsonKey(name: 'source_type') String? get sourceType => throw _privateConstructorUsedError; @JsonKey(name: 'source_id') String? get sourceId => throw _privateConstructorUsedError; @JsonKey(name: 'source_url') String? get sourceUrl => throw _privateConstructorUsedError; dynamic get userRating => throw _privateConstructorUsedError; Circle? get circle => throw _privateConstructorUsedError; String? get samCoverUrl => throw _privateConstructorUsedError; String? get thumbnailCoverUrl => throw _privateConstructorUsedError; String? get mainCoverUrl => throw _privateConstructorUsedError; /// Serializes this Work to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of Work /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $WorkCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $WorkCopyWith<$Res> { factory $WorkCopyWith(Work value, $Res Function(Work) then) = _$WorkCopyWithImpl<$Res, Work>; @useResult $Res call( {int? id, String? title, @JsonKey(name: 'circle_id') int? circleId, String? name, bool? nsfw, String? release, @JsonKey(name: 'dl_count') int? dlCount, int? price, @JsonKey(name: 'review_count') int? reviewCount, @JsonKey(name: 'rate_count') int? rateCount, @JsonKey(name: 'rate_average_2dp') int? rateAverage2dp, @JsonKey(name: 'rate_count_detail') List? rateCountDetail, dynamic rank, @JsonKey(name: 'has_subtitle') bool? hasSubtitle, @JsonKey(name: 'create_date') String? createDate, List? vas, List? tags, @JsonKey(name: 'language_editions') List? languageEditions, @JsonKey(name: 'original_workno') String? originalWorkno, @JsonKey(name: 'other_language_editions_in_db') List? otherLanguageEditionsInDb, @JsonKey(name: 'translation_info') TranslationInfo? translationInfo, @JsonKey(name: 'work_attributes') String? workAttributes, @JsonKey(name: 'age_category_string') String? ageCategoryString, int? duration, @JsonKey(name: 'source_type') String? sourceType, @JsonKey(name: 'source_id') String? sourceId, @JsonKey(name: 'source_url') String? sourceUrl, dynamic userRating, Circle? circle, String? samCoverUrl, String? thumbnailCoverUrl, String? mainCoverUrl}); $TranslationInfoCopyWith<$Res>? get translationInfo; $CircleCopyWith<$Res>? get circle; } /// @nodoc class _$WorkCopyWithImpl<$Res, $Val extends Work> implements $WorkCopyWith<$Res> { _$WorkCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of Work /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? title = freezed, Object? circleId = freezed, Object? name = freezed, Object? nsfw = freezed, Object? release = freezed, Object? dlCount = freezed, Object? price = freezed, Object? reviewCount = freezed, Object? rateCount = freezed, Object? rateAverage2dp = freezed, Object? rateCountDetail = freezed, Object? rank = freezed, Object? hasSubtitle = freezed, Object? createDate = freezed, Object? vas = freezed, Object? tags = freezed, Object? languageEditions = freezed, Object? originalWorkno = freezed, Object? otherLanguageEditionsInDb = freezed, Object? translationInfo = freezed, Object? workAttributes = freezed, Object? ageCategoryString = freezed, Object? duration = freezed, Object? sourceType = freezed, Object? sourceId = freezed, Object? sourceUrl = freezed, Object? userRating = freezed, Object? circle = freezed, Object? samCoverUrl = freezed, Object? thumbnailCoverUrl = freezed, Object? mainCoverUrl = freezed, }) { return _then(_value.copyWith( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as int?, title: freezed == title ? _value.title : title // ignore: cast_nullable_to_non_nullable as String?, circleId: freezed == circleId ? _value.circleId : circleId // ignore: cast_nullable_to_non_nullable as int?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, nsfw: freezed == nsfw ? _value.nsfw : nsfw // ignore: cast_nullable_to_non_nullable as bool?, release: freezed == release ? _value.release : release // ignore: cast_nullable_to_non_nullable as String?, dlCount: freezed == dlCount ? _value.dlCount : dlCount // ignore: cast_nullable_to_non_nullable as int?, price: freezed == price ? _value.price : price // ignore: cast_nullable_to_non_nullable as int?, reviewCount: freezed == reviewCount ? _value.reviewCount : reviewCount // ignore: cast_nullable_to_non_nullable as int?, rateCount: freezed == rateCount ? _value.rateCount : rateCount // ignore: cast_nullable_to_non_nullable as int?, rateAverage2dp: freezed == rateAverage2dp ? _value.rateAverage2dp : rateAverage2dp // ignore: cast_nullable_to_non_nullable as int?, rateCountDetail: freezed == rateCountDetail ? _value.rateCountDetail : rateCountDetail // ignore: cast_nullable_to_non_nullable as List?, rank: freezed == rank ? _value.rank : rank // ignore: cast_nullable_to_non_nullable as dynamic, hasSubtitle: freezed == hasSubtitle ? _value.hasSubtitle : hasSubtitle // ignore: cast_nullable_to_non_nullable as bool?, createDate: freezed == createDate ? _value.createDate : createDate // ignore: cast_nullable_to_non_nullable as String?, vas: freezed == vas ? _value.vas : vas // ignore: cast_nullable_to_non_nullable as List?, tags: freezed == tags ? _value.tags : tags // ignore: cast_nullable_to_non_nullable as List?, languageEditions: freezed == languageEditions ? _value.languageEditions : languageEditions // ignore: cast_nullable_to_non_nullable as List?, originalWorkno: freezed == originalWorkno ? _value.originalWorkno : originalWorkno // ignore: cast_nullable_to_non_nullable as String?, otherLanguageEditionsInDb: freezed == otherLanguageEditionsInDb ? _value.otherLanguageEditionsInDb : otherLanguageEditionsInDb // ignore: cast_nullable_to_non_nullable as List?, translationInfo: freezed == translationInfo ? _value.translationInfo : translationInfo // ignore: cast_nullable_to_non_nullable as TranslationInfo?, workAttributes: freezed == workAttributes ? _value.workAttributes : workAttributes // ignore: cast_nullable_to_non_nullable as String?, ageCategoryString: freezed == ageCategoryString ? _value.ageCategoryString : ageCategoryString // ignore: cast_nullable_to_non_nullable as String?, duration: freezed == duration ? _value.duration : duration // ignore: cast_nullable_to_non_nullable as int?, sourceType: freezed == sourceType ? _value.sourceType : sourceType // ignore: cast_nullable_to_non_nullable as String?, sourceId: freezed == sourceId ? _value.sourceId : sourceId // ignore: cast_nullable_to_non_nullable as String?, sourceUrl: freezed == sourceUrl ? _value.sourceUrl : sourceUrl // ignore: cast_nullable_to_non_nullable as String?, userRating: freezed == userRating ? _value.userRating : userRating // ignore: cast_nullable_to_non_nullable as dynamic, circle: freezed == circle ? _value.circle : circle // ignore: cast_nullable_to_non_nullable as Circle?, samCoverUrl: freezed == samCoverUrl ? _value.samCoverUrl : samCoverUrl // ignore: cast_nullable_to_non_nullable as String?, thumbnailCoverUrl: freezed == thumbnailCoverUrl ? _value.thumbnailCoverUrl : thumbnailCoverUrl // ignore: cast_nullable_to_non_nullable as String?, mainCoverUrl: freezed == mainCoverUrl ? _value.mainCoverUrl : mainCoverUrl // ignore: cast_nullable_to_non_nullable as String?, ) as $Val); } /// Create a copy of Work /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $TranslationInfoCopyWith<$Res>? get translationInfo { if (_value.translationInfo == null) { return null; } return $TranslationInfoCopyWith<$Res>(_value.translationInfo!, (value) { return _then(_value.copyWith(translationInfo: value) as $Val); }); } /// Create a copy of Work /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $CircleCopyWith<$Res>? get circle { if (_value.circle == null) { return null; } return $CircleCopyWith<$Res>(_value.circle!, (value) { return _then(_value.copyWith(circle: value) as $Val); }); } } /// @nodoc abstract class _$$WorkImplCopyWith<$Res> implements $WorkCopyWith<$Res> { factory _$$WorkImplCopyWith( _$WorkImpl value, $Res Function(_$WorkImpl) then) = __$$WorkImplCopyWithImpl<$Res>; @override @useResult $Res call( {int? id, String? title, @JsonKey(name: 'circle_id') int? circleId, String? name, bool? nsfw, String? release, @JsonKey(name: 'dl_count') int? dlCount, int? price, @JsonKey(name: 'review_count') int? reviewCount, @JsonKey(name: 'rate_count') int? rateCount, @JsonKey(name: 'rate_average_2dp') int? rateAverage2dp, @JsonKey(name: 'rate_count_detail') List? rateCountDetail, dynamic rank, @JsonKey(name: 'has_subtitle') bool? hasSubtitle, @JsonKey(name: 'create_date') String? createDate, List? vas, List? tags, @JsonKey(name: 'language_editions') List? languageEditions, @JsonKey(name: 'original_workno') String? originalWorkno, @JsonKey(name: 'other_language_editions_in_db') List? otherLanguageEditionsInDb, @JsonKey(name: 'translation_info') TranslationInfo? translationInfo, @JsonKey(name: 'work_attributes') String? workAttributes, @JsonKey(name: 'age_category_string') String? ageCategoryString, int? duration, @JsonKey(name: 'source_type') String? sourceType, @JsonKey(name: 'source_id') String? sourceId, @JsonKey(name: 'source_url') String? sourceUrl, dynamic userRating, Circle? circle, String? samCoverUrl, String? thumbnailCoverUrl, String? mainCoverUrl}); @override $TranslationInfoCopyWith<$Res>? get translationInfo; @override $CircleCopyWith<$Res>? get circle; } /// @nodoc class __$$WorkImplCopyWithImpl<$Res> extends _$WorkCopyWithImpl<$Res, _$WorkImpl> implements _$$WorkImplCopyWith<$Res> { __$$WorkImplCopyWithImpl(_$WorkImpl _value, $Res Function(_$WorkImpl) _then) : super(_value, _then); /// Create a copy of Work /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? title = freezed, Object? circleId = freezed, Object? name = freezed, Object? nsfw = freezed, Object? release = freezed, Object? dlCount = freezed, Object? price = freezed, Object? reviewCount = freezed, Object? rateCount = freezed, Object? rateAverage2dp = freezed, Object? rateCountDetail = freezed, Object? rank = freezed, Object? hasSubtitle = freezed, Object? createDate = freezed, Object? vas = freezed, Object? tags = freezed, Object? languageEditions = freezed, Object? originalWorkno = freezed, Object? otherLanguageEditionsInDb = freezed, Object? translationInfo = freezed, Object? workAttributes = freezed, Object? ageCategoryString = freezed, Object? duration = freezed, Object? sourceType = freezed, Object? sourceId = freezed, Object? sourceUrl = freezed, Object? userRating = freezed, Object? circle = freezed, Object? samCoverUrl = freezed, Object? thumbnailCoverUrl = freezed, Object? mainCoverUrl = freezed, }) { return _then(_$WorkImpl( id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as int?, title: freezed == title ? _value.title : title // ignore: cast_nullable_to_non_nullable as String?, circleId: freezed == circleId ? _value.circleId : circleId // ignore: cast_nullable_to_non_nullable as int?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, nsfw: freezed == nsfw ? _value.nsfw : nsfw // ignore: cast_nullable_to_non_nullable as bool?, release: freezed == release ? _value.release : release // ignore: cast_nullable_to_non_nullable as String?, dlCount: freezed == dlCount ? _value.dlCount : dlCount // ignore: cast_nullable_to_non_nullable as int?, price: freezed == price ? _value.price : price // ignore: cast_nullable_to_non_nullable as int?, reviewCount: freezed == reviewCount ? _value.reviewCount : reviewCount // ignore: cast_nullable_to_non_nullable as int?, rateCount: freezed == rateCount ? _value.rateCount : rateCount // ignore: cast_nullable_to_non_nullable as int?, rateAverage2dp: freezed == rateAverage2dp ? _value.rateAverage2dp : rateAverage2dp // ignore: cast_nullable_to_non_nullable as int?, rateCountDetail: freezed == rateCountDetail ? _value._rateCountDetail : rateCountDetail // ignore: cast_nullable_to_non_nullable as List?, rank: freezed == rank ? _value.rank : rank // ignore: cast_nullable_to_non_nullable as dynamic, hasSubtitle: freezed == hasSubtitle ? _value.hasSubtitle : hasSubtitle // ignore: cast_nullable_to_non_nullable as bool?, createDate: freezed == createDate ? _value.createDate : createDate // ignore: cast_nullable_to_non_nullable as String?, vas: freezed == vas ? _value._vas : vas // ignore: cast_nullable_to_non_nullable as List?, tags: freezed == tags ? _value._tags : tags // ignore: cast_nullable_to_non_nullable as List?, languageEditions: freezed == languageEditions ? _value._languageEditions : languageEditions // ignore: cast_nullable_to_non_nullable as List?, originalWorkno: freezed == originalWorkno ? _value.originalWorkno : originalWorkno // ignore: cast_nullable_to_non_nullable as String?, otherLanguageEditionsInDb: freezed == otherLanguageEditionsInDb ? _value._otherLanguageEditionsInDb : otherLanguageEditionsInDb // ignore: cast_nullable_to_non_nullable as List?, translationInfo: freezed == translationInfo ? _value.translationInfo : translationInfo // ignore: cast_nullable_to_non_nullable as TranslationInfo?, workAttributes: freezed == workAttributes ? _value.workAttributes : workAttributes // ignore: cast_nullable_to_non_nullable as String?, ageCategoryString: freezed == ageCategoryString ? _value.ageCategoryString : ageCategoryString // ignore: cast_nullable_to_non_nullable as String?, duration: freezed == duration ? _value.duration : duration // ignore: cast_nullable_to_non_nullable as int?, sourceType: freezed == sourceType ? _value.sourceType : sourceType // ignore: cast_nullable_to_non_nullable as String?, sourceId: freezed == sourceId ? _value.sourceId : sourceId // ignore: cast_nullable_to_non_nullable as String?, sourceUrl: freezed == sourceUrl ? _value.sourceUrl : sourceUrl // ignore: cast_nullable_to_non_nullable as String?, userRating: freezed == userRating ? _value.userRating : userRating // ignore: cast_nullable_to_non_nullable as dynamic, circle: freezed == circle ? _value.circle : circle // ignore: cast_nullable_to_non_nullable as Circle?, samCoverUrl: freezed == samCoverUrl ? _value.samCoverUrl : samCoverUrl // ignore: cast_nullable_to_non_nullable as String?, thumbnailCoverUrl: freezed == thumbnailCoverUrl ? _value.thumbnailCoverUrl : thumbnailCoverUrl // ignore: cast_nullable_to_non_nullable as String?, mainCoverUrl: freezed == mainCoverUrl ? _value.mainCoverUrl : mainCoverUrl // ignore: cast_nullable_to_non_nullable as String?, )); } } /// @nodoc @JsonSerializable() class _$WorkImpl implements _Work { _$WorkImpl( {this.id, this.title, @JsonKey(name: 'circle_id') this.circleId, this.name, this.nsfw, this.release, @JsonKey(name: 'dl_count') this.dlCount, this.price, @JsonKey(name: 'review_count') this.reviewCount, @JsonKey(name: 'rate_count') this.rateCount, @JsonKey(name: 'rate_average_2dp') this.rateAverage2dp, @JsonKey(name: 'rate_count_detail') final List? rateCountDetail, this.rank, @JsonKey(name: 'has_subtitle') this.hasSubtitle, @JsonKey(name: 'create_date') this.createDate, final List? vas, final List? tags, @JsonKey(name: 'language_editions') final List? languageEditions, @JsonKey(name: 'original_workno') this.originalWorkno, @JsonKey(name: 'other_language_editions_in_db') final List? otherLanguageEditionsInDb, @JsonKey(name: 'translation_info') this.translationInfo, @JsonKey(name: 'work_attributes') this.workAttributes, @JsonKey(name: 'age_category_string') this.ageCategoryString, this.duration, @JsonKey(name: 'source_type') this.sourceType, @JsonKey(name: 'source_id') this.sourceId, @JsonKey(name: 'source_url') this.sourceUrl, this.userRating, this.circle, this.samCoverUrl, this.thumbnailCoverUrl, this.mainCoverUrl}) : _rateCountDetail = rateCountDetail, _vas = vas, _tags = tags, _languageEditions = languageEditions, _otherLanguageEditionsInDb = otherLanguageEditionsInDb; factory _$WorkImpl.fromJson(Map json) => _$$WorkImplFromJson(json); @override final int? id; @override final String? title; @override @JsonKey(name: 'circle_id') final int? circleId; @override final String? name; @override final bool? nsfw; @override final String? release; @override @JsonKey(name: 'dl_count') final int? dlCount; @override final int? price; @override @JsonKey(name: 'review_count') final int? reviewCount; @override @JsonKey(name: 'rate_count') final int? rateCount; @override @JsonKey(name: 'rate_average_2dp') final int? rateAverage2dp; final List? _rateCountDetail; @override @JsonKey(name: 'rate_count_detail') List? get rateCountDetail { final value = _rateCountDetail; if (value == null) return null; if (_rateCountDetail is EqualUnmodifiableListView) return _rateCountDetail; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(value); } @override final dynamic rank; @override @JsonKey(name: 'has_subtitle') final bool? hasSubtitle; @override @JsonKey(name: 'create_date') final String? createDate; final List? _vas; @override List? get vas { final value = _vas; if (value == null) return null; if (_vas is EqualUnmodifiableListView) return _vas; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(value); } final List? _tags; @override List? get tags { final value = _tags; if (value == null) return null; if (_tags is EqualUnmodifiableListView) return _tags; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(value); } final List? _languageEditions; @override @JsonKey(name: 'language_editions') List? get languageEditions { final value = _languageEditions; if (value == null) return null; if (_languageEditions is EqualUnmodifiableListView) return _languageEditions; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(value); } @override @JsonKey(name: 'original_workno') final String? originalWorkno; final List? _otherLanguageEditionsInDb; @override @JsonKey(name: 'other_language_editions_in_db') List? get otherLanguageEditionsInDb { final value = _otherLanguageEditionsInDb; if (value == null) return null; if (_otherLanguageEditionsInDb is EqualUnmodifiableListView) return _otherLanguageEditionsInDb; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(value); } @override @JsonKey(name: 'translation_info') final TranslationInfo? translationInfo; @override @JsonKey(name: 'work_attributes') final String? workAttributes; @override @JsonKey(name: 'age_category_string') final String? ageCategoryString; @override final int? duration; @override @JsonKey(name: 'source_type') final String? sourceType; @override @JsonKey(name: 'source_id') final String? sourceId; @override @JsonKey(name: 'source_url') final String? sourceUrl; @override final dynamic userRating; @override final Circle? circle; @override final String? samCoverUrl; @override final String? thumbnailCoverUrl; @override final String? mainCoverUrl; @override String toString() { return 'Work(id: $id, title: $title, circleId: $circleId, name: $name, nsfw: $nsfw, release: $release, dlCount: $dlCount, price: $price, reviewCount: $reviewCount, rateCount: $rateCount, rateAverage2dp: $rateAverage2dp, rateCountDetail: $rateCountDetail, rank: $rank, hasSubtitle: $hasSubtitle, createDate: $createDate, vas: $vas, tags: $tags, languageEditions: $languageEditions, originalWorkno: $originalWorkno, otherLanguageEditionsInDb: $otherLanguageEditionsInDb, translationInfo: $translationInfo, workAttributes: $workAttributes, ageCategoryString: $ageCategoryString, duration: $duration, sourceType: $sourceType, sourceId: $sourceId, sourceUrl: $sourceUrl, userRating: $userRating, circle: $circle, samCoverUrl: $samCoverUrl, thumbnailCoverUrl: $thumbnailCoverUrl, mainCoverUrl: $mainCoverUrl)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$WorkImpl && (identical(other.id, id) || other.id == id) && (identical(other.title, title) || other.title == title) && (identical(other.circleId, circleId) || other.circleId == circleId) && (identical(other.name, name) || other.name == name) && (identical(other.nsfw, nsfw) || other.nsfw == nsfw) && (identical(other.release, release) || other.release == release) && (identical(other.dlCount, dlCount) || other.dlCount == dlCount) && (identical(other.price, price) || other.price == price) && (identical(other.reviewCount, reviewCount) || other.reviewCount == reviewCount) && (identical(other.rateCount, rateCount) || other.rateCount == rateCount) && (identical(other.rateAverage2dp, rateAverage2dp) || other.rateAverage2dp == rateAverage2dp) && const DeepCollectionEquality() .equals(other._rateCountDetail, _rateCountDetail) && const DeepCollectionEquality().equals(other.rank, rank) && (identical(other.hasSubtitle, hasSubtitle) || other.hasSubtitle == hasSubtitle) && (identical(other.createDate, createDate) || other.createDate == createDate) && const DeepCollectionEquality().equals(other._vas, _vas) && const DeepCollectionEquality().equals(other._tags, _tags) && const DeepCollectionEquality() .equals(other._languageEditions, _languageEditions) && (identical(other.originalWorkno, originalWorkno) || other.originalWorkno == originalWorkno) && const DeepCollectionEquality().equals( other._otherLanguageEditionsInDb, _otherLanguageEditionsInDb) && (identical(other.translationInfo, translationInfo) || other.translationInfo == translationInfo) && (identical(other.workAttributes, workAttributes) || other.workAttributes == workAttributes) && (identical(other.ageCategoryString, ageCategoryString) || other.ageCategoryString == ageCategoryString) && (identical(other.duration, duration) || other.duration == duration) && (identical(other.sourceType, sourceType) || other.sourceType == sourceType) && (identical(other.sourceId, sourceId) || other.sourceId == sourceId) && (identical(other.sourceUrl, sourceUrl) || other.sourceUrl == sourceUrl) && const DeepCollectionEquality() .equals(other.userRating, userRating) && (identical(other.circle, circle) || other.circle == circle) && (identical(other.samCoverUrl, samCoverUrl) || other.samCoverUrl == samCoverUrl) && (identical(other.thumbnailCoverUrl, thumbnailCoverUrl) || other.thumbnailCoverUrl == thumbnailCoverUrl) && (identical(other.mainCoverUrl, mainCoverUrl) || other.mainCoverUrl == mainCoverUrl)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hashAll([ runtimeType, id, title, circleId, name, nsfw, release, dlCount, price, reviewCount, rateCount, rateAverage2dp, const DeepCollectionEquality().hash(_rateCountDetail), const DeepCollectionEquality().hash(rank), hasSubtitle, createDate, const DeepCollectionEquality().hash(_vas), const DeepCollectionEquality().hash(_tags), const DeepCollectionEquality().hash(_languageEditions), originalWorkno, const DeepCollectionEquality().hash(_otherLanguageEditionsInDb), translationInfo, workAttributes, ageCategoryString, duration, sourceType, sourceId, sourceUrl, const DeepCollectionEquality().hash(userRating), circle, samCoverUrl, thumbnailCoverUrl, mainCoverUrl ]); /// Create a copy of Work /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$WorkImplCopyWith<_$WorkImpl> get copyWith => __$$WorkImplCopyWithImpl<_$WorkImpl>(this, _$identity); @override Map toJson() { return _$$WorkImplToJson( this, ); } } abstract class _Work implements Work { factory _Work( {final int? id, final String? title, @JsonKey(name: 'circle_id') final int? circleId, final String? name, final bool? nsfw, final String? release, @JsonKey(name: 'dl_count') final int? dlCount, final int? price, @JsonKey(name: 'review_count') final int? reviewCount, @JsonKey(name: 'rate_count') final int? rateCount, @JsonKey(name: 'rate_average_2dp') final int? rateAverage2dp, @JsonKey(name: 'rate_count_detail') final List? rateCountDetail, final dynamic rank, @JsonKey(name: 'has_subtitle') final bool? hasSubtitle, @JsonKey(name: 'create_date') final String? createDate, final List? vas, final List? tags, @JsonKey(name: 'language_editions') final List? languageEditions, @JsonKey(name: 'original_workno') final String? originalWorkno, @JsonKey(name: 'other_language_editions_in_db') final List? otherLanguageEditionsInDb, @JsonKey(name: 'translation_info') final TranslationInfo? translationInfo, @JsonKey(name: 'work_attributes') final String? workAttributes, @JsonKey(name: 'age_category_string') final String? ageCategoryString, final int? duration, @JsonKey(name: 'source_type') final String? sourceType, @JsonKey(name: 'source_id') final String? sourceId, @JsonKey(name: 'source_url') final String? sourceUrl, final dynamic userRating, final Circle? circle, final String? samCoverUrl, final String? thumbnailCoverUrl, final String? mainCoverUrl}) = _$WorkImpl; factory _Work.fromJson(Map json) = _$WorkImpl.fromJson; @override int? get id; @override String? get title; @override @JsonKey(name: 'circle_id') int? get circleId; @override String? get name; @override bool? get nsfw; @override String? get release; @override @JsonKey(name: 'dl_count') int? get dlCount; @override int? get price; @override @JsonKey(name: 'review_count') int? get reviewCount; @override @JsonKey(name: 'rate_count') int? get rateCount; @override @JsonKey(name: 'rate_average_2dp') int? get rateAverage2dp; @override @JsonKey(name: 'rate_count_detail') List? get rateCountDetail; @override dynamic get rank; @override @JsonKey(name: 'has_subtitle') bool? get hasSubtitle; @override @JsonKey(name: 'create_date') String? get createDate; @override List? get vas; @override List? get tags; @override @JsonKey(name: 'language_editions') List? get languageEditions; @override @JsonKey(name: 'original_workno') String? get originalWorkno; @override @JsonKey(name: 'other_language_editions_in_db') List? get otherLanguageEditionsInDb; @override @JsonKey(name: 'translation_info') TranslationInfo? get translationInfo; @override @JsonKey(name: 'work_attributes') String? get workAttributes; @override @JsonKey(name: 'age_category_string') String? get ageCategoryString; @override int? get duration; @override @JsonKey(name: 'source_type') String? get sourceType; @override @JsonKey(name: 'source_id') String? get sourceId; @override @JsonKey(name: 'source_url') String? get sourceUrl; @override dynamic get userRating; @override Circle? get circle; @override String? get samCoverUrl; @override String? get thumbnailCoverUrl; @override String? get mainCoverUrl; /// Create a copy of Work /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$WorkImplCopyWith<_$WorkImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/works/work.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'work.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$WorkImpl _$$WorkImplFromJson(Map json) => _$WorkImpl( id: (json['id'] as num?)?.toInt(), title: json['title'] as String?, circleId: (json['circle_id'] as num?)?.toInt(), name: json['name'] as String?, nsfw: json['nsfw'] as bool?, release: json['release'] as String?, dlCount: (json['dl_count'] as num?)?.toInt(), price: (json['price'] as num?)?.toInt(), reviewCount: (json['review_count'] as num?)?.toInt(), rateCount: (json['rate_count'] as num?)?.toInt(), rateAverage2dp: (json['rate_average_2dp'] as num?)?.toInt(), rateCountDetail: json['rate_count_detail'] as List?, rank: json['rank'], hasSubtitle: json['has_subtitle'] as bool?, createDate: json['create_date'] as String?, vas: json['vas'] as List?, tags: (json['tags'] as List?) ?.map((e) => Tag.fromJson(e as Map)) .toList(), languageEditions: (json['language_editions'] as List?) ?.map((e) => LanguageEdition.fromJson(e as Map)) .toList(), originalWorkno: json['original_workno'] as String?, otherLanguageEditionsInDb: (json['other_language_editions_in_db'] as List?) ?.map((e) => OtherLanguageEditionsInDb.fromJson(e as Map)) .toList(), translationInfo: json['translation_info'] == null ? null : TranslationInfo.fromJson( json['translation_info'] as Map), workAttributes: json['work_attributes'] as String?, ageCategoryString: json['age_category_string'] as String?, duration: (json['duration'] as num?)?.toInt(), sourceType: json['source_type'] as String?, sourceId: json['source_id'] as String?, sourceUrl: json['source_url'] as String?, userRating: json['userRating'], circle: json['circle'] == null ? null : Circle.fromJson(json['circle'] as Map), samCoverUrl: json['samCoverUrl'] as String?, thumbnailCoverUrl: json['thumbnailCoverUrl'] as String?, mainCoverUrl: json['mainCoverUrl'] as String?, ); Map _$$WorkImplToJson(_$WorkImpl instance) => { 'id': instance.id, 'title': instance.title, 'circle_id': instance.circleId, 'name': instance.name, 'nsfw': instance.nsfw, 'release': instance.release, 'dl_count': instance.dlCount, 'price': instance.price, 'review_count': instance.reviewCount, 'rate_count': instance.rateCount, 'rate_average_2dp': instance.rateAverage2dp, 'rate_count_detail': instance.rateCountDetail, 'rank': instance.rank, 'has_subtitle': instance.hasSubtitle, 'create_date': instance.createDate, 'vas': instance.vas, 'tags': instance.tags, 'language_editions': instance.languageEditions, 'original_workno': instance.originalWorkno, 'other_language_editions_in_db': instance.otherLanguageEditionsInDb, 'translation_info': instance.translationInfo, 'work_attributes': instance.workAttributes, 'age_category_string': instance.ageCategoryString, 'duration': instance.duration, 'source_type': instance.sourceType, 'source_id': instance.sourceId, 'source_url': instance.sourceUrl, 'userRating': instance.userRating, 'circle': instance.circle, 'samCoverUrl': instance.samCoverUrl, 'thumbnailCoverUrl': instance.thumbnailCoverUrl, 'mainCoverUrl': instance.mainCoverUrl, }; ================================================ FILE: lib/data/models/works/works.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'pagination.dart'; import 'work.dart'; part 'works.freezed.dart'; part 'works.g.dart'; @freezed class Works with _$Works { factory Works({ List? works, Pagination? pagination, }) = _Works; factory Works.fromJson(Map json) => _$WorksFromJson(json); } ================================================ FILE: lib/data/models/works/works.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'works.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Works _$WorksFromJson(Map json) { return _Works.fromJson(json); } /// @nodoc mixin _$Works { List? get works => throw _privateConstructorUsedError; Pagination? get pagination => throw _privateConstructorUsedError; /// Serializes this Works to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of Works /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $WorksCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $WorksCopyWith<$Res> { factory $WorksCopyWith(Works value, $Res Function(Works) then) = _$WorksCopyWithImpl<$Res, Works>; @useResult $Res call({List? works, Pagination? pagination}); $PaginationCopyWith<$Res>? get pagination; } /// @nodoc class _$WorksCopyWithImpl<$Res, $Val extends Works> implements $WorksCopyWith<$Res> { _$WorksCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of Works /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? works = freezed, Object? pagination = freezed, }) { return _then(_value.copyWith( works: freezed == works ? _value.works : works // ignore: cast_nullable_to_non_nullable as List?, pagination: freezed == pagination ? _value.pagination : pagination // ignore: cast_nullable_to_non_nullable as Pagination?, ) as $Val); } /// Create a copy of Works /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $PaginationCopyWith<$Res>? get pagination { if (_value.pagination == null) { return null; } return $PaginationCopyWith<$Res>(_value.pagination!, (value) { return _then(_value.copyWith(pagination: value) as $Val); }); } } /// @nodoc abstract class _$$WorksImplCopyWith<$Res> implements $WorksCopyWith<$Res> { factory _$$WorksImplCopyWith( _$WorksImpl value, $Res Function(_$WorksImpl) then) = __$$WorksImplCopyWithImpl<$Res>; @override @useResult $Res call({List? works, Pagination? pagination}); @override $PaginationCopyWith<$Res>? get pagination; } /// @nodoc class __$$WorksImplCopyWithImpl<$Res> extends _$WorksCopyWithImpl<$Res, _$WorksImpl> implements _$$WorksImplCopyWith<$Res> { __$$WorksImplCopyWithImpl( _$WorksImpl _value, $Res Function(_$WorksImpl) _then) : super(_value, _then); /// Create a copy of Works /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? works = freezed, Object? pagination = freezed, }) { return _then(_$WorksImpl( works: freezed == works ? _value._works : works // ignore: cast_nullable_to_non_nullable as List?, pagination: freezed == pagination ? _value.pagination : pagination // ignore: cast_nullable_to_non_nullable as Pagination?, )); } } /// @nodoc @JsonSerializable() class _$WorksImpl implements _Works { _$WorksImpl({final List? works, this.pagination}) : _works = works; factory _$WorksImpl.fromJson(Map json) => _$$WorksImplFromJson(json); final List? _works; @override List? get works { final value = _works; if (value == null) return null; if (_works is EqualUnmodifiableListView) return _works; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(value); } @override final Pagination? pagination; @override String toString() { return 'Works(works: $works, pagination: $pagination)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$WorksImpl && const DeepCollectionEquality().equals(other._works, _works) && (identical(other.pagination, pagination) || other.pagination == pagination)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, const DeepCollectionEquality().hash(_works), pagination); /// Create a copy of Works /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$WorksImplCopyWith<_$WorksImpl> get copyWith => __$$WorksImplCopyWithImpl<_$WorksImpl>(this, _$identity); @override Map toJson() { return _$$WorksImplToJson( this, ); } } abstract class _Works implements Works { factory _Works({final List? works, final Pagination? pagination}) = _$WorksImpl; factory _Works.fromJson(Map json) = _$WorksImpl.fromJson; @override List? get works; @override Pagination? get pagination; /// Create a copy of Works /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$WorksImplCopyWith<_$WorksImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/works/works.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'works.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$WorksImpl _$$WorksImplFromJson(Map json) => _$WorksImpl( works: (json['works'] as List?) ?.map((e) => Work.fromJson(e as Map)) .toList(), pagination: json['pagination'] == null ? null : Pagination.fromJson(json['pagination'] as Map), ); Map _$$WorksImplToJson(_$WorksImpl instance) => { 'works': instance.works, 'pagination': instance.pagination, }; ================================================ FILE: lib/data/models/works/zh_cn.dart ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; part 'zh_cn.freezed.dart'; part 'zh_cn.g.dart'; @freezed class ZhCn with _$ZhCn { factory ZhCn({ String? name, List? history, }) = _ZhCn; factory ZhCn.fromJson(Map json) => _$ZhCnFromJson(json); } ================================================ FILE: lib/data/models/works/zh_cn.freezed.dart ================================================ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'zh_cn.dart'; // ************************************************************************** // FreezedGenerator // ************************************************************************** T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); ZhCn _$ZhCnFromJson(Map json) { return _ZhCn.fromJson(json); } /// @nodoc mixin _$ZhCn { String? get name => throw _privateConstructorUsedError; List? get history => throw _privateConstructorUsedError; /// Serializes this ZhCn to a JSON map. Map toJson() => throw _privateConstructorUsedError; /// Create a copy of ZhCn /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) $ZhCnCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc abstract class $ZhCnCopyWith<$Res> { factory $ZhCnCopyWith(ZhCn value, $Res Function(ZhCn) then) = _$ZhCnCopyWithImpl<$Res, ZhCn>; @useResult $Res call({String? name, List? history}); } /// @nodoc class _$ZhCnCopyWithImpl<$Res, $Val extends ZhCn> implements $ZhCnCopyWith<$Res> { _$ZhCnCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; /// Create a copy of ZhCn /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? name = freezed, Object? history = freezed, }) { return _then(_value.copyWith( name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, history: freezed == history ? _value.history : history // ignore: cast_nullable_to_non_nullable as List?, ) as $Val); } } /// @nodoc abstract class _$$ZhCnImplCopyWith<$Res> implements $ZhCnCopyWith<$Res> { factory _$$ZhCnImplCopyWith( _$ZhCnImpl value, $Res Function(_$ZhCnImpl) then) = __$$ZhCnImplCopyWithImpl<$Res>; @override @useResult $Res call({String? name, List? history}); } /// @nodoc class __$$ZhCnImplCopyWithImpl<$Res> extends _$ZhCnCopyWithImpl<$Res, _$ZhCnImpl> implements _$$ZhCnImplCopyWith<$Res> { __$$ZhCnImplCopyWithImpl(_$ZhCnImpl _value, $Res Function(_$ZhCnImpl) _then) : super(_value, _then); /// Create a copy of ZhCn /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? name = freezed, Object? history = freezed, }) { return _then(_$ZhCnImpl( name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, history: freezed == history ? _value._history : history // ignore: cast_nullable_to_non_nullable as List?, )); } } /// @nodoc @JsonSerializable() class _$ZhCnImpl implements _ZhCn { _$ZhCnImpl({this.name, final List? history}) : _history = history; factory _$ZhCnImpl.fromJson(Map json) => _$$ZhCnImplFromJson(json); @override final String? name; final List? _history; @override List? get history { final value = _history; if (value == null) return null; if (_history is EqualUnmodifiableListView) return _history; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(value); } @override String toString() { return 'ZhCn(name: $name, history: $history)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$ZhCnImpl && (identical(other.name, name) || other.name == name) && const DeepCollectionEquality().equals(other._history, _history)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, name, const DeepCollectionEquality().hash(_history)); /// Create a copy of ZhCn /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ZhCnImplCopyWith<_$ZhCnImpl> get copyWith => __$$ZhCnImplCopyWithImpl<_$ZhCnImpl>(this, _$identity); @override Map toJson() { return _$$ZhCnImplToJson( this, ); } } abstract class _ZhCn implements ZhCn { factory _ZhCn({final String? name, final List? history}) = _$ZhCnImpl; factory _ZhCn.fromJson(Map json) = _$ZhCnImpl.fromJson; @override String? get name; @override List? get history; /// Create a copy of ZhCn /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) _$$ZhCnImplCopyWith<_$ZhCnImpl> get copyWith => throw _privateConstructorUsedError; } ================================================ FILE: lib/data/models/works/zh_cn.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'zh_cn.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _$ZhCnImpl _$$ZhCnImplFromJson(Map json) => _$ZhCnImpl( name: json['name'] as String?, history: json['history'] as List?, ); Map _$$ZhCnImplToJson(_$ZhCnImpl instance) => { 'name': instance.name, 'history': instance.history, }; ================================================ FILE: lib/data/repositories/audio/README.md ================================================ # 音频数据仓库 此目录包含音频数据访问的仓库实现。 ## 文件结构 - `audio_repository.dart` - 音频数据仓库实现 - `audio_repository_impl.dart` - 音频数据仓库具体实现 - `audio_cache_repository.dart` - 音频缓存仓库 ## 职责 - 音频数据的获取和存储 - 播放历史记录的管理 - 音频缓存的处理 ================================================ FILE: lib/data/repositories/auth_repository.dart ================================================ import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:asmrapp/data/models/auth/auth_resp/auth_resp.dart'; import 'package:asmrapp/utils/logger.dart'; class AuthRepository { static const _authDataKey = 'auth_data'; final SharedPreferences _prefs; AuthRepository(this._prefs); Future saveAuthData(AuthResp authData) async { try { final jsonStr = json.encode(authData.toJson()); await _prefs.setString(_authDataKey, jsonStr); AppLogger.info('保存认证数据成功'); } catch (e) { AppLogger.error('保存认证数据失败', e); rethrow; } } Future getAuthData() async { try { final jsonStr = _prefs.getString(_authDataKey); if (jsonStr == null) return null; final authData = AuthResp.fromJson(json.decode(jsonStr)); AppLogger.info('读取认证数据成功: ${authData.user?.name}'); return authData; } catch (e) { AppLogger.error('读取认证数据失败', e); return null; } } Future clearAuthData() async { try { await _prefs.remove(_authDataKey); AppLogger.info('清除认证数据成功'); } catch (e) { AppLogger.error('清除认证数据失败', e); rethrow; } } } ================================================ FILE: lib/data/services/api_service.dart ================================================ import 'package:asmrapp/core/cache/recommendation_cache_manager.dart'; import 'package:asmrapp/data/models/mark_status.dart'; import 'package:asmrapp/data/models/playlists_with_exist_statu/playlists_with_exist_statu.dart'; import 'package:dio/dio.dart'; import 'package:asmrapp/data/models/files/files.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/data/models/works/pagination.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:asmrapp/data/services/interceptors/auth_interceptor.dart'; import 'package:asmrapp/data/models/playlists_with_exist_statu/playlist.dart'; import 'package:asmrapp/data/models/my_lists/my_playlists/my_playlists.dart'; class WorksResponse { final List works; final Pagination pagination; WorksResponse({required this.works, required this.pagination}); } class ApiService { final Dio _dio; final _recommendationCache = RecommendationCacheManager(); ApiService() : _dio = Dio(BaseOptions( baseUrl: 'https://api.asmr.one/api', )) { _dio.interceptors.add(AuthInterceptor()); } /// 获取作品文件列表 Future getWorkFiles(String workId, {CancelToken? cancelToken}) async { try { final response = await _dio.get( '/tracks/$workId', queryParameters: { 'v': '1', }, cancelToken: cancelToken, // 添加 cancelToken 支持 ); if (response.statusCode == 200) { final filesData = { 'type': 'root', 'title': 'Root', 'children': response.data, }; return Files.fromJson(filesData); } throw Exception('获取文件列表失败: ${response.statusCode}'); } on DioException catch (e) { AppLogger.error('网络请求失败', e, e.stackTrace); throw Exception('网络请求失败: ${e.message}'); } catch (e, stackTrace) { AppLogger.error('解析数据失败', e, stackTrace); throw Exception('解析数据失败: $e'); } } /// 获取作品列表 Future getWorks({ int page = 1, bool hasSubtitle = false, String order = 'create_date', String sort = 'desc', String playlistId = '', }) async { try { final queryParams = { 'page': page, 'subtitle': hasSubtitle ? 1 : 0, 'order': order, 'sort': sort, }; // 如果提供了收藏夹ID,添加到查询参数 if (playlistId.isNotEmpty) { queryParams['withPlaylistStatus[]'] = playlistId; } final response = await _dio.get( '/works', queryParameters: queryParams, ); if (response.statusCode == 200) { final List works = response.data['works'] ?? []; final pagination = Pagination.fromJson(response.data['pagination']); return WorksResponse( works: works.map((work) => Work.fromJson(work)).toList(), pagination: pagination, ); } throw Exception('获取作品列表失败: ${response.statusCode}'); } on DioException catch (e) { AppLogger.error('网络请求失败', e, e.stackTrace); throw Exception('网络请求失败: ${e.message}'); } catch (e, stackTrace) { AppLogger.error('解析数据失败', e, stackTrace); throw Exception('解析数据失败: $e'); } } /// 搜索作品 Future searchWorks({ required String keyword, int page = 1, String order = 'create_date', String sort = 'desc', bool hasSubtitle = false, }) async { try { final response = await _dio.get( '/search/${Uri.encodeComponent(keyword)}', queryParameters: { 'page': page, 'order': order, 'sort': sort, 'subtitle': hasSubtitle ? 1 : 0, 'includeTranslationWorks': true, }, ); if (response.statusCode == 200) { AppLogger.debug('搜索返回数据: ${response.data}'); final works = (response.data['works'] as List) .map((work) => Work.fromJson(work)) .toList(); final pagination = Pagination.fromJson(response.data['pagination']); return WorksResponse( works: works, pagination: pagination, ); } throw Exception('搜索失败: ${response.statusCode}'); } catch (e) { throw Exception('搜索请求失败: $e'); } } /// 获取收藏列表 Future getFavorites({int page = 1}) async { try { final response = await _dio.get('/review', queryParameters: { 'page': page, 'order': 'updated_at', 'sort': 'desc', }); if (response.statusCode == 200) { final List works = response.data['works'] ?? []; final pagination = Pagination.fromJson(response.data['pagination']); return WorksResponse( works: works.map((work) => Work.fromJson(work)).toList(), pagination: pagination, ); } throw Exception('获取收藏列表失败: ${response.statusCode}'); } on DioException catch (e) { AppLogger.error('网络请求失败', e, e.stackTrace); throw Exception('网络请求失败: ${e.message}'); } catch (e, stackTrace) { AppLogger.error('解析数据失败', e, stackTrace); throw Exception('解析数据失败: $e'); } } /// 获取推荐作品 Future getRecommendations({ required String uuid, int page = 1, bool hasSubtitle = false, }) async { try { final response = await _dio.post( '/recommender/recommend-for-user', data: { 'keyword': ' ', 'userId': uuid, 'page': page, 'subtitle': hasSubtitle ? 1 : 0, 'localSubtitledWorks': [], 'withPlaylistStatus': [], }, ); if (response.statusCode == 200) { final List works = response.data['works'] ?? []; final pagination = Pagination.fromJson(response.data['pagination']); return WorksResponse( works: works.map((work) => Work.fromJson(work)).toList(), pagination: pagination, ); } throw Exception('获取推荐列表失败: ${response.statusCode}'); } on DioException catch (e) { AppLogger.error('网络请求失败', e, e.stackTrace); throw Exception('网络请求失败: ${e.message}'); } catch (e, stackTrace) { AppLogger.error('解析数据失败', e, stackTrace); throw Exception('解析数据失败: $e'); } } /// 获取热门作品 Future getPopular({ int page = 1, bool hasSubtitle = false, }) async { try { final response = await _dio.post( '/recommender/popular', data: { 'keyword': ' ', 'page': page, 'subtitle': hasSubtitle ? 1 : 0, 'localSubtitledWorks': [], 'withPlaylistStatus': [], }, ); if (response.statusCode == 200) { final List works = response.data['works'] ?? []; final pagination = Pagination.fromJson(response.data['pagination']); return WorksResponse( works: works.map((work) => Work.fromJson(work)).toList(), pagination: pagination, ); } throw Exception('获取热门列表失败: ${response.statusCode}'); } on DioException catch (e) { AppLogger.error('网络请求失败', e, e.stackTrace); throw Exception('网络请求失败: ${e.message}'); } catch (e, stackTrace) { AppLogger.error('解析数据失败', e, stackTrace); throw Exception('解析数据失败: $e'); } } /// 获取相关推荐作品 Future getItemNeighbors({ required String itemId, int page = 1, bool hasSubtitle = false, }) async { try { // 先尝试从缓存获取 final cachedData = _recommendationCache.get(itemId, page, hasSubtitle ? 1 : 0); if (cachedData != null) { return cachedData; } // 缓存未命中,从网络获取 final response = await _dio.post( '/recommender/item-neighbors', data: { 'keyword': '', 'itemId': itemId, 'page': page, 'subtitle': hasSubtitle ? 1 : 0, 'localSubtitledWorks': [], 'withPlaylistStatus': [], }, ); if (response.statusCode == 200) { final List works = response.data['works'] ?? []; final pagination = Pagination.fromJson(response.data['pagination']); final worksResponse = WorksResponse( works: works.map((work) => Work.fromJson(work)).toList(), pagination: pagination, ); // 存入缓存 _recommendationCache.set(itemId, page, hasSubtitle ? 1 : 0, worksResponse); return worksResponse; } throw Exception('获取相关推荐失败: ${response.statusCode}'); } on DioException catch (e) { AppLogger.error('网络请求失败', e, e.stackTrace); throw Exception('网络请求失败: ${e.message}'); } catch (e, stackTrace) { AppLogger.error('解析数据失败', e, stackTrace); throw Exception('解析数据失败: $e'); } } /// 获取作品在收藏夹中的状态 Future getWorkExistStatusInPlaylists({ required String workId, int page = 1, }) async { try { final response = await _dio.get( '/playlist/get-work-exist-status-in-my-playlists', queryParameters: { 'workID': workId, 'page': page, 'version': 2, }, ); if (response.statusCode == 200) { return PlaylistsWithExistStatu.fromJson(response.data); } throw Exception('获取收藏夹列表失败: ${response.statusCode}'); } on DioException catch (e) { AppLogger.error('网络请求失败', e, e.stackTrace); throw Exception('网络请求失败: ${e.message}'); } catch (e, stackTrace) { AppLogger.error('解析数据失败', e, stackTrace); throw Exception('解析数据失败: $e'); } } /// 添加作品到收藏夹 Future addWorkToPlaylist({ required String playlistId, required String workId, }) async { try { await _dio.post( '/playlist/add-works-to-playlist', data: { 'id': playlistId, 'works': [int.parse(workId)], }, ); } on DioException catch (e) { AppLogger.error('网络请求失败', e, e.stackTrace); throw Exception('网络请求失败: ${e.message}'); } catch (e, stackTrace) { AppLogger.error('添加到收藏夹失败', e, stackTrace); throw Exception('添加到收藏夹失败: $e'); } } /// 从收藏夹移除作品 Future removeWorkFromPlaylist({ required String playlistId, required String workId, }) async { try { await _dio.post( '/playlist/remove-works-from-playlist', data: { 'id': playlistId, 'works': [int.parse(workId)], }, ); } on DioException catch (e) { AppLogger.error('网络请求失败', e, e.stackTrace); throw Exception('网络请求失败: ${e.message}'); } catch (e, stackTrace) { AppLogger.error('从收藏夹移除失败', e, stackTrace); throw Exception('从收藏夹移除失败: $e'); } } /// 更新作品的标记状态 Future updateWorkMarkStatus(String workId, String status) async { try { final response = await _dio.put( '/review', data: { 'work_id': int.parse(workId), 'progress': status, }, ); if (response.statusCode != 200) { throw Exception('标记失败: ${response.statusCode}'); } } catch (e) { AppLogger.error('更新标记状态失败', e); rethrow; } } /// 将 MarkStatus 枚举转换为 API 参数 String convertMarkStatusToApi(MarkStatus status) { switch (status) { case MarkStatus.wantToListen: return 'marked'; case MarkStatus.listening: return 'listening'; case MarkStatus.listened: return 'listened'; case MarkStatus.relistening: return 'replay'; case MarkStatus.onHold: return 'postponed'; } } /// 获取默认标记目标收藏夹 Future getDefaultMarkTargetPlaylist() async { try { final response = await _dio.get('/playlist/get-default-mark-target-playlist'); if (response.statusCode == 200) { final playlist = Playlist.fromJson(response.data); AppLogger.info('获取默认标记目标收藏夹成功: id=${playlist.id}, name=${playlist.name}'); return playlist; } throw Exception('获取默认标记目标收藏夹失败: ${response.statusCode}'); } on DioException catch (e) { AppLogger.error('网络请求失败', e, e.stackTrace); throw Exception('网络请求失败: ${e.message}'); } catch (e, stackTrace) { AppLogger.error('解析数据失败', e, stackTrace); throw Exception('解析数据失败: $e'); } } /// 获取用户的播放列表 Future getMyPlaylists({int page = 1}) async { try { final response = await _dio.get( '/playlist/get-playlists', queryParameters: { 'page': page, }, ); if (response.statusCode == 200) { final myPlaylists = MyPlaylists.fromJson(response.data); AppLogger.info('获取播放列表成功: ${myPlaylists.playlists?.length ?? 0}个播放列表'); return myPlaylists; } throw Exception('获取播放列表失败: ${response.statusCode}'); } on DioException catch (e) { AppLogger.error('网络请求失败', e, e.stackTrace); throw Exception('网络请求失败: ${e.message}'); } catch (e, stackTrace) { AppLogger.error('解析数据失败', e, stackTrace); throw Exception('解析数据失败: $e'); } } /// 获取播放列表中的作品 Future getPlaylistWorks({ required String playlistId, int page = 1, int pageSize = 12, }) async { try { final response = await _dio.get( '/playlist/get-playlist-works', queryParameters: { 'id': playlistId, 'page': page, 'pageSize': pageSize, }, ); if (response.statusCode == 200) { final List works = response.data['works'] ?? []; final pagination = Pagination.fromJson(response.data['pagination']); return WorksResponse( works: works.map((work) => Work.fromJson(work)).toList(), pagination: pagination, ); } throw Exception('获取播放列表作品失败: ${response.statusCode}'); } on DioException catch (e) { AppLogger.error('网络请求失败', e, e.stackTrace); throw Exception('网络请求失败: ${e.message}'); } catch (e, stackTrace) { AppLogger.error('解析数据失败', e, stackTrace); throw Exception('解析数据失败: $e'); } } } ================================================ FILE: lib/data/services/auth_service.dart ================================================ import 'package:asmrapp/data/models/auth/auth_resp/auth_resp.dart'; import 'package:dio/dio.dart'; import '../../utils/logger.dart'; class AuthService { final Dio _dio; AuthService() : _dio = Dio(BaseOptions( baseUrl: 'https://api.asmr.one/api', )); Future login(String name, String password) async { try { AppLogger.info('开始登录请求: name=$name'); final response = await _dio.post('/auth/me', data: { 'name': name, 'password': password, }, ); AppLogger.info('收到登录响应: statusCode=${response.statusCode}'); AppLogger.info('响应数据: ${response.data}'); if (response.statusCode == 200) { final authResp = AuthResp.fromJson(response.data); AppLogger.info('登录成功: username=${authResp.user?.name}, group=${authResp.user?.group}'); return authResp; } throw Exception('登录失败: ${response.statusCode}'); } on DioException catch (e) { AppLogger.error('登录请求失败', e); AppLogger.error('错误详情: ${e.response?.data}'); throw Exception('网络请求失败: ${e.message}'); } catch (e) { AppLogger.error('登录失败', e); throw Exception('登录失败: $e'); } } } ================================================ FILE: lib/data/services/interceptors/auth_interceptor.dart ================================================ import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; import 'package:asmrapp/data/repositories/auth_repository.dart'; import 'package:asmrapp/utils/logger.dart'; class AuthInterceptor extends Interceptor { @override Future onRequest( RequestOptions options, RequestInterceptorHandler handler, ) async { try { final authRepository = GetIt.I(); final authData = await authRepository.getAuthData(); if (authData?.token != null) { options.headers['Authorization'] = 'Bearer ${authData!.token}'; } handler.next(options); } catch (e) { AppLogger.error('AuthInterceptor: 处理请求失败', e); handler.next(options); // 即使出错也继续请求 } } } ================================================ FILE: lib/main.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/common/constants/strings.dart'; import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; import 'core/di/service_locator.dart'; import 'package:provider/provider.dart'; import 'screens/main_screen.dart'; import 'package:asmrapp/core/theme/app_theme.dart'; import 'package:asmrapp/core/theme/theme_controller.dart'; import 'screens/search_screen.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // 初始化服务定位器 await setupServiceLocator(); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider( create: (_) => getIt(), ), ChangeNotifierProvider( create: (_) => getIt(), ), ], child: Consumer( builder: (context, themeController, child) { return MaterialApp( title: Strings.appName, theme: AppTheme.light, darkTheme: AppTheme.dark, themeMode: themeController.themeMode, home: const MainScreen(), routes: { // '/player': (context) => const PlayerScreen(), '/search': (context) { final keyword = ModalRoute.of(context)?.settings.arguments as String?; return SearchScreen(initialKeyword: keyword); }, }, ); }, ), ); } } ================================================ FILE: lib/presentation/layouts/work_layout_config.dart ================================================ import 'package:flutter/material.dart'; /// 设备类型 enum DeviceType { mobile, tablet, desktop; /// 根据屏幕宽度获取设备类型 static DeviceType fromWidth(double width) { if (width >= WorkLayoutConfig.desktopBreakpoint) return DeviceType.desktop; if (width >= WorkLayoutConfig.tabletBreakpoint) return DeviceType.tablet; return DeviceType.mobile; } } /// 作品布局配置 class WorkLayoutConfig { // 断点 static const double desktopBreakpoint = 1200; static const double tabletBreakpoint = 800; // 列数 static const int desktopColumns = 4; static const int tabletColumns = 3; static const int mobileColumns = 2; // 间距 static const double desktopSpacing = 16; static const double tabletSpacing = 12; static const double mobileSpacing = 8; // 内边距 static const EdgeInsets desktopPadding = EdgeInsets.all(16); static const EdgeInsets tabletPadding = EdgeInsets.all(12); static const EdgeInsets mobilePadding = EdgeInsets.all(8); const WorkLayoutConfig._(); /// 根据设备类型获取列数 static int getColumnsCount(DeviceType deviceType) { switch (deviceType) { case DeviceType.desktop: return desktopColumns; case DeviceType.tablet: return tabletColumns; case DeviceType.mobile: return mobileColumns; } } /// 根据设备类型获取间距 static double getSpacing(DeviceType deviceType) { switch (deviceType) { case DeviceType.desktop: return desktopSpacing; case DeviceType.tablet: return tabletSpacing; case DeviceType.mobile: return mobileSpacing; } } /// 根据设备类型获取内边距 static EdgeInsets getPadding(DeviceType deviceType) { switch (deviceType) { case DeviceType.desktop: return desktopPadding; case DeviceType.tablet: return tabletPadding; case DeviceType.mobile: return mobilePadding; } } } ================================================ FILE: lib/presentation/layouts/work_layout_strategy.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/presentation/layouts/work_layout_config.dart'; /// 作品布局策略 class WorkLayoutStrategy { const WorkLayoutStrategy(); /// 获取设备类型 DeviceType _getDeviceType(BuildContext context) { return DeviceType.fromWidth(MediaQuery.of(context).size.width); } /// 获取每行的列数 int getColumnsCount(BuildContext context) { return WorkLayoutConfig.getColumnsCount(_getDeviceType(context)); } /// 获取行间距 double getRowSpacing(BuildContext context) { return WorkLayoutConfig.getSpacing(_getDeviceType(context)); } /// 获取列间距 double getColumnSpacing(BuildContext context) { return WorkLayoutConfig.getSpacing(_getDeviceType(context)); } /// 获取内边距 EdgeInsets getPadding(BuildContext context) { return WorkLayoutConfig.getPadding(_getDeviceType(context)); } /// 将作品列表分组为行 List> groupWorksIntoRows(List works, int columnsCount) { final List> rows = []; for (var i = 0; i < works.length; i += columnsCount) { final end = i + columnsCount; rows.add(works.sublist(i, end > works.length ? works.length : end)); } return rows; } } ================================================ FILE: lib/presentation/models/filter_state.dart ================================================ class FilterState { final String orderField; final bool isDescending; const FilterState({ this.orderField = 'create_date', this.isDescending = true, }); bool get showSortDirection => orderField != 'random'; String get sortValue => orderField == 'random' ? 'desc' : (isDescending ? 'desc' : 'asc'); FilterState copyWith({ String? orderField, bool? isDescending, }) { return FilterState( orderField: orderField ?? this.orderField, isDescending: isDescending ?? this.isDescending, ); } // 用于持久化 Map toJson() => { 'orderField': orderField, 'isDescending': isDescending, }; // 从持久化恢复 factory FilterState.fromJson(Map json) => FilterState( orderField: json['orderField'] ?? 'create_date', isDescending: json['isDescending'] ?? true, ); } ================================================ FILE: lib/presentation/viewmodels/auth_viewmodel.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:asmrapp/data/models/auth/auth_resp/auth_resp.dart'; import 'package:asmrapp/data/services/auth_service.dart'; import 'package:asmrapp/data/repositories/auth_repository.dart'; import 'package:asmrapp/utils/logger.dart'; class AuthViewModel extends ChangeNotifier { final AuthService _authService; final AuthRepository _authRepository; AuthResp? _authData; bool _isLoading = false; String? _error; AuthViewModel({ required AuthService authService, required AuthRepository authRepository, }) : _authService = authService, _authRepository = authRepository { _loadSavedAuth(); } Future _loadSavedAuth() async { _authData = await _authRepository.getAuthData(); if (_authData != null) { AppLogger.info('加载保存的认证数据: ${_authData?.user?.name}'); } notifyListeners(); } Future login(String name, String password) async { if (_isLoading) return; _isLoading = true; _error = null; notifyListeners(); try { AppLogger.info('AuthViewModel: 开始登录流程'); _authData = await _authService.login(name, password); // 保存认证数据 await _authRepository.saveAuthData(_authData!); AppLogger.info(''' 登录成功,完整数据: - token: ${_authData?.token} - loggedIn: ${_authData?.user?.loggedIn} - name: ${_authData?.user?.name} - group: ${_authData?.user?.group} - email: ${_authData?.user?.email} - recommenderUuid: ${_authData?.user?.recommenderUuid} '''); } catch (e) { AppLogger.error('AuthViewModel: 登录失败', e); _error = e.toString(); _authData = null; } finally { _isLoading = false; notifyListeners(); } } Future logout() async { AppLogger.info('AuthViewModel: 执行登出'); AppLogger.info(''' 登出用户信息: - name: ${_authData?.user?.name} - group: ${_authData?.user?.group} - token: ${_authData?.token} '''); await _authRepository.clearAuthData(); _authData = null; notifyListeners(); } bool get isLoggedIn => _authData?.user != null; bool get isLoading => _isLoading; String? get error => _error; String? get username => _authData?.user?.name; String? get token => _authData?.token; String? get group => _authData?.user?.group; bool? get isUserLoggedIn => _authData?.user?.loggedIn; String? get recommenderUuid => _authData?.user?.recommenderUuid; Future loadSavedAuth() async { _authData = await _authRepository.getAuthData(); if (_authData != null) { AppLogger.info('加载保存的认证数据: ${_authData?.user?.name}'); } notifyListeners(); } } ================================================ FILE: lib/presentation/viewmodels/base/paginated_works_viewmodel.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/data/models/works/pagination.dart'; import 'package:asmrapp/data/services/api_service.dart'; import 'package:asmrapp/utils/logger.dart'; abstract class PaginatedWorksViewModel extends ChangeNotifier { final ApiService _apiService; List _works = []; bool _isLoading = false; String? _error; Pagination? _pagination; int _currentPage = 1; PaginatedWorksViewModel(this._apiService) { _init(); } // 修改为异步初始化 Future _init() async { await onInit(); // 添加初始化钩子 loadPage(1); } // 添加初始化钩子,供子类重写 Future onInit() async {} // Getters List get works => _works; bool get isLoading => _isLoading; String? get error => _error; int get currentPage => _currentPage; int? get totalPages => _pagination?.totalCount != null && _pagination?.pageSize != null ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() : null; // 获取页面名称,用于日志 String get pageName; // 子类必须实现的方法 Future fetchPage(int page); // 获取 ApiService 实例,供子类使用 ApiService get apiService => _apiService; // 通用的加载逻辑 Future loadPage(int page) async { if (_isLoading) return; if (page < 1 || (totalPages != null && page > totalPages!)) return; _isLoading = true; _error = null; notifyListeners(); try { AppLogger.info('加载$pageName: 第$page页'); final response = await fetchPage(page); _works = response.works; _pagination = response.pagination; _currentPage = page; AppLogger.info('第$page页$pageName加载成功: ${response.works.length}个作品'); } catch (e) { AppLogger.error('加载$pageName失败', e); _error = e.toString(); } finally { _isLoading = false; notifyListeners(); } } // 刷新方法 Future refresh() async { AppLogger.info('刷新$pageName'); await loadPage(1); } @override void dispose() { AppLogger.info('销毁$pageName ViewModel'); super.dispose(); } // 添加 pagination getter Pagination? get pagination => _pagination; } ================================================ FILE: lib/presentation/viewmodels/detail_viewmodel.dart ================================================ import 'package:asmrapp/data/models/playlists_with_exist_statu/pagination.dart'; import 'package:asmrapp/data/models/playlists_with_exist_statu/playlist.dart'; import 'package:get_it/get_it.dart'; import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/files/files.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/data/services/api_service.dart'; import 'package:asmrapp/core/audio/i_audio_player_service.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:asmrapp/core/audio/models/playback_context.dart'; import 'package:asmrapp/widgets/detail/playlist_selection_dialog.dart'; import 'package:asmrapp/data/models/mark_status.dart'; import 'package:asmrapp/widgets/detail/mark_selection_dialog.dart'; import 'package:dio/dio.dart'; class DetailViewModel extends ChangeNotifier { late final ApiService _apiService; late final IAudioPlayerService _audioService; final Work work; Files? _files; bool _isLoading = false; String? _error; bool _disposed = false; bool _hasRecommendations = false; bool _checkingRecommendations = false; // 收藏夹相关状态 bool _loadingPlaylists = false; String? _playlistsError; List? _playlists; Pagination? _playlistsPagination; bool _loadingFavorite = false; bool get loadingFavorite => _loadingFavorite; MarkStatus? _currentMarkStatus; MarkStatus? get currentMarkStatus => _currentMarkStatus; bool _loadingMark = false; bool get loadingMark => _loadingMark; // 添加取消标记 final _cancelToken = CancelToken(); DetailViewModel({ required this.work, }) { _audioService = GetIt.I(); _apiService = GetIt.I(); _checkRecommendations(); } Files? get files => _files; bool get isLoading => _isLoading; String? get error => _error; bool get hasRecommendations => _hasRecommendations; bool get checkingRecommendations => _checkingRecommendations; // 收藏夹相关 getters bool get loadingPlaylists => _loadingPlaylists; String? get playlistsError => _playlistsError; List? get playlists => _playlists; int? get playlistsTotalPages => _playlistsPagination?.totalCount != null && _playlistsPagination?.pageSize != null ? (_playlistsPagination!.totalCount! / _playlistsPagination!.pageSize!).ceil() : null; Future _checkRecommendations() async { _checkingRecommendations = true; notifyListeners(); try { final response = await _apiService.getItemNeighbors( itemId: work.id.toString(), page: 1, ); _hasRecommendations = (response.pagination.totalCount ?? 0) > 0; } catch (e) { AppLogger.error('检查相关推荐失败', e); _hasRecommendations = false; } finally { if (!_disposed) { _checkingRecommendations = false; notifyListeners(); } } } Future loadFiles() async { if (_isLoading) return; _isLoading = true; _error = null; notifyListeners(); try { AppLogger.info('开始加载作品文件: ${work.id}'); _files = await _apiService.getWorkFiles( work.id.toString(), cancelToken: _cancelToken, ); AppLogger.info('文件加载成功: ${work.id}'); } catch (e) { if (e is! DioException || e.type != DioExceptionType.cancel) { AppLogger.info('加载文件失败,用户取消请求'); _error = e.toString(); } } finally { if (!_disposed) { _isLoading = false; notifyListeners(); } } } Future playFile(Child file, BuildContext context) async { if (file.type?.toLowerCase() != 'audio') { throw Exception('不支持的文件类型: ${file.type}'); } if (file.mediaDownloadUrl == null) { throw Exception('无法播放:文件URL不存在'); } if (_files == null) { throw Exception('文件列表未加载'); } try { final playbackContext = PlaybackContext( work: work, files: _files!, currentFile: file, ); await _audioService.playWithContext(playbackContext); } catch (e) { if (!_disposed) { AppLogger.error('播放失败', e); } rethrow; } } /// 加载收藏夹列表 Future loadPlaylists({int page = 1}) async { if (_loadingPlaylists) return; _loadingPlaylists = true; _playlistsError = null; notifyListeners(); try { final response = await _apiService.getWorkExistStatusInPlaylists( workId: work.id.toString(), page: page, ); _playlists = response.playlists; _playlistsPagination = response.pagination; AppLogger.info('收藏夹列表加载成功: ${_playlists?.length ?? 0}个收藏夹'); } catch (e) { AppLogger.error('加载收藏夹列表失败', e); _playlistsError = e.toString(); } finally { _loadingPlaylists = false; notifyListeners(); } } Future showPlaylistsDialog(BuildContext context) async { _loadingFavorite = true; notifyListeners(); try { await loadPlaylists(); _loadingFavorite = false; notifyListeners(); if (!context.mounted) return; await showDialog( context: context, builder: (context) => PlaylistSelectionDialog( playlists: playlists, isLoading: loadingPlaylists, error: playlistsError, onRetry: () => loadPlaylists(), onPlaylistTap: (playlist) async { try { await togglePlaylistWork(playlist); } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('操作失败: $e')), ); } } }, ), ); } catch (e) { _loadingFavorite = false; notifyListeners(); rethrow; } } Future togglePlaylistWork(Playlist playlist) async { try { if (playlist.exist ?? false) { await _apiService.removeWorkFromPlaylist( playlistId: playlist.id!, workId: work.id.toString(), ); } else { await _apiService.addWorkToPlaylist( playlistId: playlist.id!, workId: work.id.toString(), ); } // 更新本地收藏夹状态 final index = _playlists?.indexWhere((p) => p.id == playlist.id); if (index != null && index != -1) { _playlists = List.from(_playlists!) ..[index] = playlist.copyWith(exist: !(playlist.exist ?? false)); notifyListeners(); } final action = (playlist.exist ?? false) ? '移除' : '添加'; AppLogger.info('$action收藏成功: ${playlist.name}'); } catch (e) { AppLogger.error('切换收藏状态失败', e); rethrow; } } Future updateMarkStatus(MarkStatus status) async { _loadingMark = true; notifyListeners(); try { await _apiService.updateWorkMarkStatus( work.id.toString(), _apiService.convertMarkStatusToApi(status), ); _currentMarkStatus = status; AppLogger.info('更新标记状态成功: ${status.label}'); } catch (e) { AppLogger.error('更新标记状态失败', e); rethrow; } finally { _loadingMark = false; notifyListeners(); } } void showMarkDialog(BuildContext context) { showDialog( context: context, builder: (dialogContext) => MarkSelectionDialog( currentStatus: _currentMarkStatus, loading: _loadingMark, onMarkSelected: (status) async { try { await updateMarkStatus(status); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('已标记为${status.label}'), duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, ), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('标记失败: $e')), ); } } }, ), ); } @override void dispose() { // 取消所有正在进行的请求 _cancelToken.cancel('ViewModel disposed'); _disposed = true; super.dispose(); } } ================================================ FILE: lib/presentation/viewmodels/favorites_viewmodel.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/data/models/works/pagination.dart'; import 'package:asmrapp/data/services/api_service.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:get_it/get_it.dart'; class FavoritesViewModel extends ChangeNotifier { final ApiService _apiService; List _works = []; bool _isLoading = false; String? _error; Pagination? _pagination; int _currentPage = 1; FavoritesViewModel() : _apiService = GetIt.I(); List get works => _works; bool get isLoading => _isLoading; String? get error => _error; int get currentPage => _currentPage; int? get totalPages => _pagination?.totalCount != null && _pagination?.pageSize != null ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() : null; /// 加载指定页面的数据 Future loadPage(int page) async { if (_isLoading) return; if (page < 1 || (totalPages != null && page > totalPages!)) return; _isLoading = true; _error = null; notifyListeners(); try { final response = await _apiService.getFavorites(page: page); _works = response.works; _pagination = response.pagination; _currentPage = page; AppLogger.info('第$page页收藏列表加载成功: ${response.works.length}个作品'); } catch (e) { AppLogger.error('加载收藏列表失败', e); _error = e.toString(); } finally { _isLoading = false; notifyListeners(); } } /// 加载收藏列表(用于初始加载和刷新) Future loadFavorites({bool refresh = false}) async { await loadPage(1); } } ================================================ FILE: lib/presentation/viewmodels/home_viewmodel.dart ================================================ import 'dart:convert'; import 'package:asmrapp/presentation/viewmodels/base/paginated_works_viewmodel.dart'; import 'package:asmrapp/data/services/api_service.dart'; import 'package:get_it/get_it.dart'; import 'package:asmrapp/presentation/models/filter_state.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:shared_preferences/shared_preferences.dart'; class HomeViewModel extends PaginatedWorksViewModel { static const String _filterStateKey = 'home_filter_state'; static const String _subtitleFilterKey = 'subtitle_filter'; bool _filterPanelExpanded = false; bool _hasSubtitle = false; FilterState _filterState = const FilterState(); bool get filterPanelExpanded => _filterPanelExpanded; bool get hasSubtitle => _hasSubtitle; FilterState get filterState => _filterState; HomeViewModel() : super(GetIt.I()); @override Future onInit() async { await _loadFilterState(); await _loadSubtitleFilter(); } Future _loadFilterState() async { try { final prefs = await SharedPreferences.getInstance(); final jsonStr = prefs.getString(_filterStateKey); if (jsonStr != null) { _filterState = FilterState.fromJson(jsonDecode(jsonStr)); notifyListeners(); } } catch (e) { AppLogger.error('加载筛选状态失败', e); } } Future _loadSubtitleFilter() async { try { final prefs = await SharedPreferences.getInstance(); _hasSubtitle = prefs.getBool(_subtitleFilterKey) ?? false; notifyListeners(); } catch (e) { AppLogger.error('加载字幕筛选状态失败', e); } } Future _saveSubtitleFilter() async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_subtitleFilterKey, _hasSubtitle); } catch (e) { AppLogger.error('保存字幕筛选状态失败', e); } } Future _saveFilterState() async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_filterStateKey, jsonEncode(_filterState.toJson())); } catch (e) { AppLogger.error('保存筛选状态失败', e); } } void toggleFilterPanel() { _filterPanelExpanded = !_filterPanelExpanded; notifyListeners(); } void updateSubtitle(bool value) { _hasSubtitle = value; _saveSubtitleFilter(); notifyListeners(); refresh(); } void updateOrderField(String value) { // 如果切换到随机排序,强制设置为降序 final newState = _filterState.copyWith( orderField: value, isDescending: value == 'random' ? true : _filterState.isDescending, ); _filterState = newState; _saveFilterState(); notifyListeners(); refresh(); } void updateSortDirection(bool isDescending) { if (_filterState.orderField == 'random') return; _filterState = _filterState.copyWith(isDescending: isDescending); _saveFilterState(); notifyListeners(); refresh(); } void closeFilterPanel() { if (_filterPanelExpanded) { _filterPanelExpanded = false; notifyListeners(); } } @override String get pageName => '主页'; @override Future fetchPage(int page) { return apiService.getWorks( page: page, hasSubtitle: _hasSubtitle, order: _filterState.orderField, sort: _filterState.sortValue, ); } @override void dispose() { _saveFilterState(); super.dispose(); } } ================================================ FILE: lib/presentation/viewmodels/player_viewmodel.dart ================================================ import 'package:asmrapp/core/audio/events/playback_event.dart'; import 'package:asmrapp/core/audio/models/audio_track_info.dart'; import 'package:asmrapp/core/audio/models/playback_context.dart'; import 'package:asmrapp/core/subtitle/i_subtitle_service.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:flutter/foundation.dart'; import 'package:asmrapp/core/audio/i_audio_player_service.dart'; import 'package:asmrapp/core/audio/models/subtitle.dart'; import 'dart:async'; import 'package:asmrapp/core/subtitle/subtitle_loader.dart'; import 'package:asmrapp/core/audio/events/playback_event_hub.dart'; class PlayerViewModel extends ChangeNotifier { final IAudioPlayerService _audioService; final PlaybackEventHub _eventHub; final ISubtitleService _subtitleService; final _subtitleLoader = SubtitleLoader(); bool _isPlaying = false; Duration? _position; Duration? _duration; Subtitle? _currentSubtitle; final List _subscriptions = []; static const _tag = 'PlayerViewModel'; PlayerViewModel({ required IAudioPlayerService audioService, required PlaybackEventHub eventHub, required ISubtitleService subtitleService, }) : _audioService = audioService, _eventHub = eventHub, _subtitleService = subtitleService { _initStreams(); _requestInitialState(); } void _initStreams() { // 播放状态事件 _subscriptions.add( _eventHub.playbackState.listen( (event) { _isPlaying = event.state.playing; _position = event.position; _duration = event.duration; notifyListeners(); }, onError: (error) => debugPrint('$_tag - 播放状态流错误: $error'), ), ); // 音轨变更事件 _subscriptions.add( _eventHub.trackChange.listen( (event) { notifyListeners(); }, onError: (error) => debugPrint('$_tag - 音轨变更流错误: $error'), ), ); // 播放进度事件 _subscriptions.add( _eventHub.playbackProgress.listen( (event) { _position = event.position; if (_position != null) { _subtitleService.updatePosition(_position!); } notifyListeners(); }, onError: (error) => debugPrint('$_tag - 播放进度流错误: $error'), ), ); // 上下文变更事件 _subscriptions.add( _eventHub.contextChange.listen( (event) async { await _loadSubtitleIfAvailable(event.context); // 如果有保存的位置,在字幕加载完成后更新位置 if (_position != null) { _subtitleService.updatePosition(_position!); } }, onError: (error) => debugPrint('$_tag - 上下文流错误: $error'), ), ); // 使用新添加的 initialState 流 _subscriptions.add( _eventHub.initialState.listen( (event) { if (event.track != null) { notifyListeners(); } if (event.context != null) { _loadSubtitleIfAvailable(event.context!); } }, onError: (error) => debugPrint('$_tag - 初始状态流错误: $error'), ), ); _initSubtitleStreams(); } void _initSubtitleStreams() { _subscriptions.add( _subtitleService.subtitleStream.listen( (subtitleList) { debugPrint('$_tag - 字幕列表更新: ${subtitleList != null ? '已加载' : '未加载'}'); }, onError: (error) => debugPrint('$_tag - 字幕流错误: $error'), ), ); _subscriptions.add( _subtitleService.currentSubtitleStream.listen( (subtitle) { _currentSubtitle = subtitle; notifyListeners(); }, onError: (error) => debugPrint('$_tag - 当前字幕流错误: $error'), ), ); } bool get isPlaying => _isPlaying; Duration? get position => _position; Duration? get duration => _duration; Subtitle? get currentSubtitle => _currentSubtitle; Future playPause() async { if (_isPlaying) { _audioService.pause(); } else { _audioService.resume(); } } Future seek(Duration position) async { await _audioService.seek(position); } Future previous() async { await _audioService.previous(); } Future next() async { await _audioService.next(); } Future stop() async { await _audioService.stop(); _position = Duration.zero; notifyListeners(); } @override void dispose() { for (var subscription in _subscriptions) { subscription.cancel(); } _subscriptions.clear(); super.dispose(); } // 请求初始状态 void _requestInitialState() { _eventHub.emit(RequestInitialStateEvent()); } // 修改字幕加载方法,返回 Future 以便等待加载完成 Future _loadSubtitleIfAvailable(PlaybackContext context) async { final subtitleFile = _subtitleLoader.findSubtitleFile( context.currentFile, context.files ); if (subtitleFile?.mediaDownloadUrl != null) { await _subtitleService.loadSubtitle(subtitleFile!.mediaDownloadUrl!); } else { _subtitleService.clearSubtitle(); AppLogger.debug('未找到字幕文件,清除现有字幕'); } } AudioTrackInfo? get currentTrackInfo => _audioService.currentTrack; PlaybackContext? get currentContext => _audioService.currentContext; Future seekToNextLyric() async { final currentSubtitle = _subtitleService.currentSubtitleWithState; final subtitleList = _subtitleService.subtitleList; if (currentSubtitle != null && subtitleList != null) { final nextSubtitle = currentSubtitle.subtitle.getNext(subtitleList); if (nextSubtitle != null) { await seek(nextSubtitle.start); } } } Future seekToPreviousLyric() async { final currentSubtitle = _subtitleService.currentSubtitleWithState; final subtitleList = _subtitleService.subtitleList; if (currentSubtitle != null && subtitleList != null) { final previousSubtitle = currentSubtitle.subtitle.getPrevious(subtitleList); if (previousSubtitle != null) { await seek(previousSubtitle.start); } } } } ================================================ FILE: lib/presentation/viewmodels/playlist_works_viewmodel.dart ================================================ import 'package:asmrapp/data/models/my_lists/my_playlists/playlist.dart'; import 'package:flutter/foundation.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/data/models/works/pagination.dart'; import 'package:asmrapp/data/services/api_service.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:get_it/get_it.dart'; class PlaylistWorksViewModel extends ChangeNotifier { final ApiService _apiService = GetIt.I(); final Playlist playlist; List _works = []; bool _isLoading = false; String? _error; Pagination? _pagination; int _currentPage = 1; PlaylistWorksViewModel(this.playlist); List get works => _works; bool get isLoading => _isLoading; String? get error => _error; int get currentPage => _currentPage; int? get totalPages => _pagination?.totalCount != null && _pagination?.pageSize != null ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() : null; Future loadWorks({int page = 1}) async { if (_isLoading) return; if (page < 1 || (totalPages != null && page > totalPages!)) return; _isLoading = true; _error = null; notifyListeners(); try { final response = await _apiService.getPlaylistWorks( playlistId: playlist.id!, page: page, ); _works = response.works; _pagination = response.pagination; _currentPage = page; AppLogger.info('第$page页播放列表作品加载成功: ${response.works.length}个作品'); } catch (e) { AppLogger.error('加载播放列表作品失败', e); _error = e.toString(); } finally { _isLoading = false; notifyListeners(); } } Future refresh() => loadWorks(page: 1); } ================================================ FILE: lib/presentation/viewmodels/playlists_viewmodel.dart ================================================ import 'package:asmrapp/data/models/works/work.dart'; import 'package:flutter/foundation.dart'; import 'package:asmrapp/data/models/my_lists/my_playlists/my_playlists.dart'; import 'package:asmrapp/data/models/my_lists/my_playlists/playlist.dart'; import 'package:asmrapp/data/models/my_lists/my_playlists/pagination.dart'; import 'package:asmrapp/data/services/api_service.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:get_it/get_it.dart'; class PlaylistsViewModel extends ChangeNotifier { final ApiService _apiService = GetIt.I(); List? _playlists; bool _isLoading = false; String? _error; Pagination? _pagination; int _currentPage = 1; // 添加视图切换相关状态 Playlist? _selectedPlaylist; List _playlistWorks = []; bool _loadingWorks = false; String? _worksError; Pagination? _worksPagination; int _worksCurrentPage = 1; // Getters List get playlists => _playlists ?? []; bool get isLoading => _isLoading; String? get error => _error; int get currentPage => _currentPage; int? get totalPages => _pagination?.totalCount != null && _pagination?.pageSize != null ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() : null; Playlist? get selectedPlaylist => _selectedPlaylist; List get playlistWorks => _playlistWorks; bool get loadingWorks => _loadingWorks; String? get worksError => _worksError; int get worksCurrentPage => _worksCurrentPage; int? get worksTotalPages => _worksPagination?.totalCount != null && _worksPagination?.pageSize != null ? (_worksPagination!.totalCount! / _worksPagination!.pageSize!).ceil() : null; PlaylistsViewModel() { loadPlaylists(); } /// 加载播放列表 Future loadPlaylists({int page = 1}) async { if (_isLoading) return; if (page < 1 || (totalPages != null && page > totalPages!)) return; _isLoading = true; _error = null; notifyListeners(); try { final response = await _apiService.getMyPlaylists(page: page); _playlists = response.playlists; _pagination = response.pagination; _currentPage = page; AppLogger.info('第$page页播放列表加载成功: ${_playlists?.length ?? 0}个播放列表'); } catch (e) { AppLogger.error('加载播放列表失败', e); _error = e.toString(); } finally { _isLoading = false; notifyListeners(); } } /// 刷新播放列表 Future refresh() async { await loadPlaylists(page: 1); } /// 选择播放列表并加载作品 Future selectPlaylist(Playlist playlist) async { _selectedPlaylist = playlist; _playlistWorks = []; _worksError = null; _worksPagination = null; _worksCurrentPage = 1; notifyListeners(); await loadPlaylistWorks(); } /// 清除选中的播放列表 void clearSelectedPlaylist() { _selectedPlaylist = null; _playlistWorks = []; _worksError = null; _worksPagination = null; _worksCurrentPage = 1; notifyListeners(); } /// 加载播放列表作品 Future loadPlaylistWorks({int page = 1}) async { if (_loadingWorks || _selectedPlaylist == null) return; if (page < 1 || (worksTotalPages != null && page > worksTotalPages!)) return; _loadingWorks = true; _worksError = null; notifyListeners(); try { final response = await _apiService.getPlaylistWorks( playlistId: _selectedPlaylist!.id!, page: page, ); _playlistWorks = response.works; _worksPagination = response.pagination as Pagination?; _worksCurrentPage = page; AppLogger.info('第$page页播放列表作品加载成功: ${response.works.length}个作品'); } catch (e) { AppLogger.error('加载播放列表作品失败', e); _worksError = e.toString(); } finally { _loadingWorks = false; notifyListeners(); } } /// 刷新播放列表作品 Future refreshWorks() => loadPlaylistWorks(page: 1); /// 获取播放列表显示名称 String getDisplayName(String? name) { switch (name) { case '__SYS_PLAYLIST_MARKED': return '我标记的'; case '__SYS_PLAYLIST_LIKED': return '我喜欢的'; default: return name ?? ''; } } @override void dispose() { AppLogger.info('销毁 PlaylistsViewModel'); super.dispose(); } } ================================================ FILE: lib/presentation/viewmodels/popular_viewmodel.dart ================================================ import 'package:asmrapp/presentation/viewmodels/base/paginated_works_viewmodel.dart'; import 'package:asmrapp/data/services/api_service.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; class PopularViewModel extends PaginatedWorksViewModel { static const _subtitleFilterKey = 'subtitle_filter'; bool _hasSubtitle = false; bool _filterPanelExpanded = false; PopularViewModel() : super(GetIt.I()) { _loadFilterState(); } @override Future onInit() async { await _loadSubtitleFilter(); // 使用 onInit 钩子加载状态 } Future _loadSubtitleFilter() async { try { final prefs = await SharedPreferences.getInstance(); _hasSubtitle = prefs.getBool(_subtitleFilterKey) ?? false; notifyListeners(); } catch (e) { AppLogger.error('加载字幕筛选状态失败', e); } } Future _loadFilterState() async { try { final prefs = await SharedPreferences.getInstance(); _hasSubtitle = prefs.getBool(_subtitleFilterKey) ?? false; notifyListeners(); } catch (e) { AppLogger.error('加载筛选状态失败', e); } } Future _saveFilterState() async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_subtitleFilterKey, _hasSubtitle); } catch (e) { AppLogger.error('保存筛选状态失败', e); } } bool get hasSubtitle => _hasSubtitle; bool get filterPanelExpanded => _filterPanelExpanded; void toggleSubtitleFilter() { _hasSubtitle = !_hasSubtitle; _saveFilterState(); notifyListeners(); refresh(); // 刷新列表 } void toggleFilterPanel() { _filterPanelExpanded = !_filterPanelExpanded; notifyListeners(); } void closeFilterPanel() { if (_filterPanelExpanded) { _filterPanelExpanded = false; notifyListeners(); } } @override String get pageName => '热门列表'; @override Future fetchPage(int page) { return apiService.getPopular( page: page, hasSubtitle: _hasSubtitle, ); } // 保持原有的便捷方法 Future loadPopular({bool refresh = false}) => refresh ? this.refresh() : loadPage(1); @override void dispose() { _saveFilterState(); super.dispose(); } } ================================================ FILE: lib/presentation/viewmodels/recommend_viewmodel.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/data/models/works/pagination.dart'; import 'package:asmrapp/data/services/api_service.dart'; import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; class RecommendViewModel extends ChangeNotifier { static const _subtitleFilterKey = 'subtitle_filter'; // 与 PopularViewModel 使用相同的 key 实现全局共享 final ApiService _apiService; final AuthViewModel _authViewModel; List _works = []; bool _isLoading = false; String? _error; Pagination? _pagination; int _currentPage = 1; bool _hasSubtitle = false; bool _filterPanelExpanded = false; RecommendViewModel(this._authViewModel) : _apiService = GetIt.I() { _loadFilterState(); } // 加载筛选状态 Future _loadFilterState() async { try { final prefs = await SharedPreferences.getInstance(); _hasSubtitle = prefs.getBool(_subtitleFilterKey) ?? false; notifyListeners(); // 首次加载时应用筛选状态 loadRecommendations(refresh: true); } catch (e) { AppLogger.error('加载筛选状态失败', e); } } // 保存筛选状态 Future _saveFilterState() async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_subtitleFilterKey, _hasSubtitle); } catch (e) { AppLogger.error('保存筛选状态失败', e); } } // Getters List get works => _works; bool get isLoading => _isLoading; String? get error => _error; int get currentPage => _currentPage; int? get totalPages => _pagination?.totalCount != null && _pagination?.pageSize != null ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() : null; bool get hasSubtitle => _hasSubtitle; bool get filterPanelExpanded => _filterPanelExpanded; Pagination? get pagination => _pagination; // 切换字幕筛选 void toggleSubtitleFilter() { _hasSubtitle = !_hasSubtitle; _saveFilterState(); // 保存状态 notifyListeners(); loadRecommendations(refresh: true); // 刷新列表 } void toggleFilterPanel() { _filterPanelExpanded = !_filterPanelExpanded; notifyListeners(); } void closeFilterPanel() { if (_filterPanelExpanded) { _filterPanelExpanded = false; notifyListeners(); } } /// 加载指定页面的数据 Future loadPage(int page) async { if (_isLoading) return; if (page < 1 || (totalPages != null && page > totalPages!)) return; // 检查是否已登录 final uuid = _authViewModel.recommenderUuid; if (uuid == null) { _error = '请先登录'; notifyListeners(); return; } _isLoading = true; _error = null; notifyListeners(); try { final response = await _apiService.getRecommendations( uuid: uuid, page: page, hasSubtitle: _hasSubtitle, // 添加字幕筛选参数 ); _works = response.works; _pagination = response.pagination; _currentPage = page; AppLogger.info('第$page页推荐列表加载成功: ${response.works.length}个作品'); } catch (e) { AppLogger.error('加载推荐列表失败', e); _error = e.toString(); } finally { _isLoading = false; notifyListeners(); } } /// 加载推荐列表(用于初始加载和刷新) Future loadRecommendations({bool refresh = false}) async { await loadPage(1); } @override void dispose() { _saveFilterState(); // 在销毁时保存状态 super.dispose(); } } ================================================ FILE: lib/presentation/viewmodels/search_viewmodel.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/data/models/works/pagination.dart'; import 'package:asmrapp/data/services/api_service.dart'; import 'package:asmrapp/utils/logger.dart'; class SearchViewModel extends ChangeNotifier { final _apiService = GetIt.I(); List _works = []; List get works => _works; String _keyword = ''; String get keyword => _keyword; bool _isLoading = false; bool get isLoading => _isLoading; String? _error; String? get error => _error; Pagination? _pagination; int get totalPages => _pagination?.totalCount != null && _pagination?.pageSize != null ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() : 1; int _currentPage = 1; int get currentPage => _currentPage; bool _hasSubtitle = false; bool get hasSubtitle => _hasSubtitle; String _order = 'create_date'; // 默认按创建时间 String get order => _order; String _sort = 'desc'; // 默认降序 String get sort => _sort; void toggleSubtitle() { _hasSubtitle = !_hasSubtitle; notifyListeners(); if (_keyword.isNotEmpty) { search(_keyword); } } void setOrder(String order, String sort) { _order = order; _sort = sort; notifyListeners(); if (_keyword.isNotEmpty) { search(_keyword); } } /// 执行搜索 Future search(String keyword, {int page = 1}) async { if (keyword.isEmpty) return; _keyword = keyword; _isLoading = true; _error = null; notifyListeners(); try { AppLogger.info('搜索关键词: $keyword, 页码: $page'); final response = await _apiService.searchWorks( keyword: keyword, page: page, order: _order, sort: _sort, hasSubtitle: _hasSubtitle, // 添加字幕过滤 ); _works = response.works; _pagination = response.pagination; _currentPage = page; AppLogger.info('搜索成功: ${response.works.length}个结果'); } catch (e) { AppLogger.error('搜索失败', e); _error = e.toString(); } finally { _isLoading = false; notifyListeners(); } } /// 加载指定页 Future loadPage(int page) async { if (_keyword.isEmpty) return; await search(_keyword, page: page); } /// 清空搜索结果 void clear() { _works = []; _keyword = ''; _error = null; _pagination = null; _currentPage = 1; notifyListeners(); } } ================================================ FILE: lib/presentation/viewmodels/settings/cache_manager_viewmodel.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:asmrapp/core/audio/cache/audio_cache_manager.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:asmrapp/core/subtitle/cache/subtitle_cache_manager.dart'; class CacheManagerViewModel extends ChangeNotifier { bool _isLoading = false; int _audioCacheSize = 0; int _subtitleCacheSize = 0; String? _error; bool get isLoading => _isLoading; int get audioCacheSize => _audioCacheSize; int get subtitleCacheSize => _subtitleCacheSize; int get totalCacheSize => _audioCacheSize + _subtitleCacheSize; String? get error => _error; // 格式化缓存大小显示 String _formatSize(int size) { if (size < 1024) return '${size}B'; if (size < 1024 * 1024) return '${(size / 1024).toStringAsFixed(2)}KB'; return '${(size / (1024 * 1024)).toStringAsFixed(2)}MB'; } String get audioCacheSizeFormatted => _formatSize(_audioCacheSize); String get subtitleCacheSizeFormatted => _formatSize(_subtitleCacheSize); String get totalCacheSizeFormatted => _formatSize(totalCacheSize); // 加载缓存大小 Future loadCacheSize() async { try { _isLoading = true; notifyListeners(); // 获取音频缓存大小 _audioCacheSize = await AudioCacheManager.getCacheSize(); // 获取字幕缓存大小 _subtitleCacheSize = await SubtitleCacheManager.getSize(); _error = null; } catch (e) { AppLogger.error('加载缓存大小失败', e); _error = '加载失败: $e'; } finally { _isLoading = false; notifyListeners(); } } // 清理音频缓存 Future clearAudioCache() async { try { _isLoading = true; notifyListeners(); await AudioCacheManager.cleanCache(); await loadCacheSize(); _error = null; } catch (e) { AppLogger.error('清理音频缓存失败', e); _error = '清理失败: $e'; } finally { _isLoading = false; notifyListeners(); } } // 清理字幕缓存 Future clearSubtitleCache() async { try { _isLoading = true; notifyListeners(); await SubtitleCacheManager.clearCache(); await loadCacheSize(); _error = null; } catch (e) { AppLogger.error('清理字幕缓存失败', e); _error = '清理失败: $e'; } finally { _isLoading = false; notifyListeners(); } } // 清理所有缓存 Future clearAllCache() async { try { _isLoading = true; notifyListeners(); await Future.wait([ AudioCacheManager.cleanCache(), SubtitleCacheManager.clearCache(), ]); await loadCacheSize(); _error = null; } catch (e) { AppLogger.error('清理缓存失败', e); _error = '清理失败: $e'; } finally { _isLoading = false; notifyListeners(); } } } ================================================ FILE: lib/presentation/viewmodels/similar_works_viewmodel.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/data/models/works/pagination.dart'; import 'package:asmrapp/data/services/api_service.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; class SimilarWorksViewModel extends ChangeNotifier { static const _subtitleFilterKey = 'subtitle_filter'; // 与其他 ViewModel 使用相同的 key final ApiService _apiService; final Work work; List _works = []; bool _isLoading = false; String? _error; Pagination? _pagination; int _currentPage = 1; bool _hasSubtitle = false; bool _filterPanelExpanded = false; SimilarWorksViewModel(this.work) : _apiService = GetIt.I() { _loadFilterState(); } // Getters List get works => _works; bool get isLoading => _isLoading; String? get error => _error; int get currentPage => _currentPage; bool get hasSubtitle => _hasSubtitle; bool get filterPanelExpanded => _filterPanelExpanded; int? get totalPages => _pagination?.totalCount != null && _pagination?.pageSize != null ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() : null; // 加载筛选状态 Future _loadFilterState() async { try { final prefs = await SharedPreferences.getInstance(); _hasSubtitle = prefs.getBool(_subtitleFilterKey) ?? false; notifyListeners(); // 首次加载时应用筛选状态 loadSimilarWorks(refresh: true); } catch (e) { AppLogger.error('加载筛选状态失败', e); } } // 保存筛选状态 Future _saveFilterState() async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_subtitleFilterKey, _hasSubtitle); } catch (e) { AppLogger.error('保存筛选状态失败', e); } } // 切换字幕筛选 void toggleSubtitleFilter() { _hasSubtitle = !_hasSubtitle; _saveFilterState(); notifyListeners(); loadSimilarWorks(refresh: true); } void toggleFilterPanel() { _filterPanelExpanded = !_filterPanelExpanded; notifyListeners(); } void closeFilterPanel() { if (_filterPanelExpanded) { _filterPanelExpanded = false; notifyListeners(); } } /// 加载指定页面的数据 Future loadPage(int page) async { if (_isLoading) return; if (page < 1 || (totalPages != null && page > totalPages!)) return; _isLoading = true; _error = null; notifyListeners(); try { final response = await _apiService.getItemNeighbors( itemId: work.id.toString(), page: page, hasSubtitle: _hasSubtitle, // 添加字幕筛选参数 ); _works = response.works; _pagination = response.pagination; _currentPage = page; AppLogger.info('第$page页相关推荐加载成功: ${response.works.length}个作品'); } catch (e) { AppLogger.error('加载相关推荐失败', e); _error = e.toString(); } finally { _isLoading = false; notifyListeners(); } } /// 加载相关推荐(用于初始加载和刷新) Future loadSimilarWorks({bool refresh = false}) async { await loadPage(1); } @override void dispose() { _saveFilterState(); super.dispose(); } } ================================================ FILE: lib/presentation/widgets/auth/login_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; import 'package:asmrapp/utils/logger.dart'; class LoginDialog extends StatefulWidget { const LoginDialog({super.key}); @override State createState() => _LoginDialogState(); } class _LoginDialogState extends State { final _nameController = TextEditingController(); final _passwordController = TextEditingController(); bool _obscurePassword = true; @override void dispose() { _nameController.dispose(); _passwordController.dispose(); super.dispose(); } Future _handleLogin() async { final name = _nameController.text.trim(); AppLogger.info('LoginDialog: 尝试登录: name=$name'); final authVM = context.read(); await authVM.login(name, _passwordController.text); if (mounted) { if (authVM.error == null) { AppLogger.info('LoginDialog: 登录成功,关闭对话框'); Navigator.of(context).pop(); } else { AppLogger.error('LoginDialog: 登录失败: ${authVM.error}'); } } } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('登录'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _nameController, decoration: const InputDecoration( labelText: '用户名', border: OutlineInputBorder(), ), textInputAction: TextInputAction.next, ), const SizedBox(height: 16), TextField( controller: _passwordController, decoration: InputDecoration( labelText: '密码', border: const OutlineInputBorder(), suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility : Icons.visibility_off, ), onPressed: () { setState(() { _obscurePassword = !_obscurePassword; }); }, ), ), obscureText: _obscurePassword, textInputAction: TextInputAction.done, onSubmitted: (_) => _handleLogin(), ), const SizedBox(height: 8), Consumer( builder: (context, authVM, _) { if (authVM.error != null) { return Text( authVM.error!, style: TextStyle( color: Theme.of(context).colorScheme.error, ), ); } return const SizedBox.shrink(); }, ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('取消'), ), Consumer( builder: (context, authVM, _) { return FilledButton( onPressed: authVM.isLoading ? null : _handleLogin, child: authVM.isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, ), ) : const Text('登录'), ); }, ), ], ); } } ================================================ FILE: lib/screens/contents/home_content.dart ================================================ import 'package:asmrapp/widgets/filter/filter_panel.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/presentation/viewmodels/home_viewmodel.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; import 'package:asmrapp/widgets/work_grid/enhanced_work_grid_view.dart'; class HomeContent extends StatefulWidget { const HomeContent({super.key}); @override State createState() => _HomeContentState(); } class _HomeContentState extends State with AutomaticKeepAliveClientMixin { final _layoutStrategy = const WorkLayoutStrategy(); final _scrollController = ScrollController(); @override bool get wantKeepAlive => true; @override void initState() { super.initState(); // 添加滚动监听 _scrollController.addListener(_onScroll); } @override void dispose() { _scrollController.removeListener(_onScroll); _scrollController.dispose(); super.dispose(); } void _onScroll() { // 当滚动开始时收起筛选面板 if (_scrollController.position.pixels != _scrollController.position.minScrollExtent) { final viewModel = context.read(); if (viewModel.filterPanelExpanded) { viewModel.closeFilterPanel(); // 需要在 ViewModel 中添加这个方法 } } } @override Widget build(BuildContext context) { super.build(context); return Consumer( builder: (context, viewModel, child) { return Stack( children: [ // 作品列表 EnhancedWorkGridView( works: viewModel.works, isLoading: viewModel.isLoading, error: viewModel.error, currentPage: viewModel.currentPage, totalPages: viewModel.totalPages, onPageChanged: (page) => viewModel.loadPage(page), onRefresh: () => viewModel.refresh(), onRetry: () => viewModel.refresh(), layoutStrategy: _layoutStrategy, scrollController: _scrollController, ), // 筛选面板 Positioned( top: 0, left: 0, right: 0, child: AnimatedSlide( duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, offset: Offset(0, viewModel.filterPanelExpanded ? 0 : -1), child: Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 8, spreadRadius: 1, offset: const Offset(0, 1), ), ], ), child: FilterPanel( hasSubtitle: viewModel.hasSubtitle, onSubtitleChanged: viewModel.updateSubtitle, orderField: viewModel.filterState.orderField, isDescending: viewModel.filterState.isDescending, onOrderFieldChanged: viewModel.updateOrderField, onSortDirectionChanged: viewModel.updateSortDirection, ), ), ), ), ], ); }, ); } } ================================================ FILE: lib/screens/contents/playlists/playlist_works_view.dart ================================================ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/data/models/my_lists/my_playlists/playlist.dart'; import 'package:asmrapp/presentation/viewmodels/playlist_works_viewmodel.dart'; import 'package:asmrapp/presentation/viewmodels/playlists_viewmodel.dart'; import 'package:asmrapp/widgets/work_grid/enhanced_work_grid_view.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; class PlaylistWorksView extends StatelessWidget { final Playlist playlist; final VoidCallback onBack; final WorkLayoutStrategy _layoutStrategy = const WorkLayoutStrategy(); const PlaylistWorksView({ super.key, required this.playlist, required this.onBack, }); @override Widget build(BuildContext context) { final playlistsViewModel = context.read(); return ChangeNotifierProvider( create: (_) => PlaylistWorksViewModel(playlist)..loadWorks(), child: Consumer( builder: (context, viewModel, child) { return Column( children: [ Material( elevation: 2, child: Container( padding: const EdgeInsets.all(8), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back), onPressed: onBack, ), Expanded( child: Text( playlistsViewModel.getDisplayName(playlist.name), style: Theme.of(context).textTheme.titleMedium, overflow: TextOverflow.ellipsis, ), ), ], ), ), ), Expanded( child: EnhancedWorkGridView( works: viewModel.works, isLoading: viewModel.isLoading, error: viewModel.error, onRetry: () => viewModel.refresh(), onRefresh: () => viewModel.refresh(), currentPage: viewModel.currentPage, totalPages: viewModel.totalPages, onPageChanged: (page) => viewModel.loadWorks(page: page), layoutStrategy: _layoutStrategy, emptyMessage: '暂无作品', ), ), ], ); }, ), ); } } ================================================ FILE: lib/screens/contents/playlists/playlists_list_view.dart ================================================ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/presentation/viewmodels/playlists_viewmodel.dart'; import 'package:asmrapp/data/models/my_lists/my_playlists/playlist.dart'; import 'package:asmrapp/widgets/pagination_controls.dart'; class PlaylistsListView extends StatelessWidget { final Function(Playlist) onPlaylistSelected; const PlaylistsListView({ super.key, required this.onPlaylistSelected, }); @override Widget build(BuildContext context) { return Consumer( builder: (context, viewModel, child) { if (viewModel.isLoading && viewModel.playlists.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (viewModel.error != null && viewModel.playlists.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(viewModel.error!), const SizedBox(height: 16), ElevatedButton( onPressed: viewModel.refresh, child: const Text('重试'), ), ], ), ); } return RefreshIndicator( onRefresh: viewModel.refresh, child: Column( children: [ Expanded( child: ListView.builder( itemCount: viewModel.playlists.length, itemBuilder: (context, index) { final playlist = viewModel.playlists[index]; return ListTile( leading: const Icon(Icons.playlist_play), title: Text(viewModel.getDisplayName(playlist.name)), subtitle: Text('${playlist.worksCount ?? 0} 个作品'), onTap: () => onPlaylistSelected(playlist), ); }, ), ), if (viewModel.playlists.isNotEmpty) PaginationControls( currentPage: viewModel.currentPage, totalPages: viewModel.totalPages ?? 1, isLoading: viewModel.isLoading, onPageChanged: (page) => viewModel.loadPlaylists(page: page), ), ], ), ); }, ); } } ================================================ FILE: lib/screens/contents/playlists_content.dart ================================================ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/data/models/my_lists/my_playlists/playlist.dart'; import 'package:asmrapp/screens/contents/playlists/playlists_list_view.dart'; import 'package:asmrapp/screens/contents/playlists/playlist_works_view.dart'; import 'package:asmrapp/presentation/viewmodels/playlists_viewmodel.dart'; class PlaylistsContent extends StatefulWidget { const PlaylistsContent({super.key}); @override State createState() => _PlaylistsContentState(); } class _PlaylistsContentState extends State with AutomaticKeepAliveClientMixin { Playlist? _selectedPlaylist; @override bool get wantKeepAlive => true; void _handlePlaylistSelected(Playlist playlist) { setState(() { _selectedPlaylist = playlist; }); } void _handleBack() { setState(() { _selectedPlaylist = null; }); } Future _onWillPop() async { if (_selectedPlaylist != null) { _handleBack(); return false; } return true; } @override Widget build(BuildContext context) { super.build(context); return PopScope( canPop: _selectedPlaylist == null, onPopInvokedWithResult: (didPop, result) { if (!didPop) { _handleBack(); } }, child: _selectedPlaylist != null ? PlaylistWorksView( playlist: _selectedPlaylist!, onBack: _handleBack, ) : PlaylistsListView( onPlaylistSelected: _handlePlaylistSelected, ), ); } } ================================================ FILE: lib/screens/contents/popular_content.dart ================================================ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/presentation/viewmodels/popular_viewmodel.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; import 'package:asmrapp/widgets/work_grid/enhanced_work_grid_view.dart'; import 'package:asmrapp/widgets/filter/filter_with_keyword.dart'; class PopularContent extends StatefulWidget { const PopularContent({super.key}); @override State createState() => _PopularContentState(); } class _PopularContentState extends State with AutomaticKeepAliveClientMixin { final _layoutStrategy = const WorkLayoutStrategy(); final _scrollController = ScrollController(); @override bool get wantKeepAlive => true; @override void initState() { super.initState(); _scrollController.addListener(_onScroll); } @override void dispose() { _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_scrollController.position.pixels != _scrollController.position.minScrollExtent) { final viewModel = context.read(); if (viewModel.filterPanelExpanded) { viewModel.closeFilterPanel(); } } } @override Widget build(BuildContext context) { super.build(context); return Consumer( builder: (context, viewModel, child) { return Stack( children: [ // 作品列表 EnhancedWorkGridView( works: viewModel.works, isLoading: viewModel.isLoading, error: viewModel.error, currentPage: viewModel.currentPage, totalPages: viewModel.totalPages, onPageChanged: (page) => viewModel.loadPage(page), onRefresh: () => viewModel.loadPopular(refresh: true), onRetry: () => viewModel.loadPopular(refresh: true), layoutStrategy: _layoutStrategy, scrollController: _scrollController, ), // 筛选面板 Positioned( top: 0, left: 0, right: 0, child: AnimatedSlide( duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, offset: Offset(0, viewModel.filterPanelExpanded ? 0 : -1), child: FilterWithKeyword( hasSubtitle: viewModel.hasSubtitle, onSubtitleChanged: (_) => viewModel.toggleSubtitleFilter(), ), ), ), ], ); }, ); } } ================================================ FILE: lib/screens/contents/recommend_content.dart ================================================ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/presentation/viewmodels/recommend_viewmodel.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; import 'package:asmrapp/widgets/work_grid/enhanced_work_grid_view.dart'; import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; import 'package:asmrapp/widgets/filter/filter_with_keyword.dart'; class RecommendContent extends StatefulWidget { const RecommendContent({super.key}); @override State createState() => _RecommendContentState(); } class _RecommendContentState extends State with AutomaticKeepAliveClientMixin { final _layoutStrategy = const WorkLayoutStrategy(); final _scrollController = ScrollController(); @override bool get wantKeepAlive => true; void _onScroll() { if (_scrollController.position.pixels != _scrollController.position.minScrollExtent) { final viewModel = context.read(); if (viewModel.filterPanelExpanded) { viewModel.closeFilterPanel(); } } } @override void initState() { super.initState(); _scrollController.addListener(_onScroll); // 初始加载 WidgetsBinding.instance.addPostFrameCallback((_) { context.read().loadRecommendations(); }); } @override void dispose() { _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { super.build(context); return Consumer( builder: (context, viewModel, child) { return Stack( children: [ // 作品列表 EnhancedWorkGridView( works: viewModel.works, isLoading: viewModel.isLoading, error: viewModel.error, currentPage: viewModel.currentPage, totalPages: viewModel.totalPages, onPageChanged: (page) => viewModel.loadPage(page), onRefresh: () => viewModel.loadRecommendations(refresh: true), onRetry: () => viewModel.loadRecommendations(refresh: true), layoutStrategy: _layoutStrategy, scrollController: _scrollController, ), // 筛选面板 Positioned( top: 0, left: 0, right: 0, child: AnimatedSlide( duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, offset: Offset(0, viewModel.filterPanelExpanded ? 0 : -1), child: FilterWithKeyword( hasSubtitle: viewModel.hasSubtitle, onSubtitleChanged: (_) => viewModel.toggleSubtitleFilter(), ), ), ), ], ); }, ); } } ================================================ FILE: lib/screens/detail_screen.dart ================================================ import 'package:asmrapp/widgets/mini_player/mini_player.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/widgets/detail/work_cover.dart'; import 'package:asmrapp/widgets/detail/work_info.dart'; import 'package:asmrapp/widgets/detail/work_files_list.dart'; import 'package:asmrapp/widgets/detail/work_files_skeleton.dart'; import 'package:asmrapp/presentation/viewmodels/detail_viewmodel.dart'; import 'package:asmrapp/widgets/detail/work_action_buttons.dart'; import 'package:asmrapp/screens/similar_works_screen.dart'; class DetailScreen extends StatelessWidget { final Work work; final bool fromPlayer; const DetailScreen({ super.key, required this.work, this.fromPlayer = false, }); @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => DetailViewModel( work: work, )..loadFiles(), child: Scaffold( appBar: AppBar( title: Text(work.sourceId ?? ''), ), body: SingleChildScrollView( padding: const EdgeInsets.only(bottom: MiniPlayer.height), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ WorkCover( imageUrl: work.mainCoverUrl ?? '', workId: work.id ?? 0, sourceId: work.sourceId ?? '', releaseDate: work.release, heroTag: 'work-cover-${work.id}', ), WorkInfo(work: work), Consumer( builder: (context, viewModel, _) => WorkActionButtons( hasRecommendations: viewModel.hasRecommendations, checkingRecommendations: viewModel.checkingRecommendations, onRecommendationsTap: () { Navigator.of(context).push( PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => SimilarWorksScreen(work: work), transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(1.0, 0.0); const end = Offset.zero; const curve = Curves.easeInOut; var tween = Tween(begin: begin, end: end).chain( CurveTween(curve: curve), ); return SlideTransition( position: animation.drive(tween), child: child, ); }, ), ); }, onFavoriteTap: () => viewModel.showPlaylistsDialog(context), loadingFavorite: viewModel.loadingFavorite, onMarkTap: () => viewModel.showMarkDialog(context), currentMarkStatus: viewModel.currentMarkStatus, loadingMark: viewModel.loadingMark, ), ), Consumer( builder: (context, viewModel, _) { if (viewModel.isLoading) { return const WorkFilesSkeleton(); } if (viewModel.error != null) { return Center( child: Text( viewModel.error!, style: TextStyle( color: Theme.of(context).colorScheme.error), ), ); } if (viewModel.files != null) { return WorkFilesList( files: viewModel.files!, onFileTap: (file) async { try { await viewModel.playFile(file, context); } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('播放失败: $e')), ); } } }, ); } return const SizedBox.shrink(); }, ), ], ), ), bottomSheet: const MiniPlayer(), ), ); } } ================================================ FILE: lib/screens/docs/main_screen.md ================================================ # 应用架构说明 ## MainScreen 架构 ### 概述 MainScreen 采用集中式的状态管理架构,作为应用的主要页面容器,它负责: 1. 管理所有主要页面的 ViewModel 2. 提供统一的状态管理入口 3. 确保 ViewModel 的单一实例 ### 核心原则 1. **ViewModel 单一实例** - 所有页面的 ViewModel 都在 MainScreen 中初始化 - 子页面通过 Provider 获取 ViewModel,不创建自己的实例 - 确保状态的一致性和可预测性 2. **状态提供机制** - 使用 MultiProvider 在顶层提供所有 ViewModel - 子页面使用 context.read 或 Provider.of 获取 ViewModel - 避免重复创建 ViewModel 实例 3. **生命周期管理** - MainScreen 负责 ViewModel 的创建和销毁 - 在 initState 中初始化所有 ViewModel - 在 dispose 中释放所有资源 ### 子页面开发指南 1. **ViewModel 访问** ```dart // 推荐使用 context.read 获取 ViewModel final viewModel = context.read(); // 或者使用 Provider.of(效果相同) final viewModel = Provider.of(context, listen: false); ``` 2. **状态监听** ```dart // 使用 Consumer 监听状态变化 Consumer( builder: (context, viewModel, child) { // 使用 viewModel 的状态 }, ) ``` 3. **注意事项** - 不要在子页面中创建新的 ViewModel 实例 - 使用 AutomaticKeepAliveClientMixin 保持页面状态 - 在 initState 中进行必要的初始化 ### 常见问题 1. **重复实例问题** - 症状:状态更新不生效 - 原因:子页面创建了新的 ViewModel 实例 - 解决:使用 MainScreen 提供的 ViewModel 2. **状态同步问题** - 症状:不同页面状态不同步 - 原因:使用了多个 ViewModel 实例 - 解决:确保使用 MainScreen 提供的单一实例 ================================================ FILE: lib/screens/favorites_screen.dart ================================================ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/widgets/drawer_menu.dart'; import 'package:asmrapp/presentation/viewmodels/favorites_viewmodel.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; import 'package:asmrapp/widgets/pagination_controls.dart'; import 'package:asmrapp/widgets/work_grid_view.dart'; class FavoritesScreen extends StatefulWidget { const FavoritesScreen({super.key}); @override State createState() => _FavoritesScreenState(); } class _FavoritesScreenState extends State { final _layoutStrategy = const WorkLayoutStrategy(); final _scrollController = ScrollController(); late FavoritesViewModel _viewModel; @override void initState() { super.initState(); _viewModel = FavoritesViewModel(); _viewModel.loadFavorites(); } @override void dispose() { _scrollController.dispose(); super.dispose(); } void _onPageChanged(int page) async { await _viewModel.loadPage(page); if (_scrollController.hasClients) { _scrollController.animateTo( 0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } } @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: _viewModel, child: Scaffold( appBar: AppBar( title: const Text('我的收藏'), ), drawer: const DrawerMenu(), body: Consumer( builder: (context, viewModel, child) { return Column( children: [ Expanded( child: WorkGridView( works: viewModel.works, isLoading: viewModel.isLoading, error: viewModel.error, onRetry: () => viewModel.loadFavorites(), layoutStrategy: _layoutStrategy, scrollController: _scrollController, bottomWidget: viewModel.works.isNotEmpty ? PaginationControls( currentPage: viewModel.currentPage, totalPages: viewModel.totalPages ?? 1, onPageChanged: _onPageChanged, isLoading: viewModel.isLoading, ) : null, ), ), ], ); }, ), ), ); } } ================================================ FILE: lib/screens/main_screen.dart ================================================ import 'package:asmrapp/screens/contents/playlists_content.dart'; import 'package:flutter/material.dart'; import 'package:asmrapp/widgets/mini_player/mini_player.dart'; import 'package:asmrapp/widgets/drawer_menu.dart'; import 'package:asmrapp/screens/contents/home_content.dart'; import 'package:asmrapp/screens/contents/recommend_content.dart'; import 'package:asmrapp/screens/contents/popular_content.dart'; import 'package:asmrapp/screens/search_screen.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/presentation/viewmodels/home_viewmodel.dart'; import 'package:asmrapp/presentation/viewmodels/popular_viewmodel.dart'; import 'package:asmrapp/presentation/viewmodels/recommend_viewmodel.dart'; import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; import 'package:asmrapp/presentation/viewmodels/playlists_viewmodel.dart'; /// MainScreen 是应用的主界面,负责管理底部导航栏和对应的内容页面。 /// 它采用了集中式的状态管理架构,所有子页面的 ViewModel 都在这里初始化和提供。 /// /// 架构说明: /// 1. ViewModel 初始化:所有页面的 ViewModel 都在 MainScreen 中初始化,确保单一实例 /// 2. 状态提供:通过 MultiProvider 将 ViewModel 提供给整个子树 /// 3. 生命周期管理:负责所有 ViewModel 的创建和销毁 class MainScreen extends StatefulWidget { const MainScreen({super.key}); @override State createState() => _MainScreenState(); } class _MainScreenState extends State { final _pageController = PageController(initialPage: 1); int _currentIndex = 1; // 集中管理所有页面的 ViewModel // 这些 ViewModel 将通过 Provider 提供给子页面 late final HomeViewModel _homeViewModel; late final PopularViewModel _popularViewModel; late final RecommendViewModel _recommendViewModel; late final PlaylistsViewModel _playlistsViewModel; final _titles = const ['收藏', '主页', '为你推荐', '热门作品']; // 页面内容列表 // 注意:这些页面不应该创建自己的 ViewModel 实例 // 而是应该通过 Provider.of 或 context.read 获取 MainScreen 提供的实例 final _pages = const [ PlaylistsContent(), HomeContent(), RecommendContent(), PopularContent(), ]; @override void initState() { super.initState(); // 初始化所有 ViewModel // 注意初始化顺序,如果有依赖关系需要先初始化依赖项 _homeViewModel = HomeViewModel(); _popularViewModel = PopularViewModel(); _recommendViewModel = RecommendViewModel( Provider.of(context, listen: false), ); _playlistsViewModel = PlaylistsViewModel(); } void _onPageChanged(int index) { setState(() { _currentIndex = index; }); } void _onTabTapped(int index) { _pageController.animateToPage( index, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); } @override void dispose() { // 确保所有 ViewModel 都被正确释放 _pageController.dispose(); _homeViewModel.dispose(); _popularViewModel.dispose(); _recommendViewModel.dispose(); _playlistsViewModel.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return MultiProvider( // 通过 MultiProvider 将所有 ViewModel 提供给子树 // 这样子页面就可以通过 Provider.of 或 context.read 获取对应的 ViewModel providers: [ ChangeNotifierProvider.value(value: _homeViewModel), ChangeNotifierProvider.value(value: _popularViewModel), ChangeNotifierProvider.value(value: _recommendViewModel), ChangeNotifierProvider.value(value: _playlistsViewModel), ], child: Builder( builder: (context) { // 根据当前页面获取对应的总数 final totalCount = _currentIndex == 1 ? context.watch().pagination?.totalCount : _currentIndex == 2 ? context.watch().pagination?.totalCount : _currentIndex == 3 ? context.watch().pagination?.totalCount : null; // 构建标题文本 final title = totalCount != null ? '${_titles[_currentIndex]} (${totalCount})' : _titles[_currentIndex]; return Scaffold( appBar: AppBar( title: Text(title), actions: [ IconButton( icon: const Icon(Icons.filter_list), onPressed: () { if (_currentIndex == 1) { context.read().toggleFilterPanel(); } else if (_currentIndex == 2) { context.read().toggleFilterPanel(); } else if (_currentIndex == 3) { context.read().toggleFilterPanel(); } }, ), IconButton( icon: const Icon(Icons.search), onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => const SearchScreen(), ), ); }, ), ], ), drawer: const DrawerMenu(), body: PageView( controller: _pageController, physics: const ClampingScrollPhysics(), onPageChanged: _onPageChanged, children: _pages, ), bottomNavigationBar: Column( mainAxisSize: MainAxisSize.min, children: [ const MiniPlayer(), NavigationBar( height: 60, labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, backgroundColor: Theme.of(context).colorScheme.surface, elevation: 0, selectedIndex: _currentIndex, onDestinationSelected: _onTabTapped, destinations: const [ NavigationDestination( icon: Icon(Icons.favorite_outline), selectedIcon: Icon(Icons.favorite), label: '收藏', ), NavigationDestination( icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: '主页', ), NavigationDestination( icon: Icon(Icons.recommend_outlined), selectedIcon: Icon(Icons.recommend), label: '推荐', ), NavigationDestination( icon: Icon(Icons.trending_up_outlined), selectedIcon: Icon(Icons.trending_up), label: '热门', ), ], ), ], ), ); }, ), ); } } ================================================ FILE: lib/screens/player_screen.dart ================================================ import 'package:asmrapp/core/platform/lyric_overlay_manager.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; import 'package:asmrapp/widgets/player/player_controls.dart'; import 'package:asmrapp/widgets/player/player_progress.dart'; import 'package:asmrapp/widgets/player/player_cover.dart'; import 'package:asmrapp/screens/detail_screen.dart'; import 'package:asmrapp/widgets/lyrics/components/player_lyric_view.dart'; import 'package:asmrapp/widgets/player/player_work_info.dart'; import 'package:asmrapp/core/platform/wakelock_controller.dart'; class PlayerScreen extends StatefulWidget { const PlayerScreen({super.key}); @override State createState() => _PlayerScreenState(); } class _PlayerScreenState extends State { bool _showLyrics = false; bool _canSwitchView = true; late final PlayerViewModel _viewModel; @override void initState() { super.initState(); _viewModel = GetIt.I(); } Widget _buildContent() { return AnimatedSwitcher( duration: const Duration(milliseconds: 400), switchInCurve: Curves.easeOutQuart, switchOutCurve: Curves.easeInQuart, transitionBuilder: (Widget child, Animation animation) { final isLyrics = (child as dynamic).key == const ValueKey('lyrics'); return FadeTransition( opacity: animation, child: SlideTransition( position: Tween( begin: Offset(0, isLyrics ? 0.1 : -0.1), end: Offset.zero, ).animate(animation), child: ScaleTransition( scale: Tween( begin: 0.95, end: 1.0, ).animate(animation), child: child, ), ), ); }, layoutBuilder: (currentChild, previousChildren) { return Stack( alignment: Alignment.center, children: [ ...previousChildren, if (currentChild != null) currentChild, ], ); }, child: _showLyrics ? LayoutBuilder( key: const ValueKey('lyrics'), builder: (context, constraints) { return PlayerLyricView( onScrollStateChanged: (canSwitch) { setState(() { _canSwitchView = canSwitch; }); }, ); }, ) : ListenableBuilder( listenable: _viewModel, builder: (context, _) { return Column( key: const ValueKey('cover'), mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 32), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Hero( tag: 'mini-player-cover', child: PlayerCover( coverUrl: _viewModel.currentTrackInfo?.coverUrl, ), ), ), const SizedBox(height: 32), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Column( children: [ Hero( tag: 'player-title', child: Material( color: Colors.transparent, child: Text( _viewModel.currentTrackInfo?.title ?? '未在播放', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, ), ), ), const SizedBox(height: 8), if (_viewModel.currentTrackInfo?.artist != null) Text( _viewModel.currentTrackInfo!.artist, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of(context) .colorScheme .onSurface .withOpacity(0.7), ), textAlign: TextAlign.center, ), ], ), ), const Spacer(), PlayerWorkInfo(context: _viewModel.currentContext), ], ); }, ), ); } @override Widget build(BuildContext context) { final lyricManager = GetIt.I(); final wakeLockController = GetIt.I(); return Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.expand_more), onPressed: () { Navigator.of(context).pop(); }, ), actions: [ IconButton( icon: const Icon(Icons.info_outline), onPressed: () { final currentWork = _viewModel.currentContext?.work; if (currentWork != null) { Navigator.of(context).push( MaterialPageRoute( builder: (context) => DetailScreen( work: currentWork, fromPlayer: true, ), ), ); } }, ), IconButton( icon: Icon( lyricManager.isShowing ? Icons.lyrics : Icons.lyrics_outlined, ), onPressed: () => lyricManager.toggle(context), ), ListenableBuilder( listenable: wakeLockController, builder: (context, _) { return IconButton( icon: Icon( wakeLockController.enabled ? Icons.lightbulb : Icons.lightbulb_outline, ), tooltip: wakeLockController.enabled ? '关闭屏幕常亮' : '开启屏幕常亮', onPressed: () => wakeLockController.toggle(), ); }, ), ], backgroundColor: Colors.transparent, elevation: 0, ), body: SafeArea( child: Column( children: [ Expanded( child: GestureDetector( onTap: () { if (_canSwitchView) { setState(() { _showLyrics = !_showLyrics; }); } }, behavior: HitTestBehavior.opaque, child: _buildContent(), ), ), Container( padding: const EdgeInsets.fromLTRB(12, 0, 12, 32), child: Column( children: const [ PlayerProgress(), SizedBox(height: 8), SizedBox(height: 8), PlayerControls(), ], ), ), ], ), ), ); } } ================================================ FILE: lib/screens/search_screen.dart ================================================ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/presentation/viewmodels/search_viewmodel.dart'; import 'package:asmrapp/widgets/work_grid_view.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:asmrapp/widgets/pagination_controls.dart'; class SearchScreen extends StatelessWidget { final String? initialKeyword; const SearchScreen({ super.key, this.initialKeyword, }); @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => SearchViewModel(), child: SearchScreenContent(initialKeyword: initialKeyword), ); } } class SearchScreenContent extends StatefulWidget { final String? initialKeyword; const SearchScreenContent({ super.key, this.initialKeyword, }); @override State createState() => _SearchScreenContentState(); } class _SearchScreenContentState extends State { late final TextEditingController _searchController; final _layoutStrategy = const WorkLayoutStrategy(); final _scrollController = ScrollController(); @override void initState() { super.initState(); _searchController = TextEditingController(text: widget.initialKeyword); // 如果有初始关键词,自动执行搜索 if (widget.initialKeyword?.isNotEmpty == true) { WidgetsBinding.instance.addPostFrameCallback((_) { _onSearch(); }); } } @override void dispose() { _searchController.dispose(); _scrollController.dispose(); super.dispose(); } void _onSearch() { final keyword = _searchController.text.trim(); if (keyword.isEmpty) return; AppLogger.debug('执行搜索: $keyword'); context.read().search(keyword); } void _onPageChanged(int page) async { final viewModel = context.read(); await viewModel.loadPage(page); if (_scrollController.hasClients) { _scrollController.animateTo( 0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } } String _getOrderText(String order, String sort) { switch (order) { case 'create_date': return sort == 'desc' ? '最新收录' : '最早收录'; case 'release': return sort == 'desc' ? '发售日期倒序' : '发售日期顺序'; case 'dl_count': return sort == 'desc' ? '销量倒序' : '销量顺序'; case 'price': return sort == 'desc' ? '价格倒序' : '价格顺序'; case 'rate_average_2dp': return '评价倒序'; case 'review_count': return '评论数量倒序'; case 'id': return sort == 'desc' ? 'RJ号倒序' : 'RJ号顺序'; case 'random': return '随机排序'; default: return '排序'; } } @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ Container( color: Theme.of(context).scaffoldBackgroundColor, padding: EdgeInsets.only( top: MediaQuery.of(context).padding.top + 8, bottom: 8, ), child: Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: '搜索...', filled: true, fillColor: Theme.of(context) .colorScheme .surfaceContainerHighest .withOpacity(0.5), border: OutlineInputBorder( borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear, size: 20), onPressed: () { _searchController.clear(); context.read().clear(); }, ) : null, prefixIcon: const Icon(Icons.search, size: 20), isDense: true, ), textInputAction: TextInputAction.search, onSubmitted: (_) => _onSearch(), onChanged: (value) => setState(() {}), ), ), const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ // 字幕选项 Consumer( builder: (context, viewModel, _) => FilterChip( label: const Text('字幕'), selected: viewModel.hasSubtitle, onSelected: (_) => viewModel.toggleSubtitle(), showCheckmark: true, ), ), const SizedBox(width: 8), // 排序选项 Consumer( builder: (context, viewModel, _) => PopupMenuButton<(String, String)>( child: Chip( label: Text( _getOrderText(viewModel.order, viewModel.sort)), deleteIcon: const Icon(Icons.arrow_drop_down, size: 18), onDeleted: null, ), itemBuilder: (context) => [ const PopupMenuItem( value: ('create_date', 'desc'), child: Text('最新收录'), ), const PopupMenuItem( value: ('release', 'desc'), child: Text('发售日期倒序'), ), const PopupMenuItem( value: ('release', 'asc'), child: Text('发售日期顺序'), ), const PopupMenuItem( value: ('dl_count', 'desc'), child: Text('销量倒序'), ), const PopupMenuItem( value: ('price', 'asc'), child: Text('价格顺序'), ), const PopupMenuItem( value: ('price', 'desc'), child: Text('价格倒序'), ), const PopupMenuItem( value: ('rate_average_2dp', 'desc'), child: Text('评价倒序'), ), const PopupMenuItem( value: ('review_count', 'desc'), child: Text('评论数量倒序'), ), const PopupMenuItem( value: ('id', 'desc'), child: Text('RJ号倒序'), ), const PopupMenuItem( value: ('id', 'asc'), child: Text('RJ号顺序'), ), const PopupMenuItem( value: ('random', 'desc'), child: Text('随机排序'), ), ], onSelected: (value) => viewModel.setOrder(value.$1, value.$2), ), ), ], ), ), ], ), ), Expanded( child: Consumer( builder: (context, viewModel, child) { Widget? emptyWidget; if (viewModel.works.isEmpty && viewModel.keyword.isEmpty) { emptyWidget = const Center( child: Text('输入关键词开始搜索'), ); } else if (viewModel.works.isEmpty) { emptyWidget = const Center( child: Text('没有找到相关结果'), ); } return WorkGridView( works: viewModel.works, isLoading: viewModel.isLoading, error: viewModel.error, onRetry: _onSearch, customEmptyWidget: emptyWidget, layoutStrategy: _layoutStrategy, scrollController: _scrollController, bottomWidget: viewModel.works.isNotEmpty ? PaginationControls( currentPage: viewModel.currentPage, totalPages: viewModel.totalPages, isLoading: viewModel.isLoading, onPageChanged: _onPageChanged, ) : null, ); }, ), ), ], ), ); } } ================================================ FILE: lib/screens/settings/cache_manager_screen.dart ================================================ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/presentation/viewmodels/settings/cache_manager_viewmodel.dart'; class CacheManagerScreen extends StatelessWidget { const CacheManagerScreen({super.key}); @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => CacheManagerViewModel()..loadCacheSize(), child: Scaffold( appBar: AppBar( title: const Text('缓存管理'), ), body: Consumer( builder: (context, viewModel, _) { if (viewModel.isLoading) { return const Center(child: CircularProgressIndicator()); } if (viewModel.error != null) { return Center( child: Text( viewModel.error!, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ); } return ListView( children: [ // 音频缓存 ListTile( title: const Text('音频缓存'), subtitle: Text(viewModel.audioCacheSizeFormatted), trailing: TextButton( onPressed: viewModel.isLoading ? null : () => viewModel.clearAudioCache(), child: const Text('清理'), ), ), const Divider(), // 字幕缓存 ListTile( title: const Text('字幕缓存'), subtitle: Text(viewModel.subtitleCacheSizeFormatted), trailing: TextButton( onPressed: viewModel.isLoading ? null : () => viewModel.clearSubtitleCache(), child: const Text('清理'), ), ), const Divider(), // 总缓存大小 ListTile( title: const Text('总缓存大小'), subtitle: Text(viewModel.totalCacheSizeFormatted), trailing: TextButton( onPressed: viewModel.isLoading ? null : () => viewModel.clearAllCache(), child: const Text('清理全部'), ), ), const Divider(), // 缓存说明 const ListTile( title: Text('缓存说明'), subtitle: Text( '缓存用于存储最近播放的音频文件和字幕文件,以提高再次播放时的加载速度。' '系统会自动清理过期和超量的缓存。' ), ), ], ); }, ), ), ); } } ================================================ FILE: lib/screens/similar_works_screen.dart ================================================ import 'package:asmrapp/widgets/filter/filter_with_keyword.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/presentation/viewmodels/similar_works_viewmodel.dart'; import 'package:asmrapp/widgets/work_grid_view.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; import 'package:asmrapp/widgets/pagination_controls.dart'; class SimilarWorksScreen extends StatefulWidget { final Work work; const SimilarWorksScreen({ super.key, required this.work, }); @override State createState() => _SimilarWorksScreenState(); } class _SimilarWorksScreenState extends State { final _layoutStrategy = const WorkLayoutStrategy(); final _scrollController = ScrollController(); late SimilarWorksViewModel _viewModel; @override void initState() { super.initState(); _viewModel = SimilarWorksViewModel(widget.work); _scrollController.addListener(_onScroll); } @override void dispose() { _scrollController.removeListener(_onScroll); _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_scrollController.position.pixels != _scrollController.position.minScrollExtent) { if (_viewModel.filterPanelExpanded) { _viewModel.closeFilterPanel(); } } } void _scrollToTop() { if (_scrollController.hasClients) { _scrollController.animateTo( 0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } } @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: _viewModel, child: Scaffold( appBar: AppBar( title: const Text('相关推荐'), actions: [ Consumer( builder: (context, viewModel, _) => IconButton( icon: const Icon(Icons.filter_list), onPressed: viewModel.toggleFilterPanel, ), ), ], ), body: Consumer( builder: (context, viewModel, child) { return Stack( children: [ Column( children: [ Expanded( child: WorkGridView( works: viewModel.works, isLoading: viewModel.isLoading, error: viewModel.error, onRetry: () => viewModel.loadSimilarWorks(), layoutStrategy: _layoutStrategy, scrollController: _scrollController, bottomWidget: viewModel.works.isNotEmpty ? PaginationControls( currentPage: viewModel.currentPage, totalPages: viewModel.totalPages ?? 1, onPageChanged: (page) { viewModel.loadPage(page); _scrollToTop(); }, isLoading: viewModel.isLoading, ) : null, ), ), ], ), Positioned( top: 0, left: 0, right: 0, child: AnimatedSlide( duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, offset: Offset(0, viewModel.filterPanelExpanded ? 0 : -1), child: FilterWithKeyword( hasSubtitle: viewModel.hasSubtitle, onSubtitleChanged: (_) => viewModel.toggleSubtitleFilter(), ), ), ), ], ); }, ), ), ); } } ================================================ FILE: lib/utils/file_size_formatter.dart ================================================ class FileSizeFormatter { static String format(int? size) { if (size == null) return ''; const kb = 1024; const mb = kb * 1024; if (size > mb) { return '${(size / mb).toStringAsFixed(2)} MB'; } return '${(size / kb).toStringAsFixed(2)} KB'; } } ================================================ FILE: lib/utils/logger.dart ================================================ import 'package:logger/logger.dart'; class AppLogger { static final Logger _logger = Logger( printer: PrettyPrinter( methodCount: 0, errorMethodCount: 8, lineLength: 120, colors: true, printEmojis: true, printTime: true, ), ); static void init() { Logger.level = Level.debug; } static void debug(String message) => _logger.d(message); static void info(String message) => _logger.i(message); static void warning(String message) => _logger.w(message); static void error(String message, [Object? error, StackTrace? stackTrace]) => _logger.e(message, error: error, stackTrace: stackTrace); } ================================================ FILE: lib/widgets/common/tag_chip.dart ================================================ import 'package:flutter/material.dart'; class TagChip extends StatelessWidget { final String text; final Color? backgroundColor; final Color? textColor; final VoidCallback? onTap; const TagChip({ super.key, required this.text, this.backgroundColor, this.textColor, this.onTap, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: backgroundColor ?? Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: Text( text, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: textColor ?? Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 13, ), ), ), ); } } ================================================ FILE: lib/widgets/detail/mark_selection_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/mark_status.dart'; class MarkSelectionDialog extends StatelessWidget { final MarkStatus? currentStatus; final Function(MarkStatus) onMarkSelected; final bool loading; const MarkSelectionDialog({ super.key, this.currentStatus, required this.onMarkSelected, this.loading = false, }); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return AlertDialog( backgroundColor: isDark ? const Color(0xFF2C2C2C) : Colors.white, title: Text( '标记状态', style: TextStyle( color: isDark ? Colors.white70 : Colors.black87, ), ), content: Column( mainAxisSize: MainAxisSize.min, children: MarkStatus.values.map((status) { final isSelected = status == currentStatus; return ListTile( enabled: !loading, leading: Radio( value: status, groupValue: currentStatus, onChanged: loading ? null : (MarkStatus? value) { if (value != null) { onMarkSelected(value); Navigator.of(context).pop(); } }, fillColor: MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.disabled)) { return isDark ? Colors.white24 : Colors.black26; } if (states.contains(MaterialState.selected)) { return isDark ? Colors.white70 : Colors.black87; } return isDark ? Colors.white38 : Colors.black45; }), ), title: Text( status.label, style: TextStyle( color: loading ? (isDark ? Colors.white38 : Colors.black38) : (isSelected ? (isDark ? Colors.white : Colors.black87) : (isDark ? Colors.white70 : Colors.black54)), ), ), onTap: loading ? null : () { onMarkSelected(status); Navigator.of(context).pop(); }, hoverColor: isDark ? Colors.white.withOpacity(0.05) : Colors.black.withOpacity(0.05), ); }).toList(), ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ); } } ================================================ FILE: lib/widgets/detail/playlist_selection_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/playlists_with_exist_statu/playlist.dart'; class PlaylistSelectionDialog extends StatefulWidget { final List? playlists; final bool isLoading; final String? error; final Future Function(Playlist playlist)? onPlaylistTap; final VoidCallback? onRetry; const PlaylistSelectionDialog({ super.key, this.playlists, required this.isLoading, this.error, this.onPlaylistTap, this.onRetry, }); @override State createState() => _PlaylistSelectionDialogState(); } class _PlaylistSelectionDialogState extends State { final Map _itemStates = {}; @override void initState() { super.initState(); _updateItemStates(); } @override void didUpdateWidget(PlaylistSelectionDialog oldWidget) { super.didUpdateWidget(oldWidget); if (widget.playlists != oldWidget.playlists) { _updateItemStates(); } } void _updateItemStates() { if (widget.playlists == null) return; final newStates = {}; for (final playlist in widget.playlists!) { newStates[playlist.id!] = _PlaylistItemState( playlist: playlist, isLoading: _itemStates[playlist.id!]?.isLoading ?? false, ); } _itemStates.clear(); _itemStates.addAll(newStates); } @override Widget build(BuildContext context) { return Dialog( child: ConstrainedBox( constraints: const BoxConstraints( maxWidth: 400, maxHeight: 500, ), child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '添加到收藏夹', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), Flexible( child: _buildContent(), ), ], ), ), ), ); } Widget _buildContent() { if (widget.isLoading) { return const Center( child: CircularProgressIndicator(), ); } if (widget.error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(widget.error!), if (widget.onRetry != null) ...[ const SizedBox(height: 8), ElevatedButton( onPressed: widget.onRetry, child: const Text('重试'), ), ], ], ), ); } if (widget.playlists == null || widget.playlists!.isEmpty) { return const Center( child: Text('暂无收藏夹'), ); } return ListView.builder( shrinkWrap: true, itemCount: widget.playlists!.length, itemBuilder: (context, index) { final playlist = widget.playlists![index]; final state = _itemStates[playlist.id!]!; return _PlaylistItem( state: state, onTap: () => _handlePlaylistTap(state), ); }, ); } Future _handlePlaylistTap(_PlaylistItemState state) async { if (state.isLoading || widget.onPlaylistTap == null) return; setState(() { state.isLoading = true; }); try { await widget.onPlaylistTap!(state.playlist); if (mounted) { final newPlaylist = state.playlist.copyWith( exist: !(state.playlist.exist ?? false), ); _itemStates[state.playlist.id!] = _PlaylistItemState( playlist: newPlaylist, isLoading: false, ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( '${newPlaylist.exist! ? '添加成功' : '移除成功'}: ${_getDisplayName(newPlaylist.name)}', style: TextStyle( color: Theme.of(context).colorScheme.onSurface, ), ), duration: const Duration(seconds: 1), backgroundColor: Theme.of(context).colorScheme.surfaceVariant, showCloseIcon: true, closeIconColor: Theme.of(context).colorScheme.onSurface, behavior: SnackBarBehavior.floating, ), ); setState(() {}); } } finally { if (mounted) { setState(() { state.isLoading = false; }); } } } String _getDisplayName(String? name) { switch (name) { case '__SYS_PLAYLIST_MARKED': return '我标记的'; case '__SYS_PLAYLIST_LIKED': return '我喜欢的'; default: return name ?? ''; } } } class _PlaylistItem extends StatelessWidget { final _PlaylistItemState state; final VoidCallback? onTap; const _PlaylistItem({ required this.state, this.onTap, }); String _getDisplayName(String? name) { switch (name) { case '__SYS_PLAYLIST_MARKED': return '我标记的'; case '__SYS_PLAYLIST_LIKED': return '我喜欢的'; default: return name ?? ''; } } @override Widget build(BuildContext context) { return ListTile( title: Text(_getDisplayName(state.playlist.name)), subtitle: Text('${state.playlist.worksCount ?? 0} 个作品'), trailing: state.isLoading ? const SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, ), ) : Checkbox( value: state.playlist.exist ?? false, onChanged: (_) => onTap?.call(), ), onTap: onTap, ); } } class _PlaylistItemState { final Playlist playlist; bool isLoading; _PlaylistItemState({ required this.playlist, this.isLoading = false, }); } ================================================ FILE: lib/widgets/detail/work_action_buttons.dart ================================================ import 'package:asmrapp/data/models/mark_status.dart'; import 'package:flutter/material.dart'; class WorkActionButtons extends StatelessWidget { final VoidCallback onRecommendationsTap; final bool hasRecommendations; final bool checkingRecommendations; final VoidCallback onFavoriteTap; final bool loadingFavorite; final VoidCallback onMarkTap; final MarkStatus? currentMarkStatus; final bool loadingMark; const WorkActionButtons({ super.key, required this.onRecommendationsTap, required this.hasRecommendations, required this.checkingRecommendations, required this.onFavoriteTap, this.loadingFavorite = false, required this.onMarkTap, this.currentMarkStatus, this.loadingMark = false, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _ActionButton( icon: Icons.favorite_border, label: '收藏', onTap: onFavoriteTap, loading: loadingFavorite, ), _ActionButton( icon: Icons.bookmark_border, label: currentMarkStatus?.label ?? '标记', onTap: onMarkTap, loading: loadingMark, ), _ActionButton( icon: Icons.star_border, label: '评分', onTap: () { // TODO: 实现评分功能 }, ), _ActionButton( icon: Icons.recommend, label: checkingRecommendations ? '检查中' : (hasRecommendations ? '相关推荐' : '暂无推荐'), onTap: hasRecommendations ? onRecommendationsTap : null, loading: checkingRecommendations, ), ], ), ); } } class _ActionButton extends StatelessWidget { final IconData icon; final String label; final VoidCallback? onTap; final bool loading; const _ActionButton({ required this.icon, required this.label, this.onTap, this.loading = false, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final disabled = onTap == null && !loading; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.all(8.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ if (loading) SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, color: theme.colorScheme.primary, ), ) else Icon( icon, color: disabled ? theme.colorScheme.onSurface.withOpacity(0.38) : null, ), const SizedBox(height: 4), Text( label, style: theme.textTheme.bodySmall?.copyWith( color: disabled ? theme.colorScheme.onSurface.withOpacity(0.38) : null, ), ), ], ), ), ); } } ================================================ FILE: lib/widgets/detail/work_cover.dart ================================================ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; class WorkCover extends StatelessWidget { final String imageUrl; final int workId; final String sourceId; final String? releaseDate; final String? heroTag; const WorkCover({ super.key, required this.imageUrl, required this.workId, required this.sourceId, this.releaseDate, this.heroTag, }); @override Widget build(BuildContext context) { Widget content = Stack( children: [ AspectRatio( aspectRatio: 195 / 146, child: CachedNetworkImage( imageUrl: imageUrl, fit: BoxFit.cover, width: double.infinity, height: double.infinity, ), ), Positioned( left: 8, top: 8, child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(4), ), child: Text( sourceId, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.white, fontSize: 12, ), ), ), ), if (releaseDate != null) Positioned( right: 8, bottom: 8, child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(4), ), child: Text( releaseDate!, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.white, fontSize: 12, ), ), ), ), ], ); if (heroTag != null) { return Hero( tag: heroTag!, child: content, ); } return content; } } ================================================ FILE: lib/widgets/detail/work_file_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:asmrapp/utils/file_size_formatter.dart'; class WorkFileItem extends StatelessWidget { final Child file; final double indentation; final Function(Child file)? onFileTap; const WorkFileItem({ super.key, required this.file, required this.indentation, this.onFileTap, }); @override Widget build(BuildContext context) { final bool isAudio = file.type?.toLowerCase() == 'audio'; final colorScheme = Theme.of(context).colorScheme; return Padding( padding: EdgeInsets.only(left: indentation), child: ListTile( title: Text( file.title ?? '', style: TextStyle( color: colorScheme.onSurface, ), ), subtitle: Text( FileSizeFormatter.format(file.size), style: TextStyle( color: colorScheme.onSurfaceVariant, ), ), leading: Icon( isAudio ? Icons.audio_file : Icons.insert_drive_file, color: isAudio ? Colors.green : Colors.blue, ), dense: true, onTap: isAudio ? () { AppLogger.debug('点击音频文件: ${file.title}'); onFileTap?.call(file); } : null, ), ); } } ================================================ FILE: lib/widgets/detail/work_files_list.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/files/files.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/widgets/detail/work_folder_item.dart'; import 'package:asmrapp/widgets/detail/work_file_item.dart'; class WorkFilesList extends StatelessWidget { final Files files; final Function(Child file)? onFileTap; const WorkFilesList({ super.key, required this.files, this.onFileTap, }); @override Widget build(BuildContext context) { // 重置文件夹展开状态 WorkFolderItem.resetExpandState(); return Card( margin: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(16), child: Text( '文件列表', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ), Divider( height: 1, color: Theme.of(context).colorScheme.surfaceVariant, ), ...files.children ?.map((child) => child.type == 'folder' ? WorkFolderItem( folder: child, indentation: 0, onFileTap: onFileTap, ) : WorkFileItem( file: child, indentation: 0, onFileTap: onFileTap, )) .toList() ?? [], ], ), ); } } ================================================ FILE: lib/widgets/detail/work_files_skeleton.dart ================================================ import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; class WorkFilesSkeleton extends StatelessWidget { const WorkFilesSkeleton({super.key}); Widget _buildShimmerItem() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ // 图标占位 Container( width: 24, height: 24, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), ), const SizedBox(width: 16), // 标题占位 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: 14, width: double.infinity, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 8), Container( height: 10, width: 100, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(2), ), ), ], ), ), ], ), ); } @override Widget build(BuildContext context) { return Card( margin: const EdgeInsets.all(8), child: Shimmer.fromColors( baseColor: Theme.of(context).colorScheme.surfaceContainerHighest, highlightColor: Theme.of(context).colorScheme.surface, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题占位 Padding( padding: const EdgeInsets.all(16), child: Container( height: 24, width: 120, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), ), ), const Divider(height: 1), // 列表项占位 ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: 6, // 显示6个占位项 itemBuilder: (context, index) => _buildShimmerItem(), ), ], ), ), ); } } ================================================ FILE: lib/widgets/detail/work_folder_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:asmrapp/widgets/detail/work_file_item.dart'; import 'package:asmrapp/core/audio/models/file_path.dart'; class WorkFolderItem extends StatelessWidget { final Child folder; final double indentation; final Function(Child file)? onFileTap; // 支持的音频格式列表,按优先级排序 static const _audioFormats = ['.mp3', '.wav']; // 静态变量用于跟踪第一个包含音频的文件夹的完整路径 static List? _audioFolderPath; // 静态方法用于重置展开状态 static void resetExpandState() { _audioFolderPath = null; } const WorkFolderItem({ super.key, required this.folder, required this.indentation, this.onFileTap, }); bool _shouldExpandFolder(Child folder) { // 如果还没有找到第一个音频文件夹,就搜索并记录 _audioFolderPath ??= FilePath.findFirstAudioFolderPath( [folder], formats: _audioFormats, ); // 判断当前文件夹是否在音频文件夹的路径上 return FilePath.isInPath(_audioFolderPath, folder.title); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final shouldExpand = _shouldExpandFolder(folder); return Padding( padding: EdgeInsets.only(left: indentation), child: Theme( data: Theme.of(context).copyWith( dividerColor: Colors.transparent, // 确保子组件也能继承正确的文字颜色 textTheme: Theme.of(context).textTheme.apply( bodyColor: colorScheme.onSurface, displayColor: colorScheme.onSurface, ), ), child: ExpansionTile( title: Text( folder.title ?? '', style: TextStyle( color: colorScheme.onSurface, ), ), leading: Icon( Icons.folder, color: colorScheme.primary, ), initiallyExpanded: shouldExpand, children: folder.children ?.map((child) => child.type == 'folder' ? WorkFolderItem( folder: child, indentation: indentation + 16.0, onFileTap: onFileTap, ) : WorkFileItem( file: child, indentation: indentation + 16.0, onFileTap: onFileTap, )) .toList() ?? [], onExpansionChanged: (expanded) { AppLogger.debug( '${expanded ? "展开" : "折叠"}文件夹: ${folder.title}', ); }, ), ), ); } } ================================================ FILE: lib/widgets/detail/work_info.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/data/models/works/tag.dart'; import 'package:asmrapp/widgets/common/tag_chip.dart'; import 'package:asmrapp/widgets/detail/work_info_header.dart'; import 'package:asmrapp/utils/logger.dart'; class WorkInfo extends StatelessWidget { final Work work; const WorkInfo({ super.key, required this.work, }); String _getLocalizedTagName(Tag tag) { final zhName = tag.i18n?.zhCn?.name; if (zhName != null) return zhName; final jaName = tag.i18n?.jaJp?.name; if (jaName != null) return jaName; return tag.name ?? ''; } void _onTagTap(BuildContext context, Tag tag) { final keyword = tag.name ?? ''; if (keyword.isEmpty) return; AppLogger.debug('点击标签: $keyword'); Navigator.pushNamed( context, '/search', arguments: keyword, ); } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ WorkInfoHeader(work: work), const SizedBox(height: 8), if (work.tags != null && work.tags!.isNotEmpty) Wrap( spacing: 8, runSpacing: 8, children: work.tags! .map((tag) => TagChip( text: _getLocalizedTagName(tag), onTap: () => _onTagTap(context, tag), )) .toList(), ), ], ), ); } } ================================================ FILE: lib/widgets/detail/work_info_header.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/widgets/common/tag_chip.dart'; import 'package:asmrapp/widgets/detail/work_stats_info.dart'; import 'package:asmrapp/utils/logger.dart'; class WorkInfoHeader extends StatelessWidget { final Work work; const WorkInfoHeader({ super.key, required this.work, }); void _onTagTap(BuildContext context, String keyword) { if (keyword.isEmpty) return; AppLogger.debug('点击标签: $keyword'); Navigator.pushNamed( context, '/search', arguments: keyword, ); } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( work.title ?? '', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), WorkStatsInfo(work: work), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: [ if (work.circle?.name != null) TagChip( text: work.circle?.name ?? '', backgroundColor: Colors.orange.withOpacity(0.2), textColor: Colors.orange[700], onTap: () => _onTagTap(context, work.circle?.name ?? ''), ), ...?work.vas?.map( (va) => TagChip( text: va['name'] ?? '', backgroundColor: Colors.green.withOpacity(0.2), textColor: Colors.green[700], onTap: () => _onTagTap(context, va['name'] ?? ''), ), ), if (work.hasSubtitle == true) TagChip( text: '字幕', backgroundColor: Colors.blue.withOpacity(0.2), textColor: Colors.blue[700], ), ], ), ], ); } } ================================================ FILE: lib/widgets/detail/work_stats_info.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; class WorkStatsInfo extends StatelessWidget { final Work work; const WorkStatsInfo({ super.key, required this.work, }); String _formatDuration(int? seconds) { if (seconds == null) return ''; final duration = Duration(seconds: seconds); final hours = duration.inHours; final minutes = duration.inMinutes.remainder(60); if (hours > 0) { return '${hours}h ${minutes}m'; } else { return '${minutes}m'; } } @override Widget build(BuildContext context) { return Row( children: [ if (work.duration != null) ...[ Icon( Icons.access_time, size: 16, color: Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox(width: 4), Text( _formatDuration(work.duration), style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(width: 16), ], if ((work.rateCount ?? 0) > 0) ...[ const Icon(Icons.star, size: 16, color: Colors.amber), const SizedBox(width: 4), Text( (work.rateAverage2dp ?? 0.0).toStringAsFixed(1), style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(width: 16), ], Icon( Icons.download, size: 16, color: Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox(width: 4), Text( '${work.dlCount ?? 0}', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ); } } ================================================ FILE: lib/widgets/drawer_menu.dart ================================================ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/common/constants/strings.dart'; import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; import 'package:asmrapp/presentation/widgets/auth/login_dialog.dart'; import 'package:asmrapp/screens/favorites_screen.dart'; import 'package:asmrapp/screens/settings/cache_manager_screen.dart'; import 'package:asmrapp/core/theme/theme_controller.dart'; import 'package:asmrapp/core/platform/wakelock_controller.dart'; import 'package:get_it/get_it.dart'; class DrawerMenu extends StatelessWidget { const DrawerMenu({super.key}); void _showLoginDialog(BuildContext context) { showDialog( context: context, builder: (context) => const LoginDialog(), ); } @override Widget build(BuildContext context) { return Drawer( backgroundColor: Theme.of(context).colorScheme.surface, child: ListTileTheme( style: ListTileStyle.drawer, child: ListView( padding: EdgeInsets.zero, children: [ Theme( data: Theme.of(context).copyWith( dividerTheme: const DividerThemeData(color: Colors.transparent), ), child: const DrawerHeader( decoration: BoxDecoration( color: Colors.deepPurple, ), child: Text( Strings.appName, style: TextStyle( color: Colors.white, fontSize: 24, ), ), ), ), Consumer( builder: (context, authVM, _) { return ListTile( leading: const Icon(Icons.person), title: Text( authVM.isLoggedIn ? authVM.username ?? '' : '登录', ), onTap: () { Navigator.pop(context); if (authVM.isLoggedIn) { authVM.logout(); } else { _showLoginDialog(context); } }, ); }, ), ListTile( leading: const Icon(Icons.favorite), title: const Text(Strings.favorites), onTap: () { Navigator.pop(context); // 检查用户是否已登录 final authVM = context.read(); if (!authVM.isLoggedIn) { // 如果未登录,显示登录对话框 _showLoginDialog(context); return; } // 导航到收藏页面 Navigator.push( context, MaterialPageRoute( builder: (context) => const FavoritesScreen(), ), ); }, ), ListTile( leading: const Icon(Icons.settings), title: const Text(Strings.settings), onTap: () { Navigator.pop(context); // TODO: 导航到设置页面 }, ), ListTile( leading: const Icon(Icons.storage), title: const Text('缓存管理'), onTap: () { Navigator.pop(context); Navigator.push( context, MaterialPageRoute( builder: (context) => const CacheManagerScreen(), ), ); }, ), Divider( color: Theme.of(context).colorScheme.surfaceVariant, height: 1, ), Consumer( builder: (context, themeController, _) { return ListTile( leading: Icon(_getThemeIcon(themeController.themeMode)), title: Text(_getThemeText(themeController.themeMode)), onTap: () => themeController.toggleThemeMode(), ); }, ), ListenableBuilder( listenable: GetIt.I(), builder: (context, _) { final controller = GetIt.I(); return SwitchListTile( title: const Text('屏幕常亮'), value: controller.enabled, onChanged: (_) => controller.toggle(), ); }, ), ], ), ), ); } IconData _getThemeIcon(ThemeMode mode) { switch (mode) { case ThemeMode.system: return Icons.brightness_auto; case ThemeMode.light: return Icons.brightness_high; case ThemeMode.dark: return Icons.brightness_2; } } String _getThemeText(ThemeMode mode) { switch (mode) { case ThemeMode.system: return '跟随系统主题'; case ThemeMode.light: return '浅色模式'; case ThemeMode.dark: return '深色模式'; } } } ================================================ FILE: lib/widgets/filter/filter_panel.dart ================================================ import 'package:flutter/material.dart'; class FilterPanel extends StatelessWidget { final bool expanded; final bool hasSubtitle; final String orderField; final bool isDescending; final ValueChanged onSubtitleChanged; final ValueChanged onOrderFieldChanged; final ValueChanged onSortDirectionChanged; const FilterPanel({ super.key, this.expanded = false, required this.hasSubtitle, required this.orderField, required this.isDescending, required this.onSubtitleChanged, required this.onOrderFieldChanged, required this.onSortDirectionChanged, }); String _getOrderFieldText(String field) { switch (field) { case 'create_date': return '收录时间'; case 'release': return '发售日期'; case 'dl_count': return '销量'; case 'price': return '价格'; case 'rate_average_2dp': return '评价'; case 'review_count': return '评论数量'; case 'id': return 'RJ号'; case 'rating': return '我的评价'; case 'nsfw': return '全年龄'; case 'random': return '随机'; default: return '排序'; } } @override Widget build(BuildContext context) { return SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ // 字幕过滤 Container( decoration: BoxDecoration( border: Border.all( color: Theme.of(context).colorScheme.outline.withOpacity(0.5), ), borderRadius: BorderRadius.circular(8), ), child: Material( type: MaterialType.transparency, child: InkWell( onTap: () => onSubtitleChanged(!hasSubtitle), borderRadius: BorderRadius.circular(7), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( hasSubtitle ? Icons.check_box : Icons.check_box_outline_blank, size: 20, color: hasSubtitle ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox(width: 8), Text( '有字幕', style: TextStyle( color: hasSubtitle ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface, ), ), ], ), ), ), ), ), const SizedBox(width: 8), // 排序字段 Container( decoration: BoxDecoration( border: Border.all( color: Theme.of(context).colorScheme.outline.withOpacity(0.5), ), borderRadius: BorderRadius.circular(8), ), child: PopupMenuButton( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(_getOrderFieldText(orderField)), const SizedBox(width: 4), const Icon(Icons.arrow_drop_down, size: 20), ], ), ), itemBuilder: (context) => [ _buildOrderMenuItem('收录时间', 'create_date'), _buildOrderMenuItem('发售日期', 'release'), _buildOrderMenuItem('销量', 'dl_count'), _buildOrderMenuItem('价格', 'price'), _buildOrderMenuItem('评价', 'rate_average_2dp'), _buildOrderMenuItem('评论数量', 'review_count'), _buildOrderMenuItem('RJ号', 'id'), _buildOrderMenuItem('我的评价', 'rating'), _buildOrderMenuItem('全年龄', 'nsfw'), _buildOrderMenuItem('随机', 'random'), ], ), ), const SizedBox(width: 8), // 排序方向 Container( decoration: BoxDecoration( border: Border.all( color: Theme.of(context).colorScheme.outline.withOpacity(0.5), ), borderRadius: BorderRadius.circular(8), ), child: Material( type: MaterialType.transparency, child: InkWell( onTap: () => onSortDirectionChanged(!isDescending), borderRadius: BorderRadius.circular(7), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(isDescending ? '降序' : '升序'), const SizedBox(width: 4), Icon( isDescending ? Icons.arrow_downward : Icons.arrow_upward, size: 20, color: Theme.of(context).colorScheme.onSurface, ), ], ), ), ), ), ), ], ), ), ); } PopupMenuItem _buildOrderMenuItem(String text, String value) { return PopupMenuItem( value: value, child: Text(text), ); } } ================================================ FILE: lib/widgets/filter/filter_with_keyword.dart ================================================ import 'package:flutter/material.dart'; class FilterWithKeyword extends StatelessWidget { final bool hasSubtitle; final Function(bool) onSubtitleChanged; final bool showSearchField; final String? keyword; final Function(String)? onSearch; final VoidCallback? onClear; const FilterWithKeyword({ super.key, required this.hasSubtitle, required this.onSubtitleChanged, this.showSearchField = false, this.keyword, this.onSearch, this.onClear, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Material( elevation: 2, color: colorScheme.surface, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Container( decoration: BoxDecoration( border: Border.all( color: Theme.of(context).colorScheme.outline.withOpacity(0.5), ), borderRadius: BorderRadius.circular(8), ), child: Material( type: MaterialType.transparency, child: InkWell( onTap: () => onSubtitleChanged(!hasSubtitle), borderRadius: BorderRadius.circular(7), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( hasSubtitle ? Icons.check_box : Icons.check_box_outline_blank, size: 20, color: hasSubtitle ? colorScheme.primary : colorScheme.onSurfaceVariant, ), const SizedBox(width: 8), Text( '有字幕', style: TextStyle( color: hasSubtitle ? colorScheme.primary : colorScheme.onSurface, ), ), ], ), ), ), ), ), ], ), ), ); } } ================================================ FILE: lib/widgets/lyrics/components/lyric_line.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/core/audio/models/subtitle.dart'; class LyricLine extends StatelessWidget { final Subtitle subtitle; final bool isActive; final double opacity; final VoidCallback? onTap; const LyricLine({ super.key, required this.subtitle, this.isActive = false, this.opacity = 1.0, this.onTap, }); @override Widget build(BuildContext context) { return Center( child: AnimatedOpacity( duration: const Duration(milliseconds: 300), opacity: opacity, child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), child: Text( subtitle.text, style: Theme.of(context).textTheme.bodyLarge?.copyWith( fontSize: 20, height: 1.3, color: isActive ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface.withOpacity(0.7), fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, ), textAlign: TextAlign.center, ), ), ), ), ); } } ================================================ FILE: lib/widgets/lyrics/components/player_lyric_view.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:asmrapp/core/subtitle/i_subtitle_service.dart'; import 'package:asmrapp/core/audio/models/subtitle.dart'; import 'lyric_line.dart'; import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; class PlayerLyricView extends StatefulWidget { final bool immediateScroll; final Function(bool canSwitch) onScrollStateChanged; const PlayerLyricView({ super.key, this.immediateScroll = false, required this.onScrollStateChanged, }); @override State createState() => _PlayerLyricViewState(); } class _PlayerLyricViewState extends State { final ISubtitleService _subtitleService = GetIt.I(); final PlayerViewModel _viewModel = GetIt.I(); final ItemScrollController _itemScrollController = ItemScrollController(); final ItemPositionsListener _itemPositionsListener = ItemPositionsListener.create(); bool _isFirstBuild = true; Subtitle? _lastScrolledSubtitle; // 用于控制视图切换的计时器和状态 // 当用户手动滚动时,暂时禁用视图切换功能,防止切换到封面 Timer? _scrollDebounceTimer; // 用于控制自动滚动的计时器和状态 // 当用户手动滚动时,暂时禁用自动滚动功能,让用户可以自由浏览歌词 bool _allowAutoScroll = true; Timer? _autoScrollDebounceTimer; @override void initState() { super.initState(); } @override void dispose() { // 清理所有计时器 _scrollDebounceTimer?.cancel(); // 视图切换计时器 _autoScrollDebounceTimer?.cancel(); // 自动滚动计时器 super.dispose(); } void _scrollToCurrentLyric(SubtitleWithState current) { if (!_itemScrollController.isAttached) return; // 如果当前禁用了自动滚动(用户正在手动浏览),则不执行自动滚动 if (!_allowAutoScroll) return; // 避免重复滚动到同一句歌词 if (_lastScrolledSubtitle == current.subtitle) return; _lastScrolledSubtitle = current.subtitle; if (_isFirstBuild) { _isFirstBuild = false; // 首次加载时直接跳转,不使用动画 _itemScrollController.jumpTo( index: current.subtitle.index, alignment: 0.5, ); } else { // 正常播放时使用平滑滚动动画 _itemScrollController.scrollTo( index: current.subtitle.index, duration: const Duration(milliseconds: 300), curve: Curves.easeOutQuart, alignment: 0.5, ); } } @override Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; final baseUnit = screenHeight * 0.04; return StreamBuilder( stream: _subtitleService.currentSubtitleWithStateStream, initialData: _subtitleService.currentSubtitleWithState, builder: (context, snapshot) { final currentSubtitle = snapshot.data; final subtitleList = _subtitleService.subtitleList; if (subtitleList == null || subtitleList.subtitles.isEmpty) { return const Center( child: Text('无歌词'), ); } if (currentSubtitle != null) { WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToCurrentLyric(currentSubtitle); }); } return NotificationListener( onNotification: (notification) { if (notification is ScrollStartNotification && notification.dragDetails != null) { // 用户开始手动滚动 // 立即禁用视图切换功能 widget.onScrollStateChanged(false); // 禁用自动滚动功能 _allowAutoScroll = false; // 取消所有待执行的计时器 _scrollDebounceTimer?.cancel(); _autoScrollDebounceTimer?.cancel(); } else if (notification is ScrollEndNotification) { // 用户结束滚动 // 延长视图切换的禁用时间到1秒 _scrollDebounceTimer?.cancel(); _scrollDebounceTimer = Timer(const Duration(milliseconds: 1000), () { if (mounted) { widget.onScrollStateChanged(true); } }); // 自动滚动计时器保持3秒 _autoScrollDebounceTimer?.cancel(); _autoScrollDebounceTimer = Timer(const Duration(milliseconds: 3000), () { if (mounted) { setState(() { _allowAutoScroll = true; // 恢复时立即滚动到当前播放位置 if (_subtitleService.currentSubtitleWithState != null) { _scrollToCurrentLyric(_subtitleService.currentSubtitleWithState!); } }); } }); } return false; }, child: ScrollablePositionedList.builder( itemCount: subtitleList.subtitles.length, itemScrollController: _itemScrollController, itemPositionsListener: _itemPositionsListener, padding: EdgeInsets.symmetric( vertical: screenHeight * 0.3, horizontal: baseUnit * 0.8, ), itemBuilder: (context, index) { final subtitle = subtitleList.subtitles[index]; final isActive = currentSubtitle?.subtitle == subtitle; return Padding( padding: EdgeInsets.symmetric( vertical: baseUnit * 0.35, ), child: LyricLine( subtitle: subtitle, isActive: isActive, opacity: isActive ? 1.0 : 0.5, onTap: () async { widget.onScrollStateChanged(false); await _viewModel.seek(subtitle.start); Future.delayed(const Duration(milliseconds: 500), () { if (mounted) { widget.onScrollStateChanged(true); } }); }, ), ); }, ), ); }, ); } } ================================================ FILE: lib/widgets/mini_player/mini_player.dart ================================================ import 'package:asmrapp/screens/player_screen.dart'; import 'package:flutter/material.dart'; import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; import 'mini_player_controls.dart'; import 'mini_player_progress.dart'; import 'package:get_it/get_it.dart'; import 'mini_player_cover.dart'; class MiniPlayer extends StatelessWidget { static const height = 48.0; const MiniPlayer({super.key}); @override Widget build(BuildContext context) { final viewModel = GetIt.I(); return ListenableBuilder( listenable: viewModel, builder: (context, _) { return GestureDetector( onTap: () { Navigator.of(context).push( PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) { return const PlayerScreen(); }, transitionsBuilder: (context, animation, secondaryAnimation, child) { // 创建一个曲线动画 final curvedAnimation = CurvedAnimation( parent: animation, curve: Curves.easeOutQuart, ); return Stack( children: [ // 背景淡入效果 FadeTransition( opacity: curvedAnimation, child: Container( color: Theme.of(context).scaffoldBackgroundColor, ), ), // 内容从底部滑入并淡入 FadeTransition( opacity: Tween( begin: 0.3, end: 1.0, ).animate(curvedAnimation), child: SlideTransition( position: Tween( begin: const Offset(0, 0.3), end: Offset.zero, ).animate(curvedAnimation), child: child, ), ), ], ); }, transitionDuration: const Duration(milliseconds: 400), ), ); }, child: Container( height: height, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, -1), ), ], ), child: Column( children: [ const MiniPlayerProgress(), Expanded( child: Row( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 4, 8, 4), child: Hero( tag: 'mini-player-cover', child: MiniPlayerCover( coverUrl: viewModel.currentTrackInfo?.coverUrl, ), ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Hero( tag: 'player-title', child: Material( color: Colors.transparent, child: Text( viewModel.currentTrackInfo?.title ?? '未在播放', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleSmall, ), ), ), ), ), const MiniPlayerControls(), ], ), ), ], ), ), ); }, ); } } ================================================ FILE: lib/widgets/mini_player/mini_player_controls.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; import 'package:get_it/get_it.dart'; class MiniPlayerControls extends StatelessWidget { const MiniPlayerControls({super.key}); @override Widget build(BuildContext context) { final viewModel = GetIt.I(); return ListenableBuilder( listenable: viewModel, builder: (context, _) { return IconButton( icon: Icon( viewModel.isPlaying ? Icons.pause : Icons.play_arrow, ), onPressed: viewModel.playPause, ); }, ); } } ================================================ FILE: lib/widgets/mini_player/mini_player_cover.dart ================================================ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:shimmer/shimmer.dart'; class MiniPlayerCover extends StatelessWidget { final String? coverUrl; final double size; const MiniPlayerCover({ super.key, this.coverUrl, this.size = 48, }); @override Widget build(BuildContext context) { if (coverUrl == null) { return _buildEmptyPlaceholder(); } return ClipRRect( borderRadius: BorderRadius.circular(4), child: CachedNetworkImage( imageUrl: coverUrl!, width: size, height: size, fit: BoxFit.cover, placeholder: (context, url) => _buildPlaceholder(context), errorWidget: (context, url, error) => _buildErrorWidget(), ), ); } Widget _buildEmptyPlaceholder() { return Container( width: size, height: size, decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(4), ), child: const Icon(Icons.music_note, color: Colors.grey), ); } Widget _buildPlaceholder(BuildContext context) { return Shimmer.fromColors( baseColor: Theme.of(context).colorScheme.surfaceContainerHighest, highlightColor: Theme.of(context).colorScheme.surface, child: Container( width: size, height: size, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), ), ); } Widget _buildErrorWidget() { return Container( width: size, height: size, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(4), ), child: const Icon(Icons.broken_image, color: Colors.grey), ); } } ================================================ FILE: lib/widgets/mini_player/mini_player_progress.dart ================================================ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; class MiniPlayerProgress extends StatelessWidget { const MiniPlayerProgress({super.key}); @override Widget build(BuildContext context) { final viewModel = GetIt.I(); return ListenableBuilder( listenable: viewModel, builder: (context, _) { final position = viewModel.position?.inMilliseconds.toDouble() ?? 0.0; final duration = viewModel.duration?.inMilliseconds.toDouble() ?? 0.0; final progress = duration > 0 ? position / duration : 0.0; return SizedBox( height: 2, child: LinearProgressIndicator( value: progress, backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary, ), ), ); }, ); } } ================================================ FILE: lib/widgets/pagination_controls.dart ================================================ import 'package:flutter/material.dart'; class PaginationControls extends StatelessWidget { final int currentPage; final int? totalPages; final bool isLoading; final Function(int) onPageChanged; const PaginationControls({ super.key, required this.currentPage, required this.totalPages, required this.isLoading, required this.onPageChanged, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(width: 32), IconButton( onPressed: currentPage > 1 && !isLoading ? () => onPageChanged(currentPage - 1) : null, icon: const Icon(Icons.chevron_left), ), const SizedBox(width: 16), Text('$currentPage/${totalPages ?? "?"}'), const SizedBox(width: 16), IconButton( onPressed: totalPages != null && currentPage < totalPages! && !isLoading ? () => onPageChanged(currentPage + 1) : null, icon: const Icon(Icons.chevron_right), ), SizedBox( width: 32, child: isLoading ? const Padding( padding: EdgeInsets.only(left: 16), child: SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, ), ), ) : null, ), ], ), ); } } ================================================ FILE: lib/widgets/player/player_controls.dart ================================================ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; class PlayerControls extends StatelessWidget { const PlayerControls({super.key}); @override Widget build(BuildContext context) { final viewModel = GetIt.I(); return ListenableBuilder( listenable: viewModel, builder: (context, _) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( iconSize: 32, icon: const Icon(Icons.skip_previous), onPressed: viewModel.previous, ), const SizedBox(width: 16), Container( width: 64, height: 64, decoration: BoxDecoration( shape: BoxShape.circle, color: Theme.of(context).primaryColor, ), child: IconButton( iconSize: 32, color: Colors.white, icon: Icon( viewModel.isPlaying ? Icons.pause : Icons.play_arrow, ), onPressed: viewModel.playPause, ), ), const SizedBox(width: 16), IconButton( iconSize: 32, icon: const Icon(Icons.skip_next), onPressed: viewModel.next, ), ], ); }, ); } } ================================================ FILE: lib/widgets/player/player_cover.dart ================================================ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:shimmer/shimmer.dart'; class PlayerCover extends StatelessWidget { final String? coverUrl; final double? maxWidth; const PlayerCover({ super.key, this.coverUrl, this.maxWidth = 480, }); @override Widget build(BuildContext context) { return AspectRatio( aspectRatio: 4/3, child: Container( constraints: BoxConstraints( maxWidth: maxWidth ?? 480, ), decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: coverUrl != null ? CachedNetworkImage( imageUrl: coverUrl!, fit: BoxFit.cover, placeholder: (context, url) => Shimmer.fromColors( baseColor: Theme.of(context).colorScheme.surfaceContainerHighest, highlightColor: Theme.of(context).colorScheme.surface, child: Container( color: Colors.white, ), ), errorWidget: (context, url, error) => Container( color: Theme.of(context).colorScheme.errorContainer, child: Center( child: Icon( Icons.error_outline, size: 48, color: Theme.of(context).colorScheme.error, ), ), ), ) : const Icon(Icons.music_note, size: 100), ), ), ); } } ================================================ FILE: lib/widgets/player/player_progress.dart ================================================ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; class PlayerProgress extends StatelessWidget { const PlayerProgress({super.key}); String _formatDuration(Duration? duration) { if (duration == null) return '--:--'; String twoDigits(int n) => n.toString().padLeft(2, '0'); String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); return '$twoDigitMinutes:$twoDigitSeconds'; } double _ensureValueInRange(double value, double min, double max) { if (value < min) return min; if (value > max) return max; return value; } @override Widget build(BuildContext context) { final viewModel = GetIt.I(); return ListenableBuilder( listenable: viewModel, builder: (context, _) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Column( children: [ SliderTheme( data: SliderTheme.of(context).copyWith( trackHeight: 2, thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 6, ), ), child: Slider( value: _ensureValueInRange( viewModel.position?.inMilliseconds.toDouble() ?? 0, 0, viewModel.duration?.inMilliseconds.toDouble() ?? 1 ), min: 0, max: viewModel.duration?.inMilliseconds.toDouble() ?? 1, onChanged: (value) { viewModel.seek(Duration(milliseconds: value.round())); }, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _formatDuration(viewModel.position), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), ), Text( _formatDuration(viewModel.duration), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), ), ], ), ), ], ), ); }, ); } } ================================================ FILE: lib/widgets/player/player_seek_controls.dart ================================================ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; class PlayerSeekControls extends StatelessWidget { const PlayerSeekControls({super.key}); @override Widget build(BuildContext context) { final viewModel = GetIt.I(); return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // 后退30s IconButton( icon: const Icon(Icons.replay_30), iconSize: 24, onPressed: () { final position = viewModel.position; if (position != null) { viewModel.seek(position - const Duration(seconds: 30)); } }, ), // 后退5s IconButton( icon: const Icon(Icons.replay_5), iconSize: 24, onPressed: () { final position = viewModel.position; if (position != null) { viewModel.seek(position - const Duration(seconds: 5)); } }, ), // 上一句歌词 IconButton( icon: const Icon(Icons.skip_previous), iconSize: 24, onPressed: () => viewModel.seekToPreviousLyric(), ), // 下一句歌词 IconButton( icon: const Icon(Icons.skip_next), iconSize: 24, onPressed: () => viewModel.seekToNextLyric(), ), // 快进5s IconButton( icon: const Icon(Icons.forward_5), iconSize: 24, onPressed: () { final position = viewModel.position; if (position != null) { viewModel.seek(position + const Duration(seconds: 5)); } }, ), // 快进30s IconButton( icon: const Icon(Icons.forward_30), iconSize: 24, onPressed: () { final position = viewModel.position; if (position != null) { viewModel.seek(position + const Duration(seconds: 30)); } }, ), ], ); } } ================================================ FILE: lib/widgets/player/player_work_info.dart ================================================ import 'package:flutter/material.dart'; import 'package:marquee/marquee.dart'; import 'package:asmrapp/core/audio/models/playback_context.dart'; class PlayerWorkInfo extends StatelessWidget { final PlaybackContext? context; const PlayerWorkInfo({ super.key, required this.context, }); @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: Theme.of(context).textTheme.titleMedium!.fontSize! * 1.5, child: Marquee( text: this.context?.work.title ?? '未知作品', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), scrollAxis: Axis.horizontal, crossAxisAlignment: CrossAxisAlignment.start, blankSpace: 50.0, velocity: 30.0, pauseAfterRound: const Duration(seconds: 2), startPadding: 10.0, accelerationDuration: const Duration(seconds: 1), accelerationCurve: Curves.linear, decelerationDuration: const Duration(milliseconds: 500), decelerationCurve: Curves.easeOut, ), ), const SizedBox(height: 2), Text( this.context?.work.vas ?.map((va) => va['name'] as String?) .where((name) => name != null) .join('、') ?? '未知演员', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ); } } ================================================ FILE: lib/widgets/work_card/components/work_cover_image.dart ================================================ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:shimmer/shimmer.dart'; class WorkCoverImage extends StatelessWidget { final String imageUrl; final int workId; final String sourceId; // 195/146 ≈ 1.336 static const double _aspectRatio = 195 / 146; const WorkCoverImage({ super.key, required this.imageUrl, required this.workId, required this.sourceId, }); @override Widget build(BuildContext context) { return AspectRatio( aspectRatio: _aspectRatio, child: Stack( children: [ Hero( tag: 'work-cover-$workId', child: CachedNetworkImage( imageUrl: imageUrl, fit: BoxFit.cover, width: double.infinity, height: double.infinity, placeholder: (context, url) => Shimmer.fromColors( baseColor: Theme.of(context).colorScheme.surfaceContainerHighest, highlightColor: Theme.of(context).colorScheme.surface, child: Container( color: Colors.white, ), ), errorWidget: (context, url, error) => Container( color: Theme.of(context).colorScheme.errorContainer, child: Center( child: Icon( Icons.error_outline, color: Theme.of(context).colorScheme.error, ), ), ), ), ), Positioned( left: 8, top: 8, child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(4), ), child: Text( sourceId, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.white, fontSize: 12, ), ), ), ), ], ), ); } } ================================================ FILE: lib/widgets/work_card/components/work_footer.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; class WorkFooter extends StatelessWidget { final Work work; const WorkFooter({ super.key, required this.work, }); @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( work.release ?? '', style: Theme.of(context).textTheme.bodySmall?.copyWith( fontSize: 10, ), ), Text( '销量 ${work.dlCount ?? 0}', style: Theme.of(context).textTheme.bodySmall?.copyWith( fontSize: 10, ), ), ], ); } } ================================================ FILE: lib/widgets/work_card/components/work_info_section.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'work_title.dart'; import 'work_tags_panel.dart'; import 'work_footer.dart'; class WorkInfoSection extends StatelessWidget { final Work work; const WorkInfoSection({ super.key, required this.work, }); String _formatDuration(int? seconds) { if (seconds == null) return ''; final duration = Duration(seconds: seconds); final hours = duration.inHours; final minutes = duration.inMinutes.remainder(60); if (hours > 0) { return '${hours}h ${minutes}m'; } else { return '${minutes}m'; } } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ WorkTitle(work: work), const SizedBox(height: 4), Row( children: [ if (work.duration != null) ...[ Icon( Icons.access_time, size: 14, color: Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox(width: 4), Text( _formatDuration(work.duration), style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ], ), const SizedBox(height: 8), WorkTagsPanel(work: work), const SizedBox(height: 4), const Spacer(), WorkFooter(work: work), ], ), ); } } ================================================ FILE: lib/widgets/work_card/components/work_tags_panel.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/data/models/works/tag.dart'; class WorkTagsPanel extends StatelessWidget { final Work work; const WorkTagsPanel({ super.key, required this.work, }); String _getLocalizedTagName(Tag tag) { final zhName = tag.i18n?.zhCn?.name; if (zhName != null) return zhName; final jaName = tag.i18n?.jaJp?.name; if (jaName != null) return jaName; return tag.name ?? ''; } @override Widget build(BuildContext context) { return Wrap( spacing: 4, runSpacing: 2, children: [ if (work.circle?.name != null) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.orange.withOpacity(0.2), borderRadius: BorderRadius.circular(4), ), child: Text( work.circle?.name ?? '', style: TextStyle( fontSize: 10, color: Colors.orange[700], ), ), ), ...?work.vas?.map((va) => Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.green.withOpacity(0.2), borderRadius: BorderRadius.circular(4), ), child: Text( va['name'] ?? '', style: TextStyle( fontSize: 10, color: Colors.green[700], ), ), )), if (work.hasSubtitle == true) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.2), borderRadius: BorderRadius.circular(4), ), child: Text( '字幕', style: TextStyle( fontSize: 10, color: Colors.blue[700], ), ), ), ...work.tags ?.map((tag) => Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Theme.of(context) .colorScheme .surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: Text( _getLocalizedTagName(tag), style: TextStyle( fontSize: 10, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), )) .toList() ?? [], ], ); } } ================================================ FILE: lib/widgets/work_card/components/work_title.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; class WorkTitle extends StatelessWidget { final Work work; const WorkTitle({ super.key, required this.work, }); @override Widget build(BuildContext context) { return Text( work.title ?? '', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontSize: 14, ), ); } } ================================================ FILE: lib/widgets/work_card/work_card.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'components/work_cover_image.dart'; import 'components/work_info_section.dart'; class WorkCard extends StatelessWidget { final Work work; final VoidCallback? onTap; const WorkCard({ super.key, required this.work, this.onTap, }); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Card( clipBehavior: Clip.antiAlias, elevation: isDark ? 0 : 1, color: isDark ? Theme.of(context).colorScheme.surfaceVariant : Theme.of(context).colorScheme.surface, child: InkWell( onTap: onTap, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ WorkCoverImage( imageUrl: work.mainCoverUrl ?? '', workId: work.id ?? 0, sourceId: work.sourceId ?? '', ), Expanded( child: WorkInfoSection(work: work), ), ], ), ), ); } } ================================================ FILE: lib/widgets/work_grid/components/grid_content.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; import 'package:asmrapp/widgets/work_grid.dart'; import 'package:asmrapp/widgets/pagination_controls.dart'; import 'package:asmrapp/widgets/work_grid/models/grid_config.dart'; import 'package:asmrapp/screens/detail_screen.dart'; class GridContent extends StatelessWidget { final List works; final bool isLoading; final WorkLayoutStrategy layoutStrategy; final int? currentPage; final int? totalPages; final Future Function(int page)? onPageChanged; final ScrollController? scrollController; final GridConfig? config; const GridContent({ super.key, required this.works, required this.isLoading, required this.layoutStrategy, this.currentPage, this.totalPages, this.onPageChanged, this.scrollController, this.config, }); void _scrollToTop() { if (scrollController?.hasClients ?? false) { scrollController!.animateTo( 0, duration: config?.scrollDuration ?? const Duration(milliseconds: 300), curve: config?.scrollCurve ?? Curves.easeOut, ); } } @override Widget build(BuildContext context) { return CustomScrollView( controller: scrollController, physics: config?.physics, slivers: [ SliverPadding( padding: config?.padding ?? layoutStrategy.getPadding(context), sliver: WorkGrid( works: works, layoutStrategy: layoutStrategy, onWorkTap: (work) { Navigator.push( context, MaterialPageRoute( builder: (context) => DetailScreen(work: work), ), ); }, ), ), if (config?.enablePagination != false && currentPage != null && totalPages != null) SliverToBoxAdapter( child: PaginationControls( currentPage: currentPage!, totalPages: totalPages!, isLoading: isLoading, onPageChanged: (page) async { await onPageChanged?.call(page); if (!isLoading) { _scrollToTop(); } }, ), ), ], ); } } ================================================ FILE: lib/widgets/work_grid/components/grid_empty.dart ================================================ import 'package:flutter/material.dart'; class GridEmpty extends StatelessWidget { final String? message; final Widget? customWidget; const GridEmpty({ super.key, this.message, this.customWidget, }); @override Widget build(BuildContext context) { if (customWidget != null) { return customWidget!; } return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.inbox_outlined, size: 48, color: Theme.of(context).colorScheme.outline, ), const SizedBox(height: 16), Text( message ?? '暂无内容', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of(context).colorScheme.outline, ), ), ], ), ); } } ================================================ FILE: lib/widgets/work_grid/components/grid_error.dart ================================================ import 'package:flutter/material.dart'; class GridError extends StatelessWidget { final String error; final VoidCallback? onRetry; const GridError({ super.key, required this.error, this.onRetry, }); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline, size: 48, color: Theme.of(context).colorScheme.error, ), const SizedBox(height: 16), Text( error, style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), if (onRetry != null) ...[ const SizedBox(height: 16), FilledButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh), label: const Text('重试'), ), ], ], ), ); } } ================================================ FILE: lib/widgets/work_grid/components/grid_loading.dart ================================================ import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; class GridLoading extends StatelessWidget { const GridLoading({super.key}); @override Widget build(BuildContext context) { return Shimmer.fromColors( baseColor: Theme.of(context).colorScheme.surfaceContainerHighest, highlightColor: Theme.of(context).colorScheme.surface, child: GridView.builder( padding: const EdgeInsets.all(16), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 0.75, crossAxisSpacing: 16, mainAxisSpacing: 16, ), itemCount: 6, itemBuilder: (context, index) { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), ), ); }, ), ); } } ================================================ FILE: lib/widgets/work_grid/enhanced_work_grid_view.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; import 'package:asmrapp/widgets/work_grid/components/grid_content.dart'; import 'package:asmrapp/widgets/work_grid/components/grid_error.dart'; import 'package:asmrapp/widgets/work_grid/components/grid_empty.dart'; import 'package:asmrapp/widgets/work_grid/components/grid_loading.dart'; import 'package:asmrapp/widgets/work_grid/models/grid_config.dart'; class EnhancedWorkGridView extends StatelessWidget { final List works; final bool isLoading; final String? error; final VoidCallback? onRetry; final Future Function()? onRefresh; final Future Function(int page)? onPageChanged; final int? currentPage; final int? totalPages; final String? emptyMessage; final Widget? customEmptyWidget; final WorkLayoutStrategy layoutStrategy; final ScrollController? scrollController; final GridConfig? config; const EnhancedWorkGridView({ super.key, required this.works, required this.isLoading, this.error, this.onRetry, this.onRefresh, this.onPageChanged, this.currentPage, this.totalPages, this.emptyMessage, this.customEmptyWidget, this.layoutStrategy = const WorkLayoutStrategy(), this.scrollController, this.config, }); @override Widget build(BuildContext context) { if (isLoading && works.isEmpty) { return const GridLoading(); } if (error != null) { return GridError( error: error!, onRetry: onRetry, ); } if (works.isEmpty) { return GridEmpty( message: emptyMessage, customWidget: customEmptyWidget, ); } Widget content = GridContent( works: works, isLoading: isLoading, layoutStrategy: layoutStrategy, currentPage: currentPage, totalPages: totalPages, onPageChanged: onPageChanged, scrollController: scrollController, config: config, ); if (onRefresh != null) { content = RefreshIndicator( onRefresh: onRefresh!, child: content, ); } return content; } } ================================================ FILE: lib/widgets/work_grid/models/grid_config.dart ================================================ import 'package:flutter/material.dart'; class GridConfig { final ScrollPhysics? physics; final bool enablePagination; final bool showLoadingOnEmpty; final Duration scrollDuration; final Curve scrollCurve; final EdgeInsets? padding; const GridConfig({ this.physics, this.enablePagination = true, this.showLoadingOnEmpty = true, this.scrollDuration = const Duration(milliseconds: 300), this.scrollCurve = Curves.easeOut, this.padding, }); static const GridConfig defaultConfig = GridConfig(); } ================================================ FILE: lib/widgets/work_grid.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/widgets/work_row.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; class WorkGrid extends StatelessWidget { final List works; final void Function(Work work)? onWorkTap; final WorkLayoutStrategy layoutStrategy; const WorkGrid({ super.key, required this.works, this.onWorkTap, this.layoutStrategy = const WorkLayoutStrategy(), }); @override Widget build(BuildContext context) { final columnsCount = layoutStrategy.getColumnsCount(context); final rows = layoutStrategy.groupWorksIntoRows(works, columnsCount); final rowSpacing = layoutStrategy.getRowSpacing(context); final columnSpacing = layoutStrategy.getColumnSpacing(context); return SliverList( delegate: SliverChildBuilderDelegate( (context, index) { if (index >= rows.length) return null; return Padding( padding: EdgeInsets.only( bottom: index < rows.length - 1 ? rowSpacing : 0), child: WorkRow( works: rows[index], onWorkTap: onWorkTap, spacing: columnSpacing, ), ); }, childCount: rows.length, ), ); } } ================================================ FILE: lib/widgets/work_grid_view.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/widgets/work_grid.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; import 'package:asmrapp/screens/detail_screen.dart'; class WorkGridView extends StatelessWidget { final List works; final bool isLoading; final String? error; final VoidCallback? onRetry; final String? emptyMessage; final Widget? customEmptyWidget; final WorkLayoutStrategy layoutStrategy; final ScrollController? scrollController; final Widget? bottomWidget; const WorkGridView({ super.key, required this.works, required this.isLoading, this.error, this.onRetry, this.emptyMessage, this.customEmptyWidget, this.layoutStrategy = const WorkLayoutStrategy(), this.scrollController, this.bottomWidget, }); @override Widget build(BuildContext context) { if (isLoading) { return const Center( child: CircularProgressIndicator(), ); } if (error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(error!), if (onRetry != null) ...[ const SizedBox(height: 16), ElevatedButton( onPressed: onRetry, child: const Text('重试'), ), ], ], ), ); } if (works.isEmpty) { if (customEmptyWidget != null) { return customEmptyWidget!; } if (emptyMessage != null) { return Center( child: Text( emptyMessage!, style: Theme.of(context).textTheme.bodyLarge, ), ); } return const SizedBox.shrink(); } return CustomScrollView( controller: scrollController, slivers: [ SliverPadding( padding: layoutStrategy.getPadding(context), sliver: WorkGrid( works: works, layoutStrategy: layoutStrategy, onWorkTap: (work) { Navigator.push( context, MaterialPageRoute( builder: (context) => DetailScreen(work: work), ), ); }, ), ), if (bottomWidget != null) SliverToBoxAdapter( child: bottomWidget!, ), ], ); } } ================================================ FILE: lib/widgets/work_row.dart ================================================ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/widgets/work_card/work_card.dart'; class WorkRow extends StatelessWidget { final List works; final void Function(Work work)? onWorkTap; final double spacing; const WorkRow({ super.key, required this.works, this.onWorkTap, this.spacing = 8.0, }); @override Widget build(BuildContext context) { return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 第一个卡片 Expanded( child: works.isNotEmpty ? WorkCard( work: works[0], onTap: onWorkTap != null ? () => onWorkTap!(works[0]) : null, ) : const SizedBox.shrink(), ), SizedBox(width: spacing), // 第二个卡片或占位符 Expanded( child: works.length > 1 ? WorkCard( work: works[1], onTap: onWorkTap != null ? () => onWorkTap!(works[1]) : null, ) : const SizedBox.shrink(), // 空占位符,保持两列布局 ), ], ), ); } } ================================================ FILE: linux/.gitignore ================================================ flutter/ephemeral ================================================ FILE: linux/CMakeLists.txt ================================================ # Project-level configuration. cmake_minimum_required(VERSION 3.10) project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "asmrapp") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "com.example.asmrapp") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) # Load bundled libraries from the lib/ directory relative to the binary. set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") # Root filesystem for cross-building. if(FLUTTER_TARGET_PLATFORM_SYSROOT) set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) endif() # Define build configuration options. if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() # Compilation settings that should be applied to most targets. # # Be cautious about adding new options here, as plugins use this function by # default. In most cases, you should add new options to specific targets instead # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_14) target_compile_options(${TARGET} PRIVATE -Wall -Werror) target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") endfunction() # Flutter library and tool build rules. set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") # Define the application target. To change its name, change BINARY_NAME above, # not the value here, or `flutter run` will no longer work. # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} "main.cc" "my_application.cc" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" ) # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) # Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) # Only the install-generated bundle's copy of the executable will launch # correctly, since the resources must in the right relative locations. To avoid # people trying to run the unbundled copy, put it in a subdirectory instead of # the default top-level location. set_target_properties(${BINARY_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" ) # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # By default, "installing" just makes a relocatable bundle in the build # directory. set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() # Start with a clean build bundle directory every time. install(CODE " file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") " COMPONENT Runtime) set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) install(FILES "${bundled_library}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endforeach(bundled_library) # Copy the native assets provided by the build.dart from all packages. set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") install(DIRECTORY "${NATIVE_ASSETS_DIR}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() ================================================ FILE: linux/flutter/CMakeLists.txt ================================================ # This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.10) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. # Serves the same purpose as list(TRANSFORM ... PREPEND ...), # which isn't available in 3.10. function(list_prepend LIST_NAME PREFIX) set(NEW_LIST "") foreach(element ${${LIST_NAME}}) list(APPEND NEW_LIST "${PREFIX}${element}") endforeach(element) set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) endfunction() # === Flutter Library === # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "fl_basic_message_channel.h" "fl_binary_codec.h" "fl_binary_messenger.h" "fl_dart_project.h" "fl_engine.h" "fl_json_message_codec.h" "fl_json_method_codec.h" "fl_message_codec.h" "fl_method_call.h" "fl_method_channel.h" "fl_method_codec.h" "fl_method_response.h" "fl_plugin_registrar.h" "fl_plugin_registry.h" "fl_standard_message_codec.h" "fl_standard_method_codec.h" "fl_string_codec.h" "fl_value.h" "fl_view.h" "flutter_linux.h" ) list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") target_link_libraries(flutter INTERFACE PkgConfig::GTK PkgConfig::GLIB PkgConfig::GIO ) add_dependencies(flutter flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/_phony_ COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ) ================================================ FILE: linux/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" void fl_register_plugins(FlPluginRegistry* registry) { } ================================================ FILE: linux/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void fl_register_plugins(FlPluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: linux/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: linux/main.cc ================================================ #include "my_application.h" int main(int argc, char** argv) { g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } ================================================ FILE: linux/my_application.cc ================================================ #include "my_application.h" #include #ifdef GDK_WINDOWING_X11 #include #endif #include "flutter/generated_plugin_registrant.h" struct _MyApplication { GtkApplication parent_instance; char** dart_entrypoint_arguments; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); // Use a header bar when running in GNOME as this is the common style used // by applications and is the setup most users will be using (e.g. Ubuntu // desktop). // If running on X and not using GNOME then just use a traditional title bar // in case the window manager does more exotic layout, e.g. tiling. // If running on Wayland assume the header bar will work (may need changing // if future cases occur). gboolean use_header_bar = TRUE; #ifdef GDK_WINDOWING_X11 GdkScreen* screen = gtk_window_get_screen(window); if (GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); if (g_strcmp0(wm_name, "GNOME Shell") != 0) { use_header_bar = FALSE; } } #endif if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); gtk_header_bar_set_title(header_bar, "asmrapp"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { gtk_window_set_title(window, "asmrapp"); } gtk_window_set_default_size(window, 1280, 720); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); } // Implements GApplication::local_command_line. static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { MyApplication* self = MY_APPLICATION(application); // Strip out the first argument as it is the binary name. self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; if (!g_application_register(application, nullptr, &error)) { g_warning("Failed to register: %s", error->message); *exit_status = 1; return TRUE; } g_application_activate(application); *exit_status = 0; return TRUE; } // Implements GApplication::startup. static void my_application_startup(GApplication* application) { //MyApplication* self = MY_APPLICATION(object); // Perform any actions required at application startup. G_APPLICATION_CLASS(my_application_parent_class)->startup(application); } // Implements GApplication::shutdown. static void my_application_shutdown(GApplication* application) { //MyApplication* self = MY_APPLICATION(object); // Perform any actions required at application shutdown. G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); } // Implements GObject::dispose. static void my_application_dispose(GObject* object) { MyApplication* self = MY_APPLICATION(object); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } static void my_application_class_init(MyApplicationClass* klass) { G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; G_APPLICATION_CLASS(klass)->startup = my_application_startup; G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, "flags", G_APPLICATION_NON_UNIQUE, nullptr)); } ================================================ FILE: linux/my_application.h ================================================ #ifndef FLUTTER_MY_APPLICATION_H_ #define FLUTTER_MY_APPLICATION_H_ #include G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, GtkApplication) /** * my_application_new: * * Creates a new Flutter-based application. * * Returns: a new #MyApplication. */ MyApplication* my_application_new(); #endif // FLUTTER_MY_APPLICATION_H_ ================================================ FILE: macos/.gitignore ================================================ # Flutter-related **/Flutter/ephemeral/ **/Pods/ # Xcode-related **/dgph **/xcuserdata/ ================================================ FILE: macos/Flutter/Flutter-Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: macos/Flutter/Flutter-Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: macos/Flutter/GeneratedPluginRegistrant.swift ================================================ // // Generated file. Do not edit. // import FlutterMacOS import Foundation import audio_service import audio_session import just_audio import package_info_plus import path_provider_foundation import shared_preferences_foundation import sqflite_darwin import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } ================================================ FILE: macos/Podfile ================================================ platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_macos_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) end end ================================================ FILE: macos/Runner/AppDelegate.swift ================================================ import Cocoa import FlutterMacOS @main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } } ================================================ FILE: macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "16x16", "idiom" : "mac", "filename" : "app_icon_16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", "filename" : "app_icon_32.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "app_icon_32.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "app_icon_64.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "app_icon_128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "app_icon_256.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "app_icon_256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "app_icon_512.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "app_icon_512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "app_icon_1024.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: macos/Runner/Base.lproj/MainMenu.xib ================================================ ================================================ FILE: macos/Runner/Configs/AppInfo.xcconfig ================================================ // Application-level settings for the Runner target. // // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the // future. If not, the values below would default to using the project name when this becomes a // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. PRODUCT_NAME = asmrapp // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.example.asmrapp // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. ================================================ FILE: macos/Runner/Configs/Debug.xcconfig ================================================ #include "../../Flutter/Flutter-Debug.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: macos/Runner/Configs/Release.xcconfig ================================================ #include "../../Flutter/Flutter-Release.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: macos/Runner/Configs/Warnings.xcconfig ================================================ WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings GCC_WARN_UNDECLARED_SELECTOR = YES CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE CLANG_WARN__DUPLICATE_METHOD_MATCH = YES CLANG_WARN_PRAGMA_PACK = YES CLANG_WARN_STRICT_PROTOTYPES = YES CLANG_WARN_COMMA = YES GCC_WARN_STRICT_SELECTOR_MATCH = YES CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES GCC_WARN_SHADOW = YES CLANG_WARN_UNREACHABLE_CODE = YES ================================================ FILE: macos/Runner/DebugProfile.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.cs.allow-jit com.apple.security.network.server ================================================ FILE: macos/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu NSPrincipalClass NSApplication ================================================ FILE: macos/Runner/MainFlutterWindow.swift ================================================ import Cocoa import FlutterMacOS class MainFlutterWindow: NSWindow { override func awakeFromNib() { let flutterViewController = FlutterViewController() let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) RegisterGeneratedPlugins(registry: flutterViewController) super.awakeFromNib() } } ================================================ FILE: macos/Runner/Release.entitlements ================================================ com.apple.security.app-sandbox ================================================ FILE: macos/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { isa = PBXAggregateTarget; buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; buildPhases = ( 33CC111E2044C6BF0003C045 /* ShellScript */, ); dependencies = ( ); name = "Flutter Assemble"; productName = FLX; }; /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC10EC2044A3C60003C045; remoteInfo = Runner; }; 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 33CC110E2044A8840003C045 /* Bundle Framework */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* asmrapp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "asmrapp.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 331C80D2294CF70F00263BE5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C80D7294CF71000263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( 33E5194F232828860026EE4D /* AppInfo.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, ); path = Configs; sourceTree = ""; }; 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* asmrapp.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; }; 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, ); name = Resources; path = ..; sourceTree = ""; }; 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, ); path = Flutter; sourceTree = ""; }; 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, 33CC11242044D66E0003C045 /* Resources */, 33BA886A226E78AF003329D5 /* Configs */, ); path = Runner; sourceTree = ""; }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C80D4294CF70F00263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, ); buildRules = ( ); dependencies = ( 331C80DA294CF71000263BE5 /* PBXTargetDependency */, ); name = RunnerTests; productName = RunnerTests; productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 33CC10EC2044A3C60003C045 /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, ); buildRules = ( ); dependencies = ( 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* asmrapp.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 33CC10EC2044A3C60003C045; }; 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; }; }; }; 33CC111A2044C6BA0003C045 = { CreatedOnToolsVersion = 9.2; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 33CC10E42044A3C60003C045; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, 331C80D4294CF70F00263BE5 /* RunnerTests */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 331C80D3294CF70F00263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( Flutter/ephemeral/FlutterInputs.xcfilelist, ); inputPaths = ( Flutter/ephemeral/tripwire, ); outputFileListPaths = ( Flutter/ephemeral/FlutterOutputs.xcfilelist, ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 331C80D1294CF70F00263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10E92044A3C60003C045 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC10EC2044A3C60003C045 /* Runner */; targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; }; 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( 33CC10F52044A3C60003C045 /* Base */, ); name = MainMenu.xib; path = Runner; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.asmrapp.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/asmrapp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/asmrapp"; }; name = Debug; }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.asmrapp.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/asmrapp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/asmrapp"; }; name = Release; }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.asmrapp.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/asmrapp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/asmrapp"; }; name = Profile; }; 338D0CE9231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 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_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 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_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Profile; }; 338D0CEA231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Profile; }; 338D0CEB231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Profile; }; 33CC10F92044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 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_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 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_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 33CC10FA2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 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_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 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_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 33CC10FC2044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 33CC10FD2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; 33CC111C2044C6BA0003C045 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 33CC111D2044C6BA0003C045 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C80DB294CF71000263BE5 /* Debug */, 331C80DC294CF71000263BE5 /* Release */, 331C80DD294CF71000263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10F92044A3C60003C045 /* Debug */, 33CC10FA2044A3C60003C045 /* Release */, 338D0CE9231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10FC2044A3C60003C045 /* Debug */, 33CC10FD2044A3C60003C045 /* Release */, 338D0CEA231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC111C2044C6BA0003C045 /* Debug */, 33CC111D2044C6BA0003C045 /* Release */, 338D0CEB231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } ================================================ FILE: macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: macos/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: macos/RunnerTests/RunnerTests.swift ================================================ import Cocoa import FlutterMacOS 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: pubspec.yaml ================================================ name: asmrapp description: "asmr one third party app." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. version: 1.1.11 environment: sdk: '>=3.2.3 <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter freezed_annotation: ^2.4.1 json_annotation: ^4.9.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 provider: ^6.1.1 dio: ^5.4.0 cached_network_image: ^3.3.0 logger: ^2.5.0 shimmer: ^3.0.0 just_audio: ^0.9.36 audio_session: ^0.1.18 get_it: ^8.0.2 audio_service: ^0.18.12 rxdart: ^0.28.0 path_provider: ^2.1.5 crypto: ^3.0.6 shared_preferences: ^2.2.2 flutter_cache_manager: ^3.4.1 permission_handler: ^11.3.1 scrollable_positioned_list: ^0.3.8 marquee: ^2.3.0 wakelock_plus: ^1.2.8 dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.4.7 freezed: ^2.4.6 json_serializable: ^6.7.1 flutter_launcher_icons: ^0.13.1 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images # For details regarding adding assets from package dependencies, see # https://flutter.dev/to/asset-from-package # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package flutter_launcher_icons: android: "ic_launcher" ios: true image_path: "assets/icon/icon.png" min_sdk_android: 21 dependency_overrides: path: ^1.9.0 # 强制使用更高版本的 path 包 ================================================ FILE: test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility in the flutter_test package. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:asmrapp/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(const MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Tap the '+' icon and trigger a frame. await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify that our counter has incremented. expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); } ================================================ FILE: web/index.html ================================================ asmrapp ================================================ FILE: web/manifest.json ================================================ { "name": "Yuro", "short_name": "Yuro", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: windows/.gitignore ================================================ flutter/ephemeral/ # Visual Studio user-specific files. *.suo *.user *.userosscache *.sln.docstates # Visual Studio build-related files. x64/ x86/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ ================================================ FILE: windows/CMakeLists.txt ================================================ # Project-level configuration. cmake_minimum_required(VERSION 3.14) project(asmrapp LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "asmrapp") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(VERSION 3.14...3.25) # Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" CACHE STRING "" FORCE) else() if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() endif() # Define settings for the Profile build mode. set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") # Use Unicode for all projects. add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. # # Be cautious about adding new options here, as plugins use this function by # default. In most cases, you should add new options to specific targets instead # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() # Flutter library and tool build rules. set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # Support files are copied into place next to the executable, so that it can # run in place. This is done instead of making a separate bundle (as on Linux) # so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") # Make the "install" step default, as it's required to run. set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() # Copy the native assets provided by the build.dart from all packages. set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") install(DIRECTORY "${NATIVE_ASSETS_DIR}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) ================================================ FILE: windows/flutter/CMakeLists.txt ================================================ # This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") # Set fallback configurations for older versions of the flutter tool. if (NOT DEFINED FLUTTER_TARGET_PLATFORM) set(FLUTTER_TARGET_PLATFORM "windows-x64") endif() # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "flutter_export.h" "flutter_windows.h" "flutter_messenger.h" "flutter_plugin_registrar.h" "flutter_texture_registrar.h" ) list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") add_dependencies(flutter flutter_assemble) # === Wrapper === list(APPEND CPP_WRAPPER_SOURCES_CORE "core_implementations.cc" "standard_codec.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_PLUGIN "plugin_registrar.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_APP "flutter_engine.cc" "flutter_view_controller.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") # Wrapper sources needed for a plugin. add_library(flutter_wrapper_plugin STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ) apply_standard_settings(flutter_wrapper_plugin) set_target_properties(flutter_wrapper_plugin PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(flutter_wrapper_plugin PROPERTIES CXX_VISIBILITY_PRESET hidden) target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) target_include_directories(flutter_wrapper_plugin PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_plugin flutter_assemble) # Wrapper sources needed for the runner. add_library(flutter_wrapper_app STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_APP} ) apply_standard_settings(flutter_wrapper_app) target_link_libraries(flutter_wrapper_app PUBLIC flutter) target_include_directories(flutter_wrapper_app PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ${PHONY_OUTPUT} COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ) ================================================ FILE: windows/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" #include void RegisterPlugins(flutter::PluginRegistry* registry) { PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); } ================================================ FILE: windows/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void RegisterPlugins(flutter::PluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: windows/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST permission_handler_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: windows/runner/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.14) project(runner LANGUAGES CXX) # Define the application target. To change its name, change BINARY_NAME in the # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer # work. # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" "utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) # Add preprocessor definitions for the build version. target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") # Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # Add dependency libraries and include directories. Add any application-specific # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) ================================================ FILE: windows/runner/Runner.rc ================================================ // Microsoft Visual C++ generated resource script. // #pragma code_page(65001) #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (United States) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APP_ICON ICON "resources\\app_icon.ico" ///////////////////////////////////////////////////////////////////////////// // // Version // #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else #define VERSION_AS_NUMBER 1,0,0,0 #endif #if defined(FLUTTER_VERSION) #define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif VS_VERSION_INFO VERSIONINFO FILEVERSION VERSION_AS_NUMBER PRODUCTVERSION VERSION_AS_NUMBER FILEFLAGSMASK VS_FFI_FILEFLAGSMASK #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG #else FILEFLAGS 0x0L #endif FILEOS VOS__WINDOWS32 FILETYPE VFT_APP FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" VALUE "FileDescription", "asmrapp" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "asmrapp" "\0" VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" VALUE "OriginalFilename", "asmrapp.exe" "\0" VALUE "ProductName", "asmrapp" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1252 END END #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED ================================================ FILE: windows/runner/flutter_window.cpp ================================================ #include "flutter_window.h" #include #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} FlutterWindow::~FlutterWindow() {} bool FlutterWindow::OnCreate() { if (!Win32Window::OnCreate()) { return false; } RECT frame = GetClientArea(); // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); flutter_controller_->engine()->SetNextFrameCallback([&]() { this->Show(); }); // Flutter can complete the first frame before the "show window" callback is // registered. The following call ensures a frame is pending to ensure the // window is shown. It is a no-op if the first frame hasn't completed yet. flutter_controller_->ForceRedraw(); return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { flutter_controller_ = nullptr; } Win32Window::OnDestroy(); } LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); if (result) { return *result; } } switch (message) { case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); } ================================================ FILE: windows/runner/flutter_window.h ================================================ #ifndef RUNNER_FLUTTER_WINDOW_H_ #define RUNNER_FLUTTER_WINDOW_H_ #include #include #include #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: // Creates a new FlutterWindow hosting a Flutter view running |project|. explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: // Win32Window: bool OnCreate() override; void OnDestroy() override; LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; private: // The project to run. flutter::DartProject project_; // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; }; #endif // RUNNER_FLUTTER_WINDOW_H_ ================================================ FILE: windows/runner/main.cpp ================================================ #include #include #include #include "flutter_window.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { CreateAndAttachConsole(); } // Initialize COM, so that it is available for use in the library and/or // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); flutter::DartProject project(L"data"); std::vector command_line_arguments = GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); if (!window.Create(L"asmrapp", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; while (::GetMessage(&msg, nullptr, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } ::CoUninitialize(); return EXIT_SUCCESS; } ================================================ FILE: windows/runner/resource.h ================================================ //{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by Runner.rc // #define IDI_APP_ICON 101 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif ================================================ FILE: windows/runner/runner.exe.manifest ================================================ PerMonitorV2 ================================================ FILE: windows/runner/utils.cpp ================================================ #include "utils.h" #include #include #include #include #include void CreateAndAttachConsole() { if (::AllocConsole()) { FILE *unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } if (freopen_s(&unused, "CONOUT$", "w", stderr)) { _dup2(_fileno(stdout), 2); } std::ios::sync_with_stdio(); FlutterDesktopResyncOutputStreams(); } } std::vector GetCommandLineArguments() { // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. int argc; wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); if (argv == nullptr) { return std::vector(); } std::vector command_line_arguments; // Skip the first argument as it's the binary name. for (int i = 1; i < argc; i++) { command_line_arguments.push_back(Utf8FromUtf16(argv[i])); } ::LocalFree(argv); return command_line_arguments; } std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } unsigned int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr) -1; // remove the trailing null character int input_length = (int)wcslen(utf16_string); std::string utf8_string; if (target_length == 0 || target_length > utf8_string.max_size()) { return utf8_string; } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } return utf8_string; } ================================================ FILE: windows/runner/utils.h ================================================ #ifndef RUNNER_UTILS_H_ #define RUNNER_UTILS_H_ #include #include // Creates a console for the process, and redirects stdout and stderr to // it for both the runner and the Flutter library. void CreateAndAttachConsole(); // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string // encoded in UTF-8. Returns an empty std::string on failure. std::string Utf8FromUtf16(const wchar_t* utf16_string); // Gets the command line arguments passed in as a std::vector, // encoded in UTF-8. Returns an empty std::vector on failure. std::vector GetCommandLineArguments(); #endif // RUNNER_UTILS_H_ ================================================ FILE: windows/runner/win32_window.cpp ================================================ #include "win32_window.h" #include #include #include "resource.h" namespace { /// Window attribute that enables dark mode window decorations. /// /// Redefined in case the developer's machine has a Windows SDK older than /// version 10.0.22000.0. /// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; /// Registry key for app theme preference. /// /// A value of 0 indicates apps should use dark mode. A non-zero or missing /// value indicates apps should use light mode. constexpr const wchar_t kGetPreferredBrightnessRegKey[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); // Scale helper to convert logical scaler values to physical using passed in // scale factor int Scale(int source, double scale_factor) { return static_cast(source * scale_factor); } // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. // This API is only needed for PerMonitor V1 awareness mode. void EnableFullDpiSupportIfAvailable(HWND hwnd) { HMODULE user32_module = LoadLibraryA("User32.dll"); if (!user32_module) { return; } auto enable_non_client_dpi_scaling = reinterpret_cast( GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); } FreeLibrary(user32_module); } } // namespace // Manages the Win32Window's window class registration. class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); } return instance_; } // Returns the name of the window class, registering the class if it hasn't // previously been registered. const wchar_t* GetWindowClass(); // Unregisters the window class. Should only be called if there are no // instances of the window. void UnregisterWindowClass(); private: WindowClassRegistrar() = default; static WindowClassRegistrar* instance_; bool class_registered_ = false; }; WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; const wchar_t* WindowClassRegistrar::GetWindowClass() { if (!class_registered_) { WNDCLASS window_class{}; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; window_class.style = CS_HREDRAW | CS_VREDRAW; window_class.cbClsExtra = 0; window_class.cbWndExtra = 0; window_class.hInstance = GetModuleHandle(nullptr); window_class.hIcon = LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); window_class.hbrBackground = 0; window_class.lpszMenuName = nullptr; window_class.lpfnWndProc = Win32Window::WndProc; RegisterClass(&window_class); class_registered_ = true; } return kWindowClassName; } void WindowClassRegistrar::UnregisterWindowClass() { UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; } Win32Window::Win32Window() { ++g_active_window_count; } Win32Window::~Win32Window() { --g_active_window_count; Destroy(); } bool Win32Window::Create(const std::wstring& title, const Point& origin, const Size& size) { Destroy(); const wchar_t* window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), static_cast(origin.y)}; HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); if (!window) { return false; } UpdateTheme(window); return OnCreate(); } bool Win32Window::Show() { return ShowWindow(window_handle_, SW_SHOWNORMAL); } // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { if (message == WM_NCCREATE) { auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; } else if (Win32Window* that = GetThisFromHandle(window)) { return that->MessageHandler(window, message, wparam, lparam); } return DefWindowProc(window, message, wparam, lparam); } LRESULT Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { switch (message) { case WM_DESTROY: window_handle_ = nullptr; Destroy(); if (quit_on_close_) { PostQuitMessage(0); } return 0; case WM_DPICHANGED: { auto newRectSize = reinterpret_cast(lparam); LONG newWidth = newRectSize->right - newRectSize->left; LONG newHeight = newRectSize->bottom - newRectSize->top; SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, newHeight, SWP_NOZORDER | SWP_NOACTIVATE); return 0; } case WM_SIZE: { RECT rect = GetClientArea(); if (child_content_ != nullptr) { // Size and position the child window. MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE); } return 0; } case WM_ACTIVATE: if (child_content_ != nullptr) { SetFocus(child_content_); } return 0; case WM_DWMCOLORIZATIONCOLORCHANGED: UpdateTheme(hwnd); return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); } void Win32Window::Destroy() { OnDestroy(); if (window_handle_) { DestroyWindow(window_handle_); window_handle_ = nullptr; } if (g_active_window_count == 0) { WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); } } Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } void Win32Window::SetChildContent(HWND content) { child_content_ = content; SetParent(content, window_handle_); RECT frame = GetClientArea(); MoveWindow(content, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, true); SetFocus(child_content_); } RECT Win32Window::GetClientArea() { RECT frame; GetClientRect(window_handle_, &frame); return frame; } HWND Win32Window::GetHandle() { return window_handle_; } void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; } bool Win32Window::OnCreate() { // No-op; provided for subclasses. return true; } void Win32Window::OnDestroy() { // No-op; provided for subclasses. } void Win32Window::UpdateTheme(HWND const window) { DWORD light_mode; DWORD light_mode_size = sizeof(light_mode); LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr, &light_mode, &light_mode_size); if (result == ERROR_SUCCESS) { BOOL enable_dark_mode = light_mode == 0; DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, &enable_dark_mode, sizeof(enable_dark_mode)); } } ================================================ FILE: windows/runner/win32_window.h ================================================ #ifndef RUNNER_WIN32_WINDOW_H_ #define RUNNER_WIN32_WINDOW_H_ #include #include #include #include // A class abstraction for a high DPI-aware Win32 Window. Intended to be // inherited from by classes that wish to specialize with custom // rendering and input handling class Win32Window { public: struct Point { unsigned int x; unsigned int y; Point(unsigned int x, unsigned int y) : x(x), y(y) {} }; struct Size { unsigned int width; unsigned int height; Size(unsigned int width, unsigned int height) : width(width), height(height) {} }; Win32Window(); virtual ~Win32Window(); // Creates a win32 window with |title| that is positioned and sized using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a // consistent size this function will scale the inputted width and height as // as appropriate for the default monitor. The window is invisible until // |Show| is called. Returns true if the window was created successfully. bool Create(const std::wstring& title, const Point& origin, const Size& size); // Show the current window. Returns true if the window was successfully shown. bool Show(); // Release OS resources associated with window. void Destroy(); // Inserts |content| into the window tree. void SetChildContent(HWND content); // Returns the backing Window handle to enable clients to set icon and other // window properties. Returns nullptr if the window has been destroyed. HWND GetHandle(); // If true, closing this window will quit the application. void SetQuitOnClose(bool quit_on_close); // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. virtual LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Called when CreateAndShow is called, allowing subclass window-related // setup. Subclasses should return false if setup fails. virtual bool OnCreate(); // Called when Destroy is called. virtual void OnDestroy(); private: friend class WindowClassRegistrar; // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Retrieves a class instance pointer for |window| static Win32Window* GetThisFromHandle(HWND const window) noexcept; // Update the window frame's theme to match the system theme. static void UpdateTheme(HWND const window); bool quit_on_close_ = false; // window handle for top level window. HWND window_handle_ = nullptr; // window handle for hosted content. HWND child_content_ = nullptr; }; #endif // RUNNER_WIN32_WINDOW_H_