├── .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-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── layout │ │ │ └── lyric_overlay.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ ├── values │ │ │ └── styles.xml │ │ │ └── xml │ │ │ └── network_security_config.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets └── icon │ └── icon.png ├── 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 ├── Podfile.lock ├── 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 ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-50x50@1x.png │ │ │ ├── Icon-App-50x50@2x.png │ │ │ ├── Icon-App-57x57@1x.png │ │ │ ├── Icon-App-57x57@2x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-72x72@1x.png │ │ │ ├── Icon-App-72x72@2x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h └── 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.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_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.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── app_icon_1024.png │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ └── app_icon_64.png │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements └── RunnerTests │ └── RunnerTests.swift ├── pubspec.lock ├── pubspec.yaml ├── test └── widget_test.dart ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── 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 ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Symbolication related 35 | app.*.symbols 36 | 37 | # Obfuscation related 38 | app.*.map.json 39 | 40 | # Android Studio will place build artifacts here 41 | /android/app/debug 42 | /android/app/profile 43 | /android/app/release 44 | 45 | # 添加以下内容 46 | **/android/key.properties 47 | **/android/app/upload-keystore.jks 48 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "2663184aa79047d0a33a14a3b607954f8fdd8730" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 17 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 18 | - platform: android 19 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 20 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 21 | - platform: ios 22 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 23 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 24 | - platform: linux 25 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 26 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 27 | - platform: macos 28 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 29 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 30 | - platform: web 31 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 32 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 33 | - platform: windows 34 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 35 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Creative Commons Attribution-NonCommercial-ShareAlike License (CC BY-NC-SA) 2 | 3 | ## License Summary 4 | 5 | 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. 6 | 7 | ## Full License 8 | 9 | ### 1. You are free to: 10 | 11 | - Share — copy and redistribute the material in any medium or format. 12 | - Adapt — remix, transform, and build upon the material. 13 | 14 | ### 2. Under the following terms: 15 | 16 | - 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. 17 | - NonCommercial — You may not use the material for commercial purposes. 18 | - ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original. 19 | 20 | ### 3. No additional restrictions: 21 | 22 | You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. 23 | 24 | ### 4. Notices: 25 | 26 | 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. 27 | 28 | ### 5. Other rights: 29 | 30 | In no way are any of the following rights affected by the license: 31 | 32 | - Your fair dealing or fair use rights; 33 | - The rights of others to use the material for their own purposes; 34 | - The rights of the licensor to use the material for their own purposes. 35 | 36 | ### 6. Disclaimer: 37 | 38 | This license does not grant you any rights to use the material in a way that would infringe on the rights of others. 39 | 40 | For more information, visit [Creative Commons](https://creativecommons.org/licenses/by-nc-sa/4.0/). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yuro 2 | 3 | [English](README_en.md) 4 | 5 | 一个使用 Flutter 构建的 ASMR.ONE 客户端。 6 | 7 | ## 项目概述 8 | 9 | Yuro 旨在通过精美的动画和现代化的用户界面,提供流畅愉悦的 ASMR 聆听体验。 10 | 11 | ## 特性 12 | 13 | - 稳定的后台播放,再也不用担心杀后台了 14 | - 精美的动画效果 15 | - 流畅的播放体验 16 | - 简洁的UI设计 17 | - 全方位的智能缓存机制 18 | - 图片智能缓存:优化封面加载速度,告别重复加载 19 | - 字幕本地缓存:实现快速字幕匹配与加载 20 | - 音频文件缓存:减少重复下载,节省流量开销 21 | - 为服务器减轻压力 22 | - 智能的缓存策略确保资源高效利用 23 | - 懒加载机制避免无效请求 24 | - 合理的缓存清理机制平衡本地存储 25 | 26 | ## 开发准则 27 | 28 | 我们维护了一套完整的开发准则以确保代码质量和一致性: 29 | - [开发准则](docs/guidelines_zh.md) 30 | 31 | ## 项目结构 32 | 33 |
34 | lib/
35 | ├── core/                 # 核心功能
36 | ├── data/                # 数据层
37 | ├── domain/              # 领域层
38 | ├── presentation/        # 表现层
39 | └── common/             # 通用功能
40 | 
41 | 42 | ## 开始使用 43 | 44 | 1. 克隆仓库 45 | ```bash 46 | git clone [repository-url] 47 | ``` 48 | 49 | 2. 安装依赖 50 | ```bash 51 | flutter pub get 52 | ``` 53 | 54 | 3. 运行应用 55 | ```bash 56 | flutter run 57 | ``` 58 | 59 | ## 功能特性 60 | 61 | - 现代化UI设计 62 | - 流畅的动画效果 63 | - ASMR 播放控制 64 | - 播放列表管理 65 | - 搜索功能 66 | - 收藏功能 67 | 68 | ## 贡献指南 69 | 70 | 在提交贡献之前,请阅读我们的[开发准则](docs/guidelines_zh.md)。 71 | 72 | ## 许可证 73 | 74 | 本项目采用 Creative Commons 非商业性使用-相同方式共享许可证 (CC BY-NC-SA) - 查看 [LICENSE](LICENSE) 文件了解详细信息。该许可证允许他人修改和分享您的作品,但禁止商业用途,要求保留署名,并要求对修改后的作品以相同的许可证发布。 75 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # ASMR One App 2 | 3 | [中文说明](README.md) 4 | 5 | A beautiful and modern ASMR player application built with Flutter. 6 | 7 | ## Project Overview 8 | 9 | ASMR One App is designed to provide a smooth and enjoyable ASMR listening experience with beautiful animations and a modern user interface. 10 | 11 | ## Development Guidelines 12 | 13 | We maintain a comprehensive set of development guidelines to ensure code quality and consistency: 14 | - [Development Guidelines](docs/guidelines_en.md) 15 | 16 | ## Project Structure 17 | 18 |
19 | lib/
20 | ├── core/                 # Core functionality
21 | ├── data/                # Data layer
22 | ├── domain/              # Domain layer
23 | ├── presentation/        # Presentation layer
24 | └── common/             # Common functionality
25 | 
26 | 27 | ## Getting Started 28 | 29 | 1. Clone the repository 30 | ```bash 31 | git clone [repository-url] 32 | ``` 33 | 34 | 2. Install dependencies 35 | ```bash 36 | flutter pub get 37 | ``` 38 | 39 | 3. Run the app 40 | ```bash 41 | flutter run 42 | ``` 43 | 44 | ## Features 45 | 46 | - Modern UI design 47 | - Smooth animations 48 | - ASMR playback control 49 | - Playlist management 50 | - Search functionality 51 | - Favorites collection 52 | 53 | ## Contributing 54 | 55 | Please read our [Development Guidelines](docs/guidelines_en.md) before making a contribution. 56 | 57 | ## License 58 | 59 | 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. -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | analyzer: 30 | exclude: 31 | - "**/*.g.dart" 32 | - "**/*.freezed.dart" 33 | errors: 34 | invalid_annotation_target: ignore -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | ## Flutter wrapper 2 | -keep class io.flutter.app.** { *; } 3 | -keep class io.flutter.plugin.** { *; } 4 | -keep class io.flutter.util.** { *; } 5 | -keep class io.flutter.view.** { *; } 6 | -keep class io.flutter.** { *; } 7 | -keep class io.flutter.plugins.** { *; } 8 | -keep class io.flutter.plugin.editing.** { *; } 9 | -dontwarn io.flutter.embedding.** 10 | -keepattributes Signature 11 | -keepattributes *Annotation* 12 | 13 | ## Gson rules 14 | -keepattributes Signature 15 | -keepattributes *Annotation* 16 | -dontwarn sun.misc.** 17 | 18 | ## audio_service plugin 19 | -keep class com.ryanheise.audioservice.** { *; } 20 | 21 | ## Fix Play Store Split 22 | -keep class com.google.android.play.core.splitcompat.** { *; } 23 | -dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication 24 | 25 | ## Fix for all Android classes that might be accessed via reflection 26 | -keep class androidx.lifecycle.DefaultLifecycleObserver 27 | -keep class androidx.lifecycle.LifecycleOwner 28 | -keepnames class androidx.lifecycle.LifecycleOwner 29 | 30 | ## Just Audio 31 | -keep class com.google.android.exoplayer2.** { *; } 32 | -dontwarn com.google.android.exoplayer2.** 33 | 34 | ## Cached network image 35 | -keep class com.bumptech.glide.** { *; } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/asmrapp/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.asmrapp 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | import com.ryanheise.audioservice.AudioServiceActivity 5 | 6 | class MainActivity: AudioServiceActivity() 7 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/one/asmr/yuro/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package one.asmr.yuro 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | import com.ryanheise.audioservice.AudioServiceActivity 5 | import io.flutter.embedding.engine.FlutterEngine 6 | import io.flutter.plugin.common.MethodChannel 7 | import one.asmr.yuro.lyric.LyricOverlayPlugin 8 | 9 | class MainActivity: AudioServiceActivity() { 10 | override fun configureFlutterEngine(flutterEngine: FlutterEngine) { 11 | super.configureFlutterEngine(flutterEngine) 12 | 13 | MethodChannel( 14 | flutterEngine.dartExecutor.binaryMessenger, 15 | "one.asmr.yuro/lyric_overlay" 16 | ).setMethodCallHandler(LyricOverlayPlugin(this)) 17 | } 18 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/lyric_overlay.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 127.0.0.1 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.1.0" apply false 22 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /assets/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/assets/icon/icon.png -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # ASMR Music App 架构设计 2 | 3 | ## 目录结构 4 | 5 |
 6 | lib/
 7 | ├── main.dart              # 应用程序入口
 8 | ├── screens/              # 页面
 9 | │   ├── home_screen.dart   # 主页(音乐列表)
10 | │   ├── player_screen.dart # 播放页面
11 | │   └── detail_screen.dart # 详情页面
12 | ├── widgets/              # 可重用组件
13 | │   └── drawer_menu.dart   # 侧滑菜单
14 | └── models/              # 数据模型(待添加)
15 |     └── music.dart        # 音乐模型(待添加)
16 | 
17 | 18 | ## 主要功能模块 19 | 20 | 1. 主页 (HomeScreen) 21 | - 显示音乐列表 22 | - 搜索功能 23 | - 侧滑菜单访问 24 | 25 | 2. 播放页 (PlayerScreen) 26 | - 音乐播放控制 27 | - 进度条 28 | - 音量控制 29 | 30 | 3. 详情页 (DetailScreen) 31 | - 显示音乐详细信息 32 | - 评论功能(待实现) 33 | - 收藏功能(待实现) 34 | 35 | 4. 侧滑菜单 (DrawerMenu) 36 | - 主页导航 37 | - 收藏列表 38 | - 设置页面 39 | 40 | ## 技术栈 41 | 42 | - Flutter SDK 43 | - Material Design 3 44 | - 路由管理: Flutter 内置导航 45 | - 状态管理: 待定 46 | 47 | ## 开发计划 48 | 49 | 1. 第一阶段:基础框架搭建 50 | - [x] 创建基本页面结构 51 | - [x] 实现页面导航 52 | - [x] 设计侧滑菜单 53 | 54 | 2. 第二阶段:UI 实现 55 | - [ ] 设计并实现音乐列表 56 | - [ ] 设计并实现播放器界面 57 | - [ ] 设计并实现详情页面 58 | 59 | 3. 第三阶段:功能实现 60 | - [ ] 音乐播放功能 61 | - [ ] 搜索功能 62 | - [ ] 收藏功能 63 | 64 | 4. 第四阶段:优化 65 | - [ ] 性能优化 66 | - [ ] UI/UX 改进 67 | - [ ] 代码重构 68 | 69 | ## 注意事项 70 | 71 | 1. 代码规范 72 | - 使用 const 构造函数 73 | - 遵循 Flutter 官方代码风格 74 | - 添加必要的代码注释 75 | 76 | 2. 性能考虑 77 | - 合理使用 StatelessWidget 和 StatefulWidget 78 | - 避免不必要的重建 79 | - 图片资源优化 80 | 81 | 3. 用户体验 82 | - 添加加载状态提示 83 | - 错误处理和提示 84 | - 合理的动画过渡 85 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Asmrapp 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | asmrapp 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/common/constants/strings.dart: -------------------------------------------------------------------------------- 1 | class Strings { 2 | // App 3 | static const String appName = 'asmr.one'; 4 | 5 | // Common 6 | static const String loading = '加载中...'; 7 | static const String error = '出错了'; 8 | static const String retry = '重试'; 9 | static const String cancel = '取消'; 10 | static const String confirm = '确认'; 11 | 12 | // Home 13 | static const String search = '搜索'; 14 | static const String musicList = '音乐列表将在这里显示'; 15 | 16 | // Player 17 | static const String nowPlaying = '正在播放'; 18 | static const String playerPlaceholder = '播放器控件将在这里显示'; 19 | 20 | // Detail 21 | static const String detail = '音乐详情'; 22 | static const String detailPlaceholder = '音乐详细信息将在这里显示'; 23 | 24 | // Drawer 25 | static const String home = '主页'; 26 | static const String favorites = '我的收藏'; 27 | static const String settings = '设置'; 28 | } 29 | -------------------------------------------------------------------------------- /lib/core/audio/README.md: -------------------------------------------------------------------------------- 1 | # 音频核心功能 2 | 3 | ## 当前架构 4 | 5 | ### 1. 事件驱动系统 6 | - 基于 RxDart 的事件中心 7 | - 统一的事件定义和处理 8 | - 支持事件过滤和转换 9 | 10 | ### 2. 核心服务 (AudioPlayerService) 11 | - 实现 IAudioPlayerService 接口 12 | - 通过依赖注入管理依赖 13 | - 负责协调各个组件 14 | 15 | ### 3. 状态管理 16 | - PlaybackStateManager 负责状态维护 17 | - 通过 EventHub 发送状态更新 18 | - 支持状态持久化 19 | 20 | ### 4. 通知栏集成 21 | - 基于 audio_service 包 22 | - 响应系统媒体控制 23 | - 支持后台播放 24 | 25 | ### 5. 依赖注入 26 | 通过 GetIt 管理所有依赖: 27 |
28 | void setupServiceLocator() {
29 |   // 注册 EventHub
30 |   getIt.registerLazySingleton(() => PlaybackEventHub());
31 |   
32 |   // 注册音频服务
33 |   getIt.registerLazySingleton(
34 |     () => AudioPlayerService(
35 |       eventHub: getIt(),
36 |       stateRepository: getIt(),
37 |     ),
38 |   );
39 | }
40 | 
41 | 42 | ## 注意事项 43 | 44 | - 所有状态更新通过 EventHub 传递 45 | - 避免组件间直接调用 46 | - 优先使用依赖注入 47 | - 保持组件职责单一 48 | -------------------------------------------------------------------------------- /lib/core/audio/audio_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:just_audio/just_audio.dart'; 2 | 3 | abstract class AudioService { 4 | Future play(String url); 5 | Future pause(); 6 | Future resume(); 7 | Future stop(); 8 | Future dispose(); 9 | 10 | Stream get playerState; 11 | } 12 | -------------------------------------------------------------------------------- /lib/core/audio/events/playback_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:just_audio/just_audio.dart'; 2 | import '../models/audio_track_info.dart'; 3 | import '../models/playback_context.dart'; 4 | import 'package:asmrapp/data/models/files/child.dart'; 5 | import 'package:asmrapp/data/models/works/work.dart'; 6 | 7 | /// 播放事件基类 8 | abstract class PlaybackEvent {} 9 | 10 | /// 播放状态事件 11 | class PlaybackStateEvent extends PlaybackEvent { 12 | final PlayerState state; 13 | final Duration position; 14 | final Duration? duration; 15 | PlaybackStateEvent(this.state, this.position, this.duration); 16 | } 17 | 18 | /// 播放上下文事件 19 | class PlaybackContextEvent extends PlaybackEvent { 20 | final PlaybackContext context; 21 | PlaybackContextEvent(this.context); 22 | } 23 | 24 | /// 音轨变更事件 25 | class TrackChangeEvent extends PlaybackEvent { 26 | final AudioTrackInfo track; 27 | final Child file; 28 | final Work work; 29 | TrackChangeEvent(this.track, this.file, this.work); 30 | } 31 | 32 | /// 播放错误事件 33 | class PlaybackErrorEvent extends PlaybackEvent { 34 | final String operation; 35 | final dynamic error; 36 | final StackTrace? stackTrace; 37 | PlaybackErrorEvent(this.operation, this.error, [this.stackTrace]); 38 | } 39 | 40 | /// 播放完成事件 41 | class PlaybackCompletedEvent extends PlaybackEvent { 42 | final PlaybackContext context; 43 | PlaybackCompletedEvent(this.context); 44 | } 45 | 46 | /// 播放进度事件 47 | class PlaybackProgressEvent extends PlaybackEvent { 48 | final Duration position; 49 | final Duration? bufferedPosition; 50 | PlaybackProgressEvent(this.position, this.bufferedPosition); 51 | } 52 | 53 | /// 添加初始状态相关事件 54 | class RequestInitialStateEvent extends PlaybackEvent {} 55 | 56 | class InitialStateEvent extends PlaybackEvent { 57 | final AudioTrackInfo? track; 58 | final PlaybackContext? context; 59 | InitialStateEvent(this.track, this.context); 60 | } -------------------------------------------------------------------------------- /lib/core/audio/events/playback_event_hub.dart: -------------------------------------------------------------------------------- 1 | import 'package:rxdart/rxdart.dart'; 2 | import './playback_event.dart'; 3 | 4 | class PlaybackEventHub { 5 | // 统一的事件流,处理所有类型的事件 6 | final _eventSubject = PublishSubject(); 7 | 8 | // 分类后的特定事件流 9 | late final Stream playbackState = _eventSubject 10 | .whereType() 11 | .distinct(); 12 | 13 | late final Stream trackChange = _eventSubject 14 | .whereType(); 15 | 16 | late final Stream contextChange = _eventSubject 17 | .whereType(); 18 | 19 | late final Stream playbackProgress = _eventSubject 20 | .whereType() 21 | .distinct((prev, next) => prev.position == next.position); 22 | 23 | late final Stream errors = _eventSubject 24 | .whereType(); 25 | 26 | // 添加新的事件流 27 | late final Stream initialState = _eventSubject 28 | .whereType(); 29 | 30 | late final Stream requestInitialState = _eventSubject 31 | .whereType(); 32 | 33 | // 发送事件 34 | void emit(PlaybackEvent event) => _eventSubject.add(event); 35 | 36 | // 资源释放 37 | void dispose() => _eventSubject.close(); 38 | } -------------------------------------------------------------------------------- /lib/core/audio/i_audio_player_service.dart: -------------------------------------------------------------------------------- 1 | import './models/audio_track_info.dart'; 2 | import './models/playback_context.dart'; 3 | 4 | abstract class IAudioPlayerService { 5 | // 基础播放控制 6 | Future pause(); 7 | Future resume(); 8 | Future stop(); 9 | Future seek(Duration position); 10 | Future previous(); 11 | Future next(); 12 | Future dispose(); 13 | 14 | // 上下文管理 15 | Future playWithContext(PlaybackContext context); 16 | 17 | // 状态访问 18 | AudioTrackInfo? get currentTrack; 19 | PlaybackContext? get currentContext; 20 | 21 | // 状态持久化 22 | Future savePlaybackState(); 23 | Future restorePlaybackState(); 24 | } 25 | -------------------------------------------------------------------------------- /lib/core/audio/models/audio_track_info.dart: -------------------------------------------------------------------------------- 1 | class AudioTrackInfo { 2 | final String title; 3 | final String artist; 4 | final String coverUrl; 5 | final String url; 6 | final Duration? duration; 7 | 8 | AudioTrackInfo({ 9 | required this.title, 10 | required this.artist, 11 | required this.coverUrl, 12 | required this.url, 13 | this.duration, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /lib/core/audio/models/play_mode.dart: -------------------------------------------------------------------------------- 1 | enum PlayMode { 2 | single, // 单曲循环 3 | loop, // 列表循环 4 | sequence, // 顺序播放 5 | } -------------------------------------------------------------------------------- /lib/core/audio/notification/audio_notification_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:asmrapp/core/audio/events/playback_event_hub.dart'; 2 | import 'package:audio_service/audio_service.dart'; 3 | import 'package:just_audio/just_audio.dart'; 4 | import 'package:asmrapp/utils/logger.dart'; 5 | import 'package:rxdart/rxdart.dart'; 6 | import '../models/audio_track_info.dart'; 7 | import '../audio_player_handler.dart'; 8 | 9 | class AudioNotificationService { 10 | final AudioPlayer _player; 11 | final PlaybackEventHub _eventHub; 12 | AudioHandler? _audioHandler; 13 | final _mediaItem = BehaviorSubject(); 14 | 15 | AudioNotificationService( 16 | this._player, 17 | this._eventHub, 18 | ); 19 | 20 | Future init() async { 21 | try { 22 | _audioHandler = await AudioService.init( 23 | builder: () => AudioPlayerHandler(_player, _eventHub), 24 | config: const AudioServiceConfig( 25 | androidNotificationChannelId: 'com.asmrapp.audio', 26 | androidNotificationChannelName: 'ASMR One 播放器', 27 | androidNotificationOngoing: true, 28 | androidStopForegroundOnPause: true, 29 | ), 30 | ); 31 | 32 | _setupEventListeners(); 33 | AppLogger.debug('通知栏服务初始化成功'); 34 | } catch (e) { 35 | AppLogger.error('通知栏服务初始化失败', e); 36 | rethrow; 37 | } 38 | } 39 | 40 | void _setupEventListeners() { 41 | // 监听轨道变更事件来更新媒体信息 42 | _eventHub.trackChange.listen((event) { 43 | updateMetadata(event.track); 44 | }); 45 | } 46 | 47 | void updateMetadata(AudioTrackInfo trackInfo) { 48 | final mediaItem = MediaItem( 49 | id: trackInfo.url, 50 | title: trackInfo.title, 51 | artist: trackInfo.artist, 52 | artUri: Uri.parse(trackInfo.coverUrl), 53 | duration: trackInfo.duration, 54 | ); 55 | 56 | _mediaItem.add(mediaItem); 57 | if (_audioHandler != null) { 58 | (_audioHandler as BaseAudioHandler).mediaItem.add(mediaItem); 59 | } 60 | } 61 | 62 | Future dispose() async { 63 | await _audioHandler?.stop(); 64 | await _mediaItem.close(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/core/audio/storage/i_playback_state_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:asmrapp/data/models/playback/playback_state.dart'; 2 | 3 | abstract class IPlaybackStateRepository { 4 | Future saveState(PlaybackState state); 5 | Future loadState(); 6 | } -------------------------------------------------------------------------------- /lib/core/audio/storage/playback_state_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | import 'package:asmrapp/utils/logger.dart'; 4 | import 'package:asmrapp/data/models/playback/playback_state.dart'; 5 | import 'i_playback_state_repository.dart'; 6 | 7 | class PlaybackStateRepository implements IPlaybackStateRepository { 8 | static const _key = 'last_playback_state'; 9 | final SharedPreferences _prefs; 10 | 11 | PlaybackStateRepository(this._prefs); 12 | 13 | @override 14 | Future saveState(PlaybackState state) async { 15 | try { 16 | final json = state.toJson(); 17 | final data = jsonEncode(json); 18 | await _prefs.setString(_key, data); 19 | AppLogger.debug('播放状态已保存'); 20 | } catch (e) { 21 | AppLogger.error('保存播放状态失败', e); 22 | rethrow; 23 | } 24 | } 25 | 26 | @override 27 | Future loadState() async { 28 | try { 29 | final data = _prefs.getString(_key); 30 | if (data == null) { 31 | AppLogger.debug('没有找到保存的播放状态'); 32 | return null; 33 | } 34 | 35 | final json = jsonDecode(data) as Map; 36 | final state = PlaybackState.fromJson(json); 37 | AppLogger.debug('播放状态已加载'); 38 | return state; 39 | } catch (e) { 40 | AppLogger.error('加载播放状态失败', e); 41 | return null; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /lib/core/audio/utils/audio_error_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:asmrapp/utils/logger.dart'; 2 | 3 | enum AudioErrorType { 4 | playback, // 播放错误 5 | playlist, // 播放列表错误 6 | state, // 状态错误 7 | context, // 上下文错误 8 | init, // 初始化错误 9 | } 10 | 11 | class AudioError implements Exception { 12 | final AudioErrorType type; 13 | final String message; 14 | final dynamic originalError; 15 | 16 | AudioError(this.type, this.message, [this.originalError]); 17 | 18 | @override 19 | String toString() => '$message${originalError != null ? ': $originalError' : ''}'; 20 | } 21 | 22 | class AudioErrorHandler { 23 | static void handleError( 24 | AudioErrorType type, 25 | String operation, 26 | dynamic error, [ 27 | StackTrace? stack, 28 | ]) { 29 | final message = _getErrorMessage(type, operation); 30 | AppLogger.error(message, error, stack); 31 | } 32 | 33 | static Never throwError( 34 | AudioErrorType type, 35 | String operation, 36 | dynamic error, 37 | ) { 38 | final message = _getErrorMessage(type, operation); 39 | throw AudioError(type, message, error); 40 | } 41 | 42 | static String _getErrorMessage(AudioErrorType type, String operation) { 43 | switch (type) { 44 | case AudioErrorType.playback: 45 | return '播放操作失败: $operation'; 46 | case AudioErrorType.playlist: 47 | return '播放列表操作失败: $operation'; 48 | case AudioErrorType.state: 49 | return '状态操作失败: $operation'; 50 | case AudioErrorType.context: 51 | return '上下文操作失败: $operation'; 52 | case AudioErrorType.init: 53 | return '初始化失败: $operation'; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /lib/core/audio/utils/playlist_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:just_audio/just_audio.dart'; 2 | import 'package:asmrapp/data/models/files/child.dart'; 3 | import 'package:asmrapp/core/audio/cache/audio_cache_manager.dart'; 4 | 5 | class PlaylistBuilder { 6 | static Future> buildAudioSources(List files) async { 7 | return await Future.wait( 8 | files.map((file) async { 9 | return AudioCacheManager.createAudioSource(file.mediaDownloadUrl!); 10 | }) 11 | ); 12 | } 13 | 14 | static Future updatePlaylist( 15 | ConcatenatingAudioSource playlist, 16 | List sources, 17 | ) async { 18 | await playlist.clear(); 19 | await playlist.addAll(sources); 20 | } 21 | 22 | static Future setPlaylistSource({ 23 | required AudioPlayer player, 24 | required ConcatenatingAudioSource playlist, 25 | required List files, 26 | required int initialIndex, 27 | required Duration initialPosition, 28 | }) async { 29 | final sources = await buildAudioSources(files); 30 | await updatePlaylist(playlist, sources); 31 | 32 | await player.setAudioSource( 33 | playlist, 34 | initialIndex: initialIndex, 35 | initialPosition: initialPosition, 36 | ); 37 | } 38 | } -------------------------------------------------------------------------------- /lib/core/audio/utils/track_info_creator.dart: -------------------------------------------------------------------------------- 1 | import 'package:asmrapp/core/audio/models/audio_track_info.dart'; 2 | import 'package:asmrapp/data/models/files/child.dart'; 3 | import 'package:asmrapp/data/models/works/work.dart'; 4 | 5 | class TrackInfoCreator { 6 | static AudioTrackInfo createTrackInfo({ 7 | required String title, 8 | required String? artistName, 9 | required String? coverUrl, 10 | required String url, 11 | }) { 12 | return AudioTrackInfo( 13 | title: title, 14 | artist: artistName ?? '', 15 | coverUrl: coverUrl ?? '', 16 | url: url, 17 | ); 18 | } 19 | 20 | static AudioTrackInfo createFromFile(Child file, Work work) { 21 | return createTrackInfo( 22 | title: file.title ?? '', 23 | artistName: work.circle?.name, 24 | coverUrl: work.mainCoverUrl, 25 | url: file.mediaDownloadUrl!, 26 | ); 27 | } 28 | } -------------------------------------------------------------------------------- /lib/core/platform/dummy_lyric_overlay_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:asmrapp/utils/logger.dart'; 2 | import 'i_lyric_overlay_controller.dart'; 3 | 4 | class DummyLyricOverlayController implements ILyricOverlayController { 5 | static const _tag = 'LyricOverlay'; 6 | 7 | @override 8 | Future initialize() async { 9 | } 10 | 11 | @override 12 | Future show() async { 13 | 14 | } 15 | 16 | @override 17 | Future hide() async { 18 | 19 | } 20 | 21 | @override 22 | Future updateLyric(String? text) async { 23 | 24 | } 25 | 26 | @override 27 | Future checkPermission() async { 28 | return true; 29 | } 30 | 31 | @override 32 | Future requestPermission() async { 33 | AppLogger.debug('[$_tag] 请求权限'); 34 | return true; 35 | } 36 | 37 | @override 38 | Future dispose() async { 39 | 40 | } 41 | 42 | @override 43 | Future isShowing() async { 44 | return false; 45 | } 46 | } -------------------------------------------------------------------------------- /lib/core/platform/i_lyric_overlay_controller.dart: -------------------------------------------------------------------------------- 1 | abstract class ILyricOverlayController { 2 | /// 初始化悬浮窗 3 | Future initialize(); 4 | 5 | /// 显示悬浮窗 6 | Future show(); 7 | 8 | /// 隐藏悬浮窗 9 | Future hide(); 10 | 11 | /// 更新歌词内容 12 | Future updateLyric(String? text); 13 | 14 | /// 检查悬浮窗权限 15 | Future checkPermission(); 16 | 17 | /// 请求悬浮窗权限 18 | Future requestPermission(); 19 | 20 | /// 释放资源 21 | Future dispose(); 22 | 23 | /// 获取悬浮窗当前显示状态 24 | Future isShowing(); 25 | } -------------------------------------------------------------------------------- /lib/core/platform/lyric_overlay_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:asmrapp/utils/logger.dart'; 3 | import 'package:permission_handler/permission_handler.dart'; 4 | import 'i_lyric_overlay_controller.dart'; 5 | 6 | class LyricOverlayController implements ILyricOverlayController { 7 | static const _tag = 'LyricOverlay'; 8 | static const _channel = MethodChannel('one.asmr.yuro/lyric_overlay'); 9 | 10 | @override 11 | Future initialize() async { 12 | try { 13 | AppLogger.debug('[$_tag] 初始化'); 14 | await _channel.invokeMethod('initialize'); 15 | } catch (e) { 16 | AppLogger.error('[$_tag] 初始化失败', e); 17 | // 这里我们不抛出异常,而是静默失败 18 | // 因为这个错误不应该影响应用的主要功能 19 | } 20 | } 21 | 22 | @override 23 | Future show() async { 24 | AppLogger.debug('[$_tag] 显示悬浮窗'); 25 | await _channel.invokeMethod('show'); 26 | } 27 | 28 | @override 29 | Future hide() async { 30 | AppLogger.debug('[$_tag] 隐藏悬浮窗'); 31 | await _channel.invokeMethod('hide'); 32 | } 33 | 34 | @override 35 | Future updateLyric(String? text) async { 36 | AppLogger.debug('[$_tag] 更新歌词: ${text ?? '<空>'}'); 37 | await _channel.invokeMethod('updateLyric', {'text': text}); 38 | } 39 | 40 | @override 41 | Future checkPermission() async { 42 | AppLogger.debug('[$_tag] 检查权限'); 43 | return await Permission.systemAlertWindow.isGranted; 44 | } 45 | 46 | @override 47 | Future requestPermission() async { 48 | AppLogger.debug('[$_tag] 请求权限'); 49 | final status = await Permission.systemAlertWindow.request(); 50 | return status.isGranted; 51 | } 52 | 53 | @override 54 | Future dispose() async { 55 | AppLogger.debug('[$_tag] 释放资源'); 56 | await _channel.invokeMethod('dispose'); 57 | } 58 | 59 | @override 60 | Future isShowing() async { 61 | final result = await _channel.invokeMethod('isShowing') ?? false; 62 | return result; 63 | } 64 | } -------------------------------------------------------------------------------- /lib/core/platform/wakelock_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | import 'package:wakelock_plus/wakelock_plus.dart'; 5 | import 'package:asmrapp/utils/logger.dart'; 6 | 7 | class WakeLockController extends ChangeNotifier { 8 | static const _tag = 'WakeLock'; 9 | static const _wakeLockKey = 'wakelock_enabled'; 10 | final SharedPreferences _prefs; 11 | bool _enabled = false; 12 | 13 | WakeLockController(this._prefs) { 14 | _loadState(); 15 | } 16 | 17 | bool get enabled => _enabled; 18 | 19 | Future _loadState() async { 20 | try { 21 | _enabled = _prefs.getBool(_wakeLockKey) ?? false; 22 | if (_enabled) { 23 | await WakelockPlus.enable(); 24 | } 25 | notifyListeners(); 26 | } catch (e) { 27 | AppLogger.error('[$_tag] 加载状态失败', e); 28 | } 29 | } 30 | 31 | Future toggle() async { 32 | try { 33 | _enabled = !_enabled; 34 | if (_enabled) { 35 | await WakelockPlus.enable(); 36 | } else { 37 | await WakelockPlus.disable(); 38 | } 39 | await _prefs.setBool(_wakeLockKey, _enabled); 40 | notifyListeners(); 41 | } catch (e) { 42 | AppLogger.error('[$_tag] 切换状态失败', e); 43 | // 恢复状态 44 | _enabled = !_enabled; 45 | notifyListeners(); 46 | } 47 | } 48 | 49 | Future dispose() async { 50 | try { 51 | await WakelockPlus.disable(); 52 | } catch (e) { 53 | AppLogger.error('[$_tag] 释放失败', e); 54 | } 55 | super.dispose(); 56 | } 57 | } -------------------------------------------------------------------------------- /lib/core/subtitle/cache/subtitle_cache_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 5 | import 'package:asmrapp/utils/logger.dart'; 6 | 7 | class SubtitleCacheManager { 8 | static const String key = 'subtitleCache'; 9 | 10 | static final CacheManager instance = CacheManager( 11 | Config( 12 | key, 13 | stalePeriod: const Duration(days: 365), // 字幕文件不会变更,设置较长的有效期 14 | maxNrOfCacheObjects: 1000, // 最大缓存文件数 15 | repo: JsonCacheInfoRepository(databaseName: key), 16 | fileService: HttpFileService(), 17 | ), 18 | ); 19 | 20 | /// 获取缓存的字幕内容 21 | static Future getCachedContent(String url) async { 22 | try { 23 | final file = await instance.getSingleFile(url); 24 | AppLogger.debug('使用字幕缓存: $url'); 25 | return await file.readAsString(); 26 | } catch (e) { 27 | AppLogger.error('读取字幕缓存失败', e); 28 | return null; 29 | } 30 | } 31 | 32 | /// 保存字幕内容到缓存 33 | static Future cacheContent(String url, String content) async { 34 | try { 35 | await instance.putFile( 36 | url, 37 | Uint8List.fromList(utf8.encode(content)), 38 | fileExtension: 'txt', 39 | ); 40 | AppLogger.debug('字幕已缓存: $url'); 41 | } catch (e) { 42 | AppLogger.error('保存字幕缓存失败', e); 43 | } 44 | } 45 | 46 | /// 清理缓存 47 | static Future clearCache() async { 48 | try { 49 | await instance.emptyCache(); 50 | AppLogger.debug('字幕缓存已清空'); 51 | } catch (e) { 52 | AppLogger.error('清理字幕缓存失败', e); 53 | } 54 | } 55 | 56 | /// 获取缓存大小 57 | static Future getSize() async { 58 | try { 59 | return instance.store.getCacheSize(); 60 | } catch (e) { 61 | AppLogger.error('获取字幕缓存大小失败', e); 62 | return 0; 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /lib/core/subtitle/i_subtitle_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:asmrapp/core/audio/models/subtitle.dart'; 2 | 3 | abstract class ISubtitleService { 4 | // 字幕加载 5 | Future loadSubtitle(String url); 6 | 7 | // 字幕状态流 8 | Stream get subtitleStream; 9 | 10 | // 当前字幕流 11 | Stream get currentSubtitleStream; 12 | 13 | // 当前字幕 14 | Subtitle? get currentSubtitle; 15 | 16 | // 更新播放位置 17 | void updatePosition(Duration position); 18 | 19 | // 资源释放 20 | void dispose(); 21 | 22 | // 添加这一行 23 | SubtitleList? get subtitleList; // 获取当前字幕列表 24 | 25 | // 添加清除字幕的方法 26 | void clearSubtitle(); 27 | 28 | Stream get currentSubtitleWithStateStream; 29 | SubtitleWithState? get currentSubtitleWithState; 30 | } -------------------------------------------------------------------------------- /lib/core/subtitle/parsers/subtitle_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:asmrapp/core/audio/models/subtitle.dart'; 2 | 3 | /// 字幕解析器接口 4 | abstract class SubtitleParser { 5 | /// 解析字幕内容 6 | SubtitleList parse(String content); 7 | 8 | /// 检查内容格式是否匹配 9 | bool canParse(String content); 10 | } 11 | 12 | /// 字幕解析器基类 13 | abstract class BaseSubtitleParser implements SubtitleParser { 14 | @override 15 | SubtitleList parse(String content) { 16 | if (!canParse(content)) { 17 | throw FormatException('不支持的字幕格式'); 18 | } 19 | return doParse(content); 20 | } 21 | 22 | /// 具体的解析实现 23 | SubtitleList doParse(String content); 24 | } -------------------------------------------------------------------------------- /lib/core/subtitle/parsers/subtitle_parser_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:asmrapp/core/subtitle/parsers/subtitle_parser.dart'; 2 | import 'package:asmrapp/core/subtitle/parsers/vtt_parser.dart'; 3 | import 'package:asmrapp/core/subtitle/parsers/lrc_parser.dart'; 4 | import 'package:asmrapp/utils/logger.dart'; 5 | 6 | class SubtitleParserFactory { 7 | static final List _parsers = [ 8 | VttParser(), 9 | LrcParser(), 10 | ]; 11 | 12 | static SubtitleParser? getParser(String content) { 13 | try { 14 | return _parsers.firstWhere((parser) => parser.canParse(content)); 15 | } catch (e) { 16 | AppLogger.debug('没有找到匹配的字幕解析器'); 17 | return null; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /lib/core/subtitle/parsers/vtt_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:asmrapp/core/audio/models/subtitle.dart'; 2 | import 'package:asmrapp/core/subtitle/parsers/subtitle_parser.dart'; 3 | 4 | class VttParser extends BaseSubtitleParser { 5 | static final _vttHeaderRegex = RegExp(r'^WEBVTT'); 6 | 7 | @override 8 | bool canParse(String content) { 9 | return content.trim().startsWith(_vttHeaderRegex); 10 | } 11 | 12 | @override 13 | SubtitleList doParse(String content) { 14 | final lines = content.split('\n'); 15 | final subtitles = []; 16 | int index = 0; 17 | 18 | // 跳过WEBVTT头部 19 | while (index < lines.length && !lines[index].contains('-->')) { 20 | index++; 21 | } 22 | 23 | while (index < lines.length) { 24 | final timeLine = lines[index]; 25 | if (timeLine.contains('-->')) { 26 | final times = timeLine.split('-->'); 27 | if (times.length == 2) { 28 | final start = _parseTimeString(times[0].trim()); 29 | final end = _parseTimeString(times[1].trim()); 30 | 31 | // 收集字幕文本 32 | index++; 33 | String text = ''; 34 | while (index < lines.length && lines[index].trim().isNotEmpty) { 35 | text += lines[index].trim() + '\n'; 36 | index++; 37 | } 38 | 39 | if (text.isNotEmpty) { 40 | subtitles.add(Subtitle( 41 | start: start, 42 | end: end, 43 | text: text.trim(), 44 | index: subtitles.length, 45 | )); 46 | } 47 | } 48 | } 49 | index++; 50 | } 51 | 52 | return SubtitleList(subtitles); 53 | } 54 | 55 | Duration _parseTimeString(String timeString) { 56 | final parts = timeString.split(':'); 57 | if (parts.length != 3) throw FormatException('Invalid time format'); 58 | 59 | final seconds = parts[2].split('.'); 60 | return Duration( 61 | hours: int.parse(parts[0]), 62 | minutes: int.parse(parts[1]), 63 | seconds: int.parse(seconds[0]), 64 | milliseconds: seconds.length > 1 ? int.parse(seconds[1].padRight(3, '0')) : 0, 65 | ); 66 | } 67 | } -------------------------------------------------------------------------------- /lib/core/subtitle/subtitle_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:asmrapp/utils/logger.dart'; 3 | import 'package:asmrapp/core/audio/models/subtitle.dart'; 4 | import 'package:asmrapp/core/subtitle/i_subtitle_service.dart'; 5 | import 'package:get_it/get_it.dart'; 6 | import 'package:asmrapp/core/subtitle/subtitle_loader.dart'; 7 | import 'package:asmrapp/core/subtitle/managers/subtitle_state_manager.dart'; 8 | 9 | 10 | class SubtitleService implements ISubtitleService { 11 | final _subtitleLoader = GetIt.I(); 12 | final _stateManager = SubtitleStateManager(); 13 | 14 | @override 15 | Stream get subtitleStream => _stateManager.subtitleStream; 16 | 17 | @override 18 | Stream get currentSubtitleStream => _stateManager.currentSubtitleStream; 19 | 20 | @override 21 | Subtitle? get currentSubtitle => _stateManager.currentSubtitle; 22 | 23 | @override 24 | Future loadSubtitle(String url) async { 25 | try { 26 | clearSubtitle(); 27 | final subtitleList = await _subtitleLoader.loadSubtitleContent(url); 28 | _stateManager.setSubtitleList(subtitleList); 29 | } catch (e) { 30 | AppLogger.debug('字幕加载失败: $e'); 31 | clearSubtitle(); 32 | rethrow; 33 | } 34 | } 35 | 36 | @override 37 | void updatePosition(Duration position) { 38 | _stateManager.updatePosition(position); 39 | } 40 | 41 | @override 42 | void dispose() { 43 | _stateManager.dispose(); 44 | } 45 | 46 | @override 47 | SubtitleList? get subtitleList => _stateManager.subtitleList; 48 | 49 | @override 50 | void clearSubtitle() { 51 | _stateManager.clear(); 52 | } 53 | 54 | @override 55 | Stream get currentSubtitleWithStateStream => 56 | _stateManager.currentSubtitleWithStateStream; 57 | 58 | @override 59 | SubtitleWithState? get currentSubtitleWithState => 60 | _stateManager.currentSubtitleWithState; 61 | } -------------------------------------------------------------------------------- /lib/core/subtitle/utils/subtitle_matcher.dart: -------------------------------------------------------------------------------- 1 | import 'package:asmrapp/data/models/files/child.dart'; 2 | 3 | class SubtitleMatcher { 4 | // 支持的字幕格式 5 | static const supportedFormats = ['.vtt', '.lrc']; 6 | 7 | // 检查文件是否为字幕文件 8 | static bool isSubtitleFile(String? fileName) { 9 | if (fileName == null) return false; 10 | return supportedFormats.any((format) => 11 | fileName.toLowerCase().endsWith(format)); 12 | } 13 | 14 | // 获取音频文件的可能的字幕文件名列表 15 | static List getPossibleSubtitleNames(String audioFileName) { 16 | final names = []; 17 | final baseName = _getBaseName(audioFileName); 18 | 19 | // 生成可能的字幕文件名 20 | for (final format in supportedFormats) { 21 | // 1. 直接替换扩展名: aaa.mp3 -> aaa.vtt 22 | names.add('$baseName$format'); 23 | 24 | // 2. 保留原扩展名: aaa.mp3 -> aaa.mp3.vtt 25 | names.add('$audioFileName$format'); 26 | } 27 | 28 | return names; 29 | } 30 | 31 | // 查找匹配的字幕文件 32 | static Child? findMatchingSubtitle(String audioFileName, List siblings) { 33 | final possibleNames = getPossibleSubtitleNames(audioFileName); 34 | 35 | // 遍历所有可能的字幕文件名 36 | for (final subtitleName in possibleNames) { 37 | try { 38 | final subtitleFile = siblings.firstWhere( 39 | (file) => file.title?.toLowerCase() == subtitleName.toLowerCase() 40 | ); 41 | return subtitleFile; 42 | } catch (_) { 43 | // 继续查找下一个可能的文件名 44 | continue; 45 | } 46 | } 47 | 48 | return null; 49 | } 50 | 51 | // 获取不带扩展名的文件名 52 | static String _getBaseName(String fileName) { 53 | final lastDot = fileName.lastIndexOf('.'); 54 | if (lastDot == -1) return fileName; 55 | return fileName.substring(0, lastDot); 56 | } 57 | } -------------------------------------------------------------------------------- /lib/core/theme/app_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 应用颜色配置 4 | class AppColors { 5 | // 禁止实例化 6 | const AppColors._(); 7 | 8 | // 亮色主题颜色 9 | static const ColorScheme lightColorScheme = ColorScheme.light( 10 | // 基础色调 11 | primary: Color(0xFF6750A4), 12 | onPrimary: Colors.white, 13 | 14 | // 表面颜色 15 | surface: Colors.white, 16 | surfaceVariant: Color(0xFFF4F4F4), 17 | onSurface: Colors.black87, 18 | surfaceContainerHighest: Color(0xFFE6E6E6), 19 | 20 | // 背景颜色 21 | background: Colors.white, 22 | onBackground: Colors.black87, 23 | 24 | // 错误状态颜色 25 | error: Color(0xFFB3261E), 26 | errorContainer: Color(0xFFF9DEDC), 27 | onError: Colors.white, 28 | ); 29 | 30 | // 暗色主题颜色 31 | static const ColorScheme darkColorScheme = ColorScheme.dark( 32 | // 基础色调 33 | primary: Color(0xFFD0BCFF), 34 | onPrimary: Color(0xFF381E72), 35 | 36 | // 表面颜色 37 | surface: Color(0xFF1C1B1F), 38 | surfaceVariant: Color(0xFF2B2930), 39 | onSurface: Colors.white, 40 | surfaceContainerHighest: Color(0xFF2B2B2B), 41 | 42 | // 背景颜色 43 | background: Color(0xFF1C1B1F), 44 | onBackground: Colors.white, 45 | 46 | // 错误状态颜色 47 | error: Color(0xFFF2B8B5), 48 | errorContainer: Color(0xFF8C1D18), 49 | onError: Color(0xFF601410), 50 | ); 51 | } -------------------------------------------------------------------------------- /lib/core/theme/app_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'app_colors.dart'; 3 | 4 | /// 应用主题配置 5 | class AppTheme { 6 | // 禁止实例化 7 | const AppTheme._(); 8 | 9 | // 亮色主题 10 | static ThemeData get light => ThemeData( 11 | useMaterial3: true, 12 | brightness: Brightness.light, 13 | colorScheme: AppColors.lightColorScheme, 14 | 15 | // Card主题 16 | cardTheme: const CardTheme( 17 | elevation: 0, 18 | shape: RoundedRectangleBorder( 19 | borderRadius: BorderRadius.all(Radius.circular(12)), 20 | ), 21 | ), 22 | 23 | // AppBar主题 24 | appBarTheme: const AppBarTheme( 25 | centerTitle: true, 26 | elevation: 0, 27 | scrolledUnderElevation: 0, 28 | ), 29 | ); 30 | 31 | // 暗色主题 32 | static ThemeData get dark => ThemeData( 33 | useMaterial3: true, 34 | brightness: Brightness.dark, 35 | colorScheme: AppColors.darkColorScheme, 36 | 37 | // Card主题 38 | cardTheme: const CardTheme( 39 | elevation: 0, 40 | shape: RoundedRectangleBorder( 41 | borderRadius: BorderRadius.all(Radius.circular(12)), 42 | ), 43 | ), 44 | 45 | // AppBar主题 46 | appBarTheme: const AppBarTheme( 47 | centerTitle: true, 48 | elevation: 0, 49 | scrolledUnderElevation: 0, 50 | ), 51 | ); 52 | } -------------------------------------------------------------------------------- /lib/core/theme/theme_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | class ThemeController extends ChangeNotifier { 5 | static const String _themeKey = 'theme_mode'; 6 | final SharedPreferences _prefs; 7 | 8 | ThemeController(this._prefs) { 9 | // 从持久化存储加载主题模式 10 | final savedThemeMode = _prefs.getString(_themeKey); 11 | if (savedThemeMode != null) { 12 | _themeMode = ThemeMode.values.firstWhere( 13 | (mode) => mode.toString() == savedThemeMode, 14 | orElse: () => ThemeMode.system, 15 | ); 16 | } 17 | } 18 | 19 | ThemeMode _themeMode = ThemeMode.system; 20 | 21 | ThemeMode get themeMode => _themeMode; 22 | 23 | // 切换主题模式 24 | Future setThemeMode(ThemeMode mode) async { 25 | if (_themeMode == mode) return; 26 | 27 | _themeMode = mode; 28 | notifyListeners(); 29 | 30 | // 保存到持久化存储 31 | await _prefs.setString(_themeKey, mode.toString()); 32 | } 33 | 34 | // 切换到下一个主题模式 35 | Future toggleThemeMode() async { 36 | final modes = ThemeMode.values; 37 | final currentIndex = modes.indexOf(_themeMode); 38 | final nextIndex = (currentIndex + 1) % modes.length; 39 | await setThemeMode(modes[nextIndex]); 40 | } 41 | } -------------------------------------------------------------------------------- /lib/data/models/audio/README.md: -------------------------------------------------------------------------------- 1 | # 音频数据模型 2 | 3 | 此目录包含所有音频相关的数据模型定义。 4 | 5 | ## 文件结构 6 | 7 | - `audio_track.dart` - 音频轨道模型 8 | - `playlist.dart` - 播放列表模型 9 | - `audio_metadata.dart` - 音频元数据模型 10 | 11 | ## 说明 12 | 13 | 这些模型用于: 14 | - 音频文件信息的封装 15 | - 播放列表数据的组织 16 | - 音频元数据的管理 -------------------------------------------------------------------------------- /lib/data/models/auth/auth_resp/auth_resp.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'user.dart'; 4 | 5 | part 'auth_resp.freezed.dart'; 6 | part 'auth_resp.g.dart'; 7 | 8 | @freezed 9 | class AuthResp with _$AuthResp { 10 | factory AuthResp({ 11 | User? user, 12 | String? token, 13 | }) = _AuthResp; 14 | 15 | factory AuthResp.fromJson(Map json) => 16 | _$AuthRespFromJson(json); 17 | } 18 | -------------------------------------------------------------------------------- /lib/data/models/auth/auth_resp/auth_resp.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'auth_resp.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$AuthRespImpl _$$AuthRespImplFromJson(Map json) => 10 | _$AuthRespImpl( 11 | user: json['user'] == null 12 | ? null 13 | : User.fromJson(json['user'] as Map), 14 | token: json['token'] as String?, 15 | ); 16 | 17 | Map _$$AuthRespImplToJson(_$AuthRespImpl instance) => 18 | { 19 | 'user': instance.user, 20 | 'token': instance.token, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/data/models/auth/auth_resp/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'user.freezed.dart'; 4 | part 'user.g.dart'; 5 | 6 | @freezed 7 | class User with _$User { 8 | factory User({ 9 | bool? loggedIn, 10 | String? name, 11 | String? group, 12 | dynamic email, 13 | String? recommenderUuid, 14 | }) = _User; 15 | 16 | factory User.fromJson(Map json) => _$UserFromJson(json); 17 | } 18 | -------------------------------------------------------------------------------- /lib/data/models/auth/auth_resp/user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$UserImpl _$$UserImplFromJson(Map json) => _$UserImpl( 10 | loggedIn: json['loggedIn'] as bool?, 11 | name: json['name'] as String?, 12 | group: json['group'] as String?, 13 | email: json['email'], 14 | recommenderUuid: json['recommenderUuid'] as String?, 15 | ); 16 | 17 | Map _$$UserImplToJson(_$UserImpl instance) => 18 | { 19 | 'loggedIn': instance.loggedIn, 20 | 'name': instance.name, 21 | 'group': instance.group, 22 | 'email': instance.email, 23 | 'recommenderUuid': instance.recommenderUuid, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/data/models/files/child.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'work.dart'; 4 | 5 | part 'child.freezed.dart'; 6 | part 'child.g.dart'; 7 | 8 | @freezed 9 | class Child with _$Child { 10 | factory Child({ 11 | String? type, 12 | String? title, 13 | List? children, 14 | String? hash, 15 | Work? work, 16 | String? workTitle, 17 | String? mediaStreamUrl, 18 | String? mediaDownloadUrl, 19 | int? size, 20 | }) = _Child; 21 | 22 | factory Child.fromJson(Map json) => _$ChildFromJson(json); 23 | } 24 | -------------------------------------------------------------------------------- /lib/data/models/files/child.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'child.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$ChildImpl _$$ChildImplFromJson(Map json) => _$ChildImpl( 10 | type: json['type'] as String?, 11 | title: json['title'] as String?, 12 | children: (json['children'] as List?) 13 | ?.map((e) => Child.fromJson(e as Map)) 14 | .toList(), 15 | hash: json['hash'] as String?, 16 | work: json['work'] == null 17 | ? null 18 | : Work.fromJson(json['work'] as Map), 19 | workTitle: json['workTitle'] as String?, 20 | mediaStreamUrl: json['mediaStreamUrl'] as String?, 21 | mediaDownloadUrl: json['mediaDownloadUrl'] as String?, 22 | size: (json['size'] as num?)?.toInt(), 23 | ); 24 | 25 | Map _$$ChildImplToJson(_$ChildImpl instance) => 26 | { 27 | 'type': instance.type, 28 | 'title': instance.title, 29 | 'children': instance.children, 30 | 'hash': instance.hash, 31 | 'work': instance.work, 32 | 'workTitle': instance.workTitle, 33 | 'mediaStreamUrl': instance.mediaStreamUrl, 34 | 'mediaDownloadUrl': instance.mediaDownloadUrl, 35 | 'size': instance.size, 36 | }; 37 | -------------------------------------------------------------------------------- /lib/data/models/files/files.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'child.dart'; 4 | 5 | part 'files.freezed.dart'; 6 | part 'files.g.dart'; 7 | 8 | @freezed 9 | class Files with _$Files { 10 | factory Files({ 11 | String? type, 12 | String? title, 13 | List? children, 14 | }) = _Files; 15 | 16 | factory Files.fromJson(Map json) => _$FilesFromJson(json); 17 | } 18 | -------------------------------------------------------------------------------- /lib/data/models/files/files.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'files.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$FilesImpl _$$FilesImplFromJson(Map json) => _$FilesImpl( 10 | type: json['type'] as String?, 11 | title: json['title'] as String?, 12 | children: (json['children'] as List?) 13 | ?.map((e) => Child.fromJson(e as Map)) 14 | .toList(), 15 | ); 16 | 17 | Map _$$FilesImplToJson(_$FilesImpl instance) => 18 | { 19 | 'type': instance.type, 20 | 'title': instance.title, 21 | 'children': instance.children, 22 | }; 23 | -------------------------------------------------------------------------------- /lib/data/models/files/work.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'work.freezed.dart'; 4 | part 'work.g.dart'; 5 | 6 | @freezed 7 | class Work with _$Work { 8 | factory Work({ 9 | int? id, 10 | @JsonKey(name: 'source_id') String? sourceId, 11 | @JsonKey(name: 'source_type') String? sourceType, 12 | }) = _Work; 13 | 14 | factory Work.fromJson(Map json) => _$WorkFromJson(json); 15 | } 16 | -------------------------------------------------------------------------------- /lib/data/models/files/work.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'work.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$WorkImpl _$$WorkImplFromJson(Map json) => _$WorkImpl( 10 | id: (json['id'] as num?)?.toInt(), 11 | sourceId: json['source_id'] as String?, 12 | sourceType: json['source_type'] as String?, 13 | ); 14 | 15 | Map _$$WorkImplToJson(_$WorkImpl instance) => 16 | { 17 | 'id': instance.id, 18 | 'source_id': instance.sourceId, 19 | 'source_type': instance.sourceType, 20 | }; 21 | -------------------------------------------------------------------------------- /lib/data/models/mark_lists/mark_lists.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'pagination.dart'; 4 | import 'playlist.dart'; 5 | 6 | part 'mark_lists.freezed.dart'; 7 | part 'mark_lists.g.dart'; 8 | 9 | @freezed 10 | class MarkLists with _$MarkLists { 11 | factory MarkLists({ 12 | List? playlists, 13 | Pagination? pagination, 14 | }) = _MarkLists; 15 | 16 | factory MarkLists.fromJson(Map json) => 17 | _$MarkListsFromJson(json); 18 | } 19 | -------------------------------------------------------------------------------- /lib/data/models/mark_lists/mark_lists.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'mark_lists.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$MarkListsImpl _$$MarkListsImplFromJson(Map json) => 10 | _$MarkListsImpl( 11 | playlists: (json['playlists'] as List?) 12 | ?.map((e) => Playlist.fromJson(e as Map)) 13 | .toList(), 14 | pagination: json['pagination'] == null 15 | ? null 16 | : Pagination.fromJson(json['pagination'] as Map), 17 | ); 18 | 19 | Map _$$MarkListsImplToJson(_$MarkListsImpl instance) => 20 | { 21 | 'playlists': instance.playlists, 22 | 'pagination': instance.pagination, 23 | }; 24 | -------------------------------------------------------------------------------- /lib/data/models/mark_lists/pagination.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'pagination.freezed.dart'; 4 | part 'pagination.g.dart'; 5 | 6 | @freezed 7 | class Pagination with _$Pagination { 8 | factory Pagination({ 9 | int? page, 10 | int? pageSize, 11 | int? totalCount, 12 | }) = _Pagination; 13 | 14 | factory Pagination.fromJson(Map json) => 15 | _$PaginationFromJson(json); 16 | } 17 | -------------------------------------------------------------------------------- /lib/data/models/mark_lists/pagination.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'pagination.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$PaginationImpl _$$PaginationImplFromJson(Map json) => 10 | _$PaginationImpl( 11 | page: (json['page'] as num?)?.toInt(), 12 | pageSize: (json['pageSize'] as num?)?.toInt(), 13 | totalCount: (json['totalCount'] as num?)?.toInt(), 14 | ); 15 | 16 | Map _$$PaginationImplToJson(_$PaginationImpl instance) => 17 | { 18 | 'page': instance.page, 19 | 'pageSize': instance.pageSize, 20 | 'totalCount': instance.totalCount, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/data/models/mark_lists/playlist.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'playlist.freezed.dart'; 4 | part 'playlist.g.dart'; 5 | 6 | @freezed 7 | class Playlist with _$Playlist { 8 | factory Playlist({ 9 | String? id, 10 | @JsonKey(name: 'user_name') String? userName, 11 | int? privacy, 12 | String? locale, 13 | @JsonKey(name: 'playback_count') int? playbackCount, 14 | String? name, 15 | String? description, 16 | @JsonKey(name: 'created_at') String? createdAt, 17 | @JsonKey(name: 'updated_at') String? updatedAt, 18 | @JsonKey(name: 'works_count') int? worksCount, 19 | @JsonKey(name: 'latestWorkID') dynamic latestWorkId, 20 | String? mainCoverUrl, 21 | }) = _Playlist; 22 | 23 | factory Playlist.fromJson(Map json) => 24 | _$PlaylistFromJson(json); 25 | } 26 | -------------------------------------------------------------------------------- /lib/data/models/mark_lists/playlist.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'playlist.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$PlaylistImpl _$$PlaylistImplFromJson(Map json) => 10 | _$PlaylistImpl( 11 | id: json['id'] as String?, 12 | userName: json['user_name'] as String?, 13 | privacy: (json['privacy'] as num?)?.toInt(), 14 | locale: json['locale'] as String?, 15 | playbackCount: (json['playback_count'] as num?)?.toInt(), 16 | name: json['name'] as String?, 17 | description: json['description'] as String?, 18 | createdAt: json['created_at'] as String?, 19 | updatedAt: json['updated_at'] as String?, 20 | worksCount: (json['works_count'] as num?)?.toInt(), 21 | latestWorkId: json['latestWorkID'], 22 | mainCoverUrl: json['mainCoverUrl'] as String?, 23 | ); 24 | 25 | Map _$$PlaylistImplToJson(_$PlaylistImpl instance) => 26 | { 27 | 'id': instance.id, 28 | 'user_name': instance.userName, 29 | 'privacy': instance.privacy, 30 | 'locale': instance.locale, 31 | 'playback_count': instance.playbackCount, 32 | 'name': instance.name, 33 | 'description': instance.description, 34 | 'created_at': instance.createdAt, 35 | 'updated_at': instance.updatedAt, 36 | 'works_count': instance.worksCount, 37 | 'latestWorkID': instance.latestWorkId, 38 | 'mainCoverUrl': instance.mainCoverUrl, 39 | }; 40 | -------------------------------------------------------------------------------- /lib/data/models/mark_status.dart: -------------------------------------------------------------------------------- 1 | enum MarkStatus { 2 | wantToListen('想听'), 3 | listening('在听'), 4 | listened('听过'), 5 | relistening('重听'), 6 | onHold('搁置'); 7 | 8 | final String label; 9 | const MarkStatus(this.label); 10 | } -------------------------------------------------------------------------------- /lib/data/models/my_lists/README.md: -------------------------------------------------------------------------------- 1 | 虽然已有相似结构,但为了方便管理,还是单独创建一个文件夹,专门用来处理“播放清单”这个页面的东西。 -------------------------------------------------------------------------------- /lib/data/models/my_lists/my_playlists/my_playlists.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'pagination.dart'; 4 | import 'playlist.dart'; 5 | 6 | part 'my_playlists.freezed.dart'; 7 | part 'my_playlists.g.dart'; 8 | 9 | @freezed 10 | class MyPlaylists with _$MyPlaylists { 11 | factory MyPlaylists({ 12 | List? playlists, 13 | Pagination? pagination, 14 | }) = _MyPlaylists; 15 | 16 | factory MyPlaylists.fromJson(Map json) => 17 | _$MyPlaylistsFromJson(json); 18 | } 19 | -------------------------------------------------------------------------------- /lib/data/models/my_lists/my_playlists/my_playlists.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'my_playlists.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$MyPlaylistsImpl _$$MyPlaylistsImplFromJson(Map json) => 10 | _$MyPlaylistsImpl( 11 | playlists: (json['playlists'] as List?) 12 | ?.map((e) => Playlist.fromJson(e as Map)) 13 | .toList(), 14 | pagination: json['pagination'] == null 15 | ? null 16 | : Pagination.fromJson(json['pagination'] as Map), 17 | ); 18 | 19 | Map _$$MyPlaylistsImplToJson(_$MyPlaylistsImpl instance) => 20 | { 21 | 'playlists': instance.playlists, 22 | 'pagination': instance.pagination, 23 | }; 24 | -------------------------------------------------------------------------------- /lib/data/models/my_lists/my_playlists/pagination.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'pagination.freezed.dart'; 4 | part 'pagination.g.dart'; 5 | 6 | @freezed 7 | class Pagination with _$Pagination { 8 | factory Pagination({ 9 | int? page, 10 | int? pageSize, 11 | int? totalCount, 12 | }) = _Pagination; 13 | 14 | factory Pagination.fromJson(Map json) => 15 | _$PaginationFromJson(json); 16 | } 17 | -------------------------------------------------------------------------------- /lib/data/models/my_lists/my_playlists/pagination.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'pagination.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$PaginationImpl _$$PaginationImplFromJson(Map json) => 10 | _$PaginationImpl( 11 | page: (json['page'] as num?)?.toInt(), 12 | pageSize: (json['pageSize'] as num?)?.toInt(), 13 | totalCount: (json['totalCount'] as num?)?.toInt(), 14 | ); 15 | 16 | Map _$$PaginationImplToJson(_$PaginationImpl instance) => 17 | { 18 | 'page': instance.page, 19 | 'pageSize': instance.pageSize, 20 | 'totalCount': instance.totalCount, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/data/models/my_lists/my_playlists/playlist.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'playlist.freezed.dart'; 4 | part 'playlist.g.dart'; 5 | 6 | @freezed 7 | class Playlist with _$Playlist { 8 | factory Playlist({ 9 | String? id, 10 | @JsonKey(name: 'user_name') String? userName, 11 | int? privacy, 12 | String? locale, 13 | @JsonKey(name: 'playback_count') int? playbackCount, 14 | String? name, 15 | String? description, 16 | @JsonKey(name: 'created_at') String? createdAt, 17 | @JsonKey(name: 'updated_at') String? updatedAt, 18 | @JsonKey(name: 'works_count') int? worksCount, 19 | @JsonKey(name: 'latestWorkID') dynamic latestWorkId, 20 | String? mainCoverUrl, 21 | }) = _Playlist; 22 | 23 | factory Playlist.fromJson(Map json) => 24 | _$PlaylistFromJson(json); 25 | } 26 | -------------------------------------------------------------------------------- /lib/data/models/my_lists/my_playlists/playlist.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'playlist.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$PlaylistImpl _$$PlaylistImplFromJson(Map json) => 10 | _$PlaylistImpl( 11 | id: json['id'] as String?, 12 | userName: json['user_name'] as String?, 13 | privacy: (json['privacy'] as num?)?.toInt(), 14 | locale: json['locale'] as String?, 15 | playbackCount: (json['playback_count'] as num?)?.toInt(), 16 | name: json['name'] as String?, 17 | description: json['description'] as String?, 18 | createdAt: json['created_at'] as String?, 19 | updatedAt: json['updated_at'] as String?, 20 | worksCount: (json['works_count'] as num?)?.toInt(), 21 | latestWorkId: json['latestWorkID'], 22 | mainCoverUrl: json['mainCoverUrl'] as String?, 23 | ); 24 | 25 | Map _$$PlaylistImplToJson(_$PlaylistImpl instance) => 26 | { 27 | 'id': instance.id, 28 | 'user_name': instance.userName, 29 | 'privacy': instance.privacy, 30 | 'locale': instance.locale, 31 | 'playback_count': instance.playbackCount, 32 | 'name': instance.name, 33 | 'description': instance.description, 34 | 'created_at': instance.createdAt, 35 | 'updated_at': instance.updatedAt, 36 | 'works_count': instance.worksCount, 37 | 'latestWorkID': instance.latestWorkId, 38 | 'mainCoverUrl': instance.mainCoverUrl, 39 | }; 40 | -------------------------------------------------------------------------------- /lib/data/models/playback/playback_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:asmrapp/data/models/works/work.dart'; 3 | import 'package:asmrapp/data/models/files/files.dart'; 4 | import 'package:asmrapp/data/models/files/child.dart'; 5 | import 'package:asmrapp/core/audio/models/play_mode.dart'; 6 | 7 | part 'playback_state.freezed.dart'; 8 | part 'playback_state.g.dart'; 9 | 10 | @freezed 11 | class PlaybackState with _$PlaybackState { 12 | const factory PlaybackState({ 13 | required Work work, 14 | required Files files, 15 | required Child currentFile, 16 | required List playlist, 17 | required int currentIndex, 18 | required PlayMode playMode, 19 | required int position, // 使用毫秒存储 20 | required String timestamp, // ISO8601 格式 21 | }) = _PlaybackState; 22 | 23 | factory PlaybackState.fromJson(Map json) => 24 | _$PlaybackStateFromJson(json); 25 | } -------------------------------------------------------------------------------- /lib/data/models/playback/playback_state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'playback_state.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$PlaybackStateImpl _$$PlaybackStateImplFromJson(Map json) => 10 | _$PlaybackStateImpl( 11 | work: Work.fromJson(json['work'] as Map), 12 | files: Files.fromJson(json['files'] as Map), 13 | currentFile: Child.fromJson(json['currentFile'] as Map), 14 | playlist: (json['playlist'] as List) 15 | .map((e) => Child.fromJson(e as Map)) 16 | .toList(), 17 | currentIndex: (json['currentIndex'] as num).toInt(), 18 | playMode: $enumDecode(_$PlayModeEnumMap, json['playMode']), 19 | position: (json['position'] as num).toInt(), 20 | timestamp: json['timestamp'] as String, 21 | ); 22 | 23 | Map _$$PlaybackStateImplToJson(_$PlaybackStateImpl instance) => 24 | { 25 | 'work': instance.work, 26 | 'files': instance.files, 27 | 'currentFile': instance.currentFile, 28 | 'playlist': instance.playlist, 29 | 'currentIndex': instance.currentIndex, 30 | 'playMode': _$PlayModeEnumMap[instance.playMode]!, 31 | 'position': instance.position, 32 | 'timestamp': instance.timestamp, 33 | }; 34 | 35 | const _$PlayModeEnumMap = { 36 | PlayMode.single: 'single', 37 | PlayMode.loop: 'loop', 38 | PlayMode.sequence: 'sequence', 39 | }; 40 | -------------------------------------------------------------------------------- /lib/data/models/playlists_with_exist_statu/pagination.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'pagination.freezed.dart'; 4 | part 'pagination.g.dart'; 5 | 6 | @freezed 7 | class Pagination with _$Pagination { 8 | factory Pagination({ 9 | int? page, 10 | int? pageSize, 11 | int? totalCount, 12 | }) = _Pagination; 13 | 14 | factory Pagination.fromJson(Map json) => 15 | _$PaginationFromJson(json); 16 | } 17 | -------------------------------------------------------------------------------- /lib/data/models/playlists_with_exist_statu/pagination.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'pagination.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$PaginationImpl _$$PaginationImplFromJson(Map json) => 10 | _$PaginationImpl( 11 | page: (json['page'] as num?)?.toInt(), 12 | pageSize: (json['pageSize'] as num?)?.toInt(), 13 | totalCount: (json['totalCount'] as num?)?.toInt(), 14 | ); 15 | 16 | Map _$$PaginationImplToJson(_$PaginationImpl instance) => 17 | { 18 | 'page': instance.page, 19 | 'pageSize': instance.pageSize, 20 | 'totalCount': instance.totalCount, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/data/models/playlists_with_exist_statu/playlist.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'playlist.freezed.dart'; 4 | part 'playlist.g.dart'; 5 | 6 | @freezed 7 | class Playlist with _$Playlist { 8 | factory Playlist({ 9 | String? id, 10 | @JsonKey(name: 'user_name') String? userName, 11 | int? privacy, 12 | String? locale, 13 | @JsonKey(name: 'playback_count') int? playbackCount, 14 | String? name, 15 | String? description, 16 | @JsonKey(name: 'created_at') String? createdAt, 17 | @JsonKey(name: 'updated_at') String? updatedAt, 18 | @JsonKey(name: 'works_count') int? worksCount, 19 | bool? exist, 20 | }) = _Playlist; 21 | 22 | factory Playlist.fromJson(Map json) => 23 | _$PlaylistFromJson(json); 24 | } 25 | -------------------------------------------------------------------------------- /lib/data/models/playlists_with_exist_statu/playlist.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'playlist.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$PlaylistImpl _$$PlaylistImplFromJson(Map json) => 10 | _$PlaylistImpl( 11 | id: json['id'] as String?, 12 | userName: json['user_name'] as String?, 13 | privacy: (json['privacy'] as num?)?.toInt(), 14 | locale: json['locale'] as String?, 15 | playbackCount: (json['playback_count'] as num?)?.toInt(), 16 | name: json['name'] as String?, 17 | description: json['description'] as String?, 18 | createdAt: json['created_at'] as String?, 19 | updatedAt: json['updated_at'] as String?, 20 | worksCount: (json['works_count'] as num?)?.toInt(), 21 | exist: json['exist'] as bool?, 22 | ); 23 | 24 | Map _$$PlaylistImplToJson(_$PlaylistImpl instance) => 25 | { 26 | 'id': instance.id, 27 | 'user_name': instance.userName, 28 | 'privacy': instance.privacy, 29 | 'locale': instance.locale, 30 | 'playback_count': instance.playbackCount, 31 | 'name': instance.name, 32 | 'description': instance.description, 33 | 'created_at': instance.createdAt, 34 | 'updated_at': instance.updatedAt, 35 | 'works_count': instance.worksCount, 36 | 'exist': instance.exist, 37 | }; 38 | -------------------------------------------------------------------------------- /lib/data/models/playlists_with_exist_statu/playlists_with_exist_statu.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'pagination.dart'; 4 | import 'playlist.dart'; 5 | 6 | part 'playlists_with_exist_statu.freezed.dart'; 7 | part 'playlists_with_exist_statu.g.dart'; 8 | 9 | @freezed 10 | class PlaylistsWithExistStatu with _$PlaylistsWithExistStatu { 11 | factory PlaylistsWithExistStatu({ 12 | List? playlists, 13 | Pagination? pagination, 14 | }) = _PlaylistsWithExistStatu; 15 | 16 | factory PlaylistsWithExistStatu.fromJson(Map json) => 17 | _$PlaylistsWithExistStatuFromJson(json); 18 | } 19 | -------------------------------------------------------------------------------- /lib/data/models/playlists_with_exist_statu/playlists_with_exist_statu.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'playlists_with_exist_statu.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$PlaylistsWithExistStatuImpl _$$PlaylistsWithExistStatuImplFromJson( 10 | Map json) => 11 | _$PlaylistsWithExistStatuImpl( 12 | playlists: (json['playlists'] as List?) 13 | ?.map((e) => Playlist.fromJson(e as Map)) 14 | .toList(), 15 | pagination: json['pagination'] == null 16 | ? null 17 | : Pagination.fromJson(json['pagination'] as Map), 18 | ); 19 | 20 | Map _$$PlaylistsWithExistStatuImplToJson( 21 | _$PlaylistsWithExistStatuImpl instance) => 22 | { 23 | 'playlists': instance.playlists, 24 | 'pagination': instance.pagination, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/data/models/works/circle.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'circle.freezed.dart'; 4 | part 'circle.g.dart'; 5 | 6 | @freezed 7 | class Circle with _$Circle { 8 | factory Circle({ 9 | int? id, 10 | String? name, 11 | @JsonKey(name: 'source_id') String? sourceId, 12 | @JsonKey(name: 'source_type') String? sourceType, 13 | }) = _Circle; 14 | 15 | factory Circle.fromJson(Map json) => _$CircleFromJson(json); 16 | } 17 | -------------------------------------------------------------------------------- /lib/data/models/works/circle.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'circle.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$CircleImpl _$$CircleImplFromJson(Map json) => _$CircleImpl( 10 | id: (json['id'] as num?)?.toInt(), 11 | name: json['name'] as String?, 12 | sourceId: json['source_id'] as String?, 13 | sourceType: json['source_type'] as String?, 14 | ); 15 | 16 | Map _$$CircleImplToJson(_$CircleImpl instance) => 17 | { 18 | 'id': instance.id, 19 | 'name': instance.name, 20 | 'source_id': instance.sourceId, 21 | 'source_type': instance.sourceType, 22 | }; 23 | -------------------------------------------------------------------------------- /lib/data/models/works/en_us.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'en_us.freezed.dart'; 4 | part 'en_us.g.dart'; 5 | 6 | @freezed 7 | class EnUs with _$EnUs { 8 | factory EnUs({ 9 | String? name, 10 | List? history, 11 | }) = _EnUs; 12 | 13 | factory EnUs.fromJson(Map json) => _$EnUsFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /lib/data/models/works/en_us.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'en_us.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$EnUsImpl _$$EnUsImplFromJson(Map json) => _$EnUsImpl( 10 | name: json['name'] as String?, 11 | history: json['history'] as List?, 12 | ); 13 | 14 | Map _$$EnUsImplToJson(_$EnUsImpl instance) => 15 | { 16 | 'name': instance.name, 17 | 'history': instance.history, 18 | }; 19 | -------------------------------------------------------------------------------- /lib/data/models/works/i18n.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'en_us.dart'; 4 | import 'ja_jp.dart'; 5 | import 'zh_cn.dart'; 6 | 7 | part 'i18n.freezed.dart'; 8 | part 'i18n.g.dart'; 9 | 10 | @freezed 11 | class I18n with _$I18n { 12 | factory I18n({ 13 | @JsonKey(name: 'en-us') EnUs? enUs, 14 | @JsonKey(name: 'ja-jp') JaJp? jaJp, 15 | @JsonKey(name: 'zh-cn') ZhCn? zhCn, 16 | }) = _I18n; 17 | 18 | factory I18n.fromJson(Map json) => _$I18nFromJson(json); 19 | } 20 | -------------------------------------------------------------------------------- /lib/data/models/works/i18n.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'i18n.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$I18nImpl _$$I18nImplFromJson(Map json) => _$I18nImpl( 10 | enUs: json['en-us'] == null 11 | ? null 12 | : EnUs.fromJson(json['en-us'] as Map), 13 | jaJp: json['ja-jp'] == null 14 | ? null 15 | : JaJp.fromJson(json['ja-jp'] as Map), 16 | zhCn: json['zh-cn'] == null 17 | ? null 18 | : ZhCn.fromJson(json['zh-cn'] as Map), 19 | ); 20 | 21 | Map _$$I18nImplToJson(_$I18nImpl instance) => 22 | { 23 | 'en-us': instance.enUs, 24 | 'ja-jp': instance.jaJp, 25 | 'zh-cn': instance.zhCn, 26 | }; 27 | -------------------------------------------------------------------------------- /lib/data/models/works/ja_jp.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'ja_jp.freezed.dart'; 4 | part 'ja_jp.g.dart'; 5 | 6 | @freezed 7 | class JaJp with _$JaJp { 8 | factory JaJp({ 9 | String? name, 10 | }) = _JaJp; 11 | 12 | factory JaJp.fromJson(Map json) => _$JaJpFromJson(json); 13 | } 14 | -------------------------------------------------------------------------------- /lib/data/models/works/ja_jp.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'ja_jp.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$JaJpImpl _$$JaJpImplFromJson(Map json) => _$JaJpImpl( 10 | name: json['name'] as String?, 11 | ); 12 | 13 | Map _$$JaJpImplToJson(_$JaJpImpl instance) => 14 | { 15 | 'name': instance.name, 16 | }; 17 | -------------------------------------------------------------------------------- /lib/data/models/works/language_edition.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'language_edition.freezed.dart'; 4 | part 'language_edition.g.dart'; 5 | 6 | @freezed 7 | class LanguageEdition with _$LanguageEdition { 8 | factory LanguageEdition({ 9 | String? lang, 10 | String? label, 11 | String? workno, 12 | @JsonKey(name: 'edition_id') int? editionId, 13 | @JsonKey(name: 'edition_type') String? editionType, 14 | @JsonKey(name: 'display_order') int? displayOrder, 15 | }) = _LanguageEdition; 16 | 17 | factory LanguageEdition.fromJson(Map json) => 18 | _$LanguageEditionFromJson(json); 19 | } 20 | -------------------------------------------------------------------------------- /lib/data/models/works/language_edition.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'language_edition.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$LanguageEditionImpl _$$LanguageEditionImplFromJson( 10 | Map json) => 11 | _$LanguageEditionImpl( 12 | lang: json['lang'] as String?, 13 | label: json['label'] as String?, 14 | workno: json['workno'] as String?, 15 | editionId: (json['edition_id'] as num?)?.toInt(), 16 | editionType: json['edition_type'] as String?, 17 | displayOrder: (json['display_order'] as num?)?.toInt(), 18 | ); 19 | 20 | Map _$$LanguageEditionImplToJson( 21 | _$LanguageEditionImpl instance) => 22 | { 23 | 'lang': instance.lang, 24 | 'label': instance.label, 25 | 'workno': instance.workno, 26 | 'edition_id': instance.editionId, 27 | 'edition_type': instance.editionType, 28 | 'display_order': instance.displayOrder, 29 | }; 30 | -------------------------------------------------------------------------------- /lib/data/models/works/other_language_editions_in_db.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'other_language_editions_in_db.freezed.dart'; 4 | part 'other_language_editions_in_db.g.dart'; 5 | 6 | @freezed 7 | class OtherLanguageEditionsInDb with _$OtherLanguageEditionsInDb { 8 | factory OtherLanguageEditionsInDb({ 9 | int? id, 10 | String? lang, 11 | String? title, 12 | @JsonKey(name: 'source_id') String? sourceId, 13 | @JsonKey(name: 'is_original') bool? isOriginal, 14 | @JsonKey(name: 'source_type') String? sourceType, 15 | }) = _OtherLanguageEditionsInDb; 16 | 17 | factory OtherLanguageEditionsInDb.fromJson(Map json) => 18 | _$OtherLanguageEditionsInDbFromJson(json); 19 | } 20 | -------------------------------------------------------------------------------- /lib/data/models/works/other_language_editions_in_db.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'other_language_editions_in_db.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$OtherLanguageEditionsInDbImpl _$$OtherLanguageEditionsInDbImplFromJson( 10 | Map json) => 11 | _$OtherLanguageEditionsInDbImpl( 12 | id: (json['id'] as num?)?.toInt(), 13 | lang: json['lang'] as String?, 14 | title: json['title'] as String?, 15 | sourceId: json['source_id'] as String?, 16 | isOriginal: json['is_original'] as bool?, 17 | sourceType: json['source_type'] as String?, 18 | ); 19 | 20 | Map _$$OtherLanguageEditionsInDbImplToJson( 21 | _$OtherLanguageEditionsInDbImpl instance) => 22 | { 23 | 'id': instance.id, 24 | 'lang': instance.lang, 25 | 'title': instance.title, 26 | 'source_id': instance.sourceId, 27 | 'is_original': instance.isOriginal, 28 | 'source_type': instance.sourceType, 29 | }; 30 | -------------------------------------------------------------------------------- /lib/data/models/works/pagination.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'pagination.freezed.dart'; 4 | part 'pagination.g.dart'; 5 | 6 | @freezed 7 | class Pagination with _$Pagination { 8 | factory Pagination({ 9 | int? currentPage, 10 | int? pageSize, 11 | int? totalCount, 12 | }) = _Pagination; 13 | 14 | factory Pagination.fromJson(Map json) => 15 | _$PaginationFromJson(json); 16 | } 17 | -------------------------------------------------------------------------------- /lib/data/models/works/pagination.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'pagination.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$PaginationImpl _$$PaginationImplFromJson(Map json) => 10 | _$PaginationImpl( 11 | currentPage: (json['currentPage'] as num?)?.toInt(), 12 | pageSize: (json['pageSize'] as num?)?.toInt(), 13 | totalCount: (json['totalCount'] as num?)?.toInt(), 14 | ); 15 | 16 | Map _$$PaginationImplToJson(_$PaginationImpl instance) => 17 | { 18 | 'currentPage': instance.currentPage, 19 | 'pageSize': instance.pageSize, 20 | 'totalCount': instance.totalCount, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/data/models/works/tag.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'i18n.dart'; 4 | 5 | part 'tag.freezed.dart'; 6 | part 'tag.g.dart'; 7 | 8 | @freezed 9 | class Tag with _$Tag { 10 | factory Tag({ 11 | int? id, 12 | I18n? i18n, 13 | String? name, 14 | }) = _Tag; 15 | 16 | factory Tag.fromJson(Map json) => _$TagFromJson(json); 17 | } 18 | -------------------------------------------------------------------------------- /lib/data/models/works/tag.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'tag.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$TagImpl _$$TagImplFromJson(Map json) => _$TagImpl( 10 | id: (json['id'] as num?)?.toInt(), 11 | i18n: json['i18n'] == null 12 | ? null 13 | : I18n.fromJson(json['i18n'] as Map), 14 | name: json['name'] as String?, 15 | ); 16 | 17 | Map _$$TagImplToJson(_$TagImpl instance) => { 18 | 'id': instance.id, 19 | 'i18n': instance.i18n, 20 | 'name': instance.name, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/data/models/works/translation_bonus_lang.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'translation_bonus_lang.freezed.dart'; 4 | part 'translation_bonus_lang.g.dart'; 5 | 6 | @freezed 7 | class TranslationBonusLang with _$TranslationBonusLang { 8 | factory TranslationBonusLang({ 9 | int? price, 10 | String? status, 11 | @JsonKey(name: 'price_tax') int? priceTax, 12 | @JsonKey(name: 'child_count') int? childCount, 13 | @JsonKey(name: 'price_in_tax') int? priceInTax, 14 | @JsonKey(name: 'recipient_max') int? recipientMax, 15 | @JsonKey(name: 'recipient_available_count') int? recipientAvailableCount, 16 | }) = _TranslationBonusLang; 17 | 18 | factory TranslationBonusLang.fromJson(Map json) => 19 | _$TranslationBonusLangFromJson(json); 20 | } 21 | -------------------------------------------------------------------------------- /lib/data/models/works/translation_bonus_lang.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'translation_bonus_lang.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$TranslationBonusLangImpl _$$TranslationBonusLangImplFromJson( 10 | Map json) => 11 | _$TranslationBonusLangImpl( 12 | price: (json['price'] as num?)?.toInt(), 13 | status: json['status'] as String?, 14 | priceTax: (json['price_tax'] as num?)?.toInt(), 15 | childCount: (json['child_count'] as num?)?.toInt(), 16 | priceInTax: (json['price_in_tax'] as num?)?.toInt(), 17 | recipientMax: (json['recipient_max'] as num?)?.toInt(), 18 | recipientAvailableCount: 19 | (json['recipient_available_count'] as num?)?.toInt(), 20 | ); 21 | 22 | Map _$$TranslationBonusLangImplToJson( 23 | _$TranslationBonusLangImpl instance) => 24 | { 25 | 'price': instance.price, 26 | 'status': instance.status, 27 | 'price_tax': instance.priceTax, 28 | 'child_count': instance.childCount, 29 | 'price_in_tax': instance.priceInTax, 30 | 'recipient_max': instance.recipientMax, 31 | 'recipient_available_count': instance.recipientAvailableCount, 32 | }; 33 | -------------------------------------------------------------------------------- /lib/data/models/works/translation_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'translation_bonus_lang.dart'; 3 | 4 | part 'translation_info.freezed.dart'; 5 | part 'translation_info.g.dart'; 6 | 7 | @freezed 8 | class TranslationInfo with _$TranslationInfo { 9 | factory TranslationInfo({ 10 | String? lang, 11 | @JsonKey(name: 'is_child') bool? isChild, 12 | @JsonKey(name: 'is_parent') bool? isParent, 13 | @JsonKey(name: 'is_original') bool? isOriginal, 14 | @JsonKey(name: 'is_volunteer') bool? isVolunteer, 15 | @JsonKey(name: 'child_worknos') List? childWorknos, 16 | @JsonKey(name: 'parent_workno') String? parentWorkno, 17 | @JsonKey(name: 'original_workno') String? originalWorkno, 18 | @JsonKey(name: 'is_translation_agree') bool? isTranslationAgree, 19 | @JsonKey( 20 | name: 'translation_bonus_langs', 21 | fromJson: _translationBonusLangsFromJson, 22 | toJson: _translationBonusLangsToJson, 23 | ) 24 | Map? translationBonusLangs, 25 | @JsonKey(name: 'is_translation_bonus_child') bool? isTranslationBonusChild, 26 | @JsonKey(name: 'production_trade_price_rate') int? productionTradePriceRate, 27 | }) = _TranslationInfo; 28 | 29 | factory TranslationInfo.fromJson(Map json) => 30 | _$TranslationInfoFromJson(json); 31 | } 32 | 33 | Map? _translationBonusLangsFromJson( 34 | dynamic json) { 35 | if (json == null) return null; 36 | if (json is List && json.isEmpty) return {}; 37 | 38 | if (json is Map) { 39 | return json.map((key, value) => MapEntry( 40 | key, 41 | TranslationBonusLang.fromJson(value as Map), 42 | )); 43 | } 44 | 45 | return {}; 46 | } 47 | 48 | dynamic _translationBonusLangsToJson(Map? map) { 49 | if (map == null) return null; 50 | if (map.isEmpty) return []; 51 | 52 | return map.map((key, value) => MapEntry(key, value.toJson())); 53 | } 54 | -------------------------------------------------------------------------------- /lib/data/models/works/translation_info.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'translation_info.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$TranslationInfoImpl _$$TranslationInfoImplFromJson( 10 | Map json) => 11 | _$TranslationInfoImpl( 12 | lang: json['lang'] as String?, 13 | isChild: json['is_child'] as bool?, 14 | isParent: json['is_parent'] as bool?, 15 | isOriginal: json['is_original'] as bool?, 16 | isVolunteer: json['is_volunteer'] as bool?, 17 | childWorknos: json['child_worknos'] as List?, 18 | parentWorkno: json['parent_workno'] as String?, 19 | originalWorkno: json['original_workno'] as String?, 20 | isTranslationAgree: json['is_translation_agree'] as bool?, 21 | translationBonusLangs: 22 | _translationBonusLangsFromJson(json['translation_bonus_langs']), 23 | isTranslationBonusChild: json['is_translation_bonus_child'] as bool?, 24 | productionTradePriceRate: 25 | (json['production_trade_price_rate'] as num?)?.toInt(), 26 | ); 27 | 28 | Map _$$TranslationInfoImplToJson( 29 | _$TranslationInfoImpl instance) => 30 | { 31 | 'lang': instance.lang, 32 | 'is_child': instance.isChild, 33 | 'is_parent': instance.isParent, 34 | 'is_original': instance.isOriginal, 35 | 'is_volunteer': instance.isVolunteer, 36 | 'child_worknos': instance.childWorknos, 37 | 'parent_workno': instance.parentWorkno, 38 | 'original_workno': instance.originalWorkno, 39 | 'is_translation_agree': instance.isTranslationAgree, 40 | 'translation_bonus_langs': 41 | _translationBonusLangsToJson(instance.translationBonusLangs), 42 | 'is_translation_bonus_child': instance.isTranslationBonusChild, 43 | 'production_trade_price_rate': instance.productionTradePriceRate, 44 | }; 45 | -------------------------------------------------------------------------------- /lib/data/models/works/work.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'circle.dart'; 4 | import 'language_edition.dart'; 5 | import 'other_language_editions_in_db.dart'; 6 | import 'tag.dart'; 7 | import 'translation_info.dart'; 8 | 9 | part 'work.freezed.dart'; 10 | part 'work.g.dart'; 11 | 12 | @freezed 13 | class Work with _$Work { 14 | factory Work({ 15 | int? id, 16 | String? title, 17 | @JsonKey(name: 'circle_id') int? circleId, 18 | String? name, 19 | bool? nsfw, 20 | String? release, 21 | @JsonKey(name: 'dl_count') int? dlCount, 22 | int? price, 23 | @JsonKey(name: 'review_count') int? reviewCount, 24 | @JsonKey(name: 'rate_count') int? rateCount, 25 | @JsonKey(name: 'rate_average_2dp') int? rateAverage2dp, 26 | @JsonKey(name: 'rate_count_detail') List? rateCountDetail, 27 | dynamic rank, 28 | @JsonKey(name: 'has_subtitle') bool? hasSubtitle, 29 | @JsonKey(name: 'create_date') String? createDate, 30 | List? vas, 31 | List? tags, 32 | @JsonKey(name: 'language_editions') List? languageEditions, 33 | @JsonKey(name: 'original_workno') String? originalWorkno, 34 | @JsonKey(name: 'other_language_editions_in_db') 35 | List? otherLanguageEditionsInDb, 36 | @JsonKey(name: 'translation_info') TranslationInfo? translationInfo, 37 | @JsonKey(name: 'work_attributes') String? workAttributes, 38 | @JsonKey(name: 'age_category_string') String? ageCategoryString, 39 | int? duration, 40 | @JsonKey(name: 'source_type') String? sourceType, 41 | @JsonKey(name: 'source_id') String? sourceId, 42 | @JsonKey(name: 'source_url') String? sourceUrl, 43 | dynamic userRating, 44 | Circle? circle, 45 | String? samCoverUrl, 46 | String? thumbnailCoverUrl, 47 | String? mainCoverUrl, 48 | }) = _Work; 49 | 50 | factory Work.fromJson(Map json) => _$WorkFromJson(json); 51 | } 52 | -------------------------------------------------------------------------------- /lib/data/models/works/works.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'pagination.dart'; 4 | import 'work.dart'; 5 | 6 | part 'works.freezed.dart'; 7 | part 'works.g.dart'; 8 | 9 | @freezed 10 | class Works with _$Works { 11 | factory Works({ 12 | List? works, 13 | Pagination? pagination, 14 | }) = _Works; 15 | 16 | factory Works.fromJson(Map json) => _$WorksFromJson(json); 17 | } 18 | -------------------------------------------------------------------------------- /lib/data/models/works/works.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'works.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$WorksImpl _$$WorksImplFromJson(Map json) => _$WorksImpl( 10 | works: (json['works'] as List?) 11 | ?.map((e) => Work.fromJson(e as Map)) 12 | .toList(), 13 | pagination: json['pagination'] == null 14 | ? null 15 | : Pagination.fromJson(json['pagination'] as Map), 16 | ); 17 | 18 | Map _$$WorksImplToJson(_$WorksImpl instance) => 19 | { 20 | 'works': instance.works, 21 | 'pagination': instance.pagination, 22 | }; 23 | -------------------------------------------------------------------------------- /lib/data/models/works/zh_cn.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'zh_cn.freezed.dart'; 4 | part 'zh_cn.g.dart'; 5 | 6 | @freezed 7 | class ZhCn with _$ZhCn { 8 | factory ZhCn({ 9 | String? name, 10 | List? history, 11 | }) = _ZhCn; 12 | 13 | factory ZhCn.fromJson(Map json) => _$ZhCnFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /lib/data/models/works/zh_cn.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'zh_cn.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$ZhCnImpl _$$ZhCnImplFromJson(Map json) => _$ZhCnImpl( 10 | name: json['name'] as String?, 11 | history: json['history'] as List?, 12 | ); 13 | 14 | Map _$$ZhCnImplToJson(_$ZhCnImpl instance) => 15 | { 16 | 'name': instance.name, 17 | 'history': instance.history, 18 | }; 19 | -------------------------------------------------------------------------------- /lib/data/repositories/audio/README.md: -------------------------------------------------------------------------------- 1 | # 音频数据仓库 2 | 3 | 此目录包含音频数据访问的仓库实现。 4 | 5 | ## 文件结构 6 | 7 | - `audio_repository.dart` - 音频数据仓库实现 8 | - `audio_repository_impl.dart` - 音频数据仓库具体实现 9 | - `audio_cache_repository.dart` - 音频缓存仓库 10 | 11 | ## 职责 12 | 13 | - 音频数据的获取和存储 14 | - 播放历史记录的管理 15 | - 音频缓存的处理 -------------------------------------------------------------------------------- /lib/data/repositories/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | import 'package:asmrapp/data/models/auth/auth_resp/auth_resp.dart'; 4 | import 'package:asmrapp/utils/logger.dart'; 5 | 6 | class AuthRepository { 7 | static const _authDataKey = 'auth_data'; 8 | final SharedPreferences _prefs; 9 | 10 | AuthRepository(this._prefs); 11 | 12 | Future saveAuthData(AuthResp authData) async { 13 | try { 14 | final jsonStr = json.encode(authData.toJson()); 15 | await _prefs.setString(_authDataKey, jsonStr); 16 | AppLogger.info('保存认证数据成功'); 17 | } catch (e) { 18 | AppLogger.error('保存认证数据失败', e); 19 | rethrow; 20 | } 21 | } 22 | 23 | Future getAuthData() async { 24 | try { 25 | final jsonStr = _prefs.getString(_authDataKey); 26 | if (jsonStr == null) return null; 27 | 28 | final authData = AuthResp.fromJson(json.decode(jsonStr)); 29 | AppLogger.info('读取认证数据成功: ${authData.user?.name}'); 30 | return authData; 31 | } catch (e) { 32 | AppLogger.error('读取认证数据失败', e); 33 | return null; 34 | } 35 | } 36 | 37 | Future clearAuthData() async { 38 | try { 39 | await _prefs.remove(_authDataKey); 40 | AppLogger.info('清除认证数据成功'); 41 | } catch (e) { 42 | AppLogger.error('清除认证数据失败', e); 43 | rethrow; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /lib/data/services/auth_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:asmrapp/data/models/auth/auth_resp/auth_resp.dart'; 2 | import 'package:dio/dio.dart'; 3 | import '../../utils/logger.dart'; 4 | 5 | class AuthService { 6 | final Dio _dio; 7 | 8 | AuthService() 9 | : _dio = Dio(BaseOptions( 10 | baseUrl: 'https://api.asmr.one/api', 11 | )); 12 | 13 | Future login(String name, String password) async { 14 | try { 15 | AppLogger.info('开始登录请求: name=$name'); 16 | final response = await _dio.post('/auth/me', 17 | data: { 18 | 'name': name, 19 | 'password': password, 20 | }, 21 | ); 22 | 23 | AppLogger.info('收到登录响应: statusCode=${response.statusCode}'); 24 | AppLogger.info('响应数据: ${response.data}'); 25 | 26 | if (response.statusCode == 200) { 27 | final authResp = AuthResp.fromJson(response.data); 28 | AppLogger.info('登录成功: username=${authResp.user?.name}, group=${authResp.user?.group}'); 29 | return authResp; 30 | } 31 | 32 | throw Exception('登录失败: ${response.statusCode}'); 33 | } on DioException catch (e) { 34 | AppLogger.error('登录请求失败', e); 35 | AppLogger.error('错误详情: ${e.response?.data}'); 36 | throw Exception('网络请求失败: ${e.message}'); 37 | } catch (e) { 38 | AppLogger.error('登录失败', e); 39 | throw Exception('登录失败: $e'); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /lib/data/services/interceptors/auth_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:get_it/get_it.dart'; 3 | import 'package:asmrapp/data/repositories/auth_repository.dart'; 4 | import 'package:asmrapp/utils/logger.dart'; 5 | 6 | class AuthInterceptor extends Interceptor { 7 | @override 8 | Future onRequest( 9 | RequestOptions options, 10 | RequestInterceptorHandler handler, 11 | ) async { 12 | try { 13 | final authRepository = GetIt.I(); 14 | final authData = await authRepository.getAuthData(); 15 | 16 | if (authData?.token != null) { 17 | options.headers['Authorization'] = 'Bearer ${authData!.token}'; 18 | } 19 | 20 | handler.next(options); 21 | } catch (e) { 22 | AppLogger.error('AuthInterceptor: 处理请求失败', e); 23 | handler.next(options); // 即使出错也继续请求 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:asmrapp/common/constants/strings.dart'; 3 | import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; 4 | import 'core/di/service_locator.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'screens/main_screen.dart'; 7 | import 'package:asmrapp/core/theme/app_theme.dart'; 8 | import 'package:asmrapp/core/theme/theme_controller.dart'; 9 | import 'screens/search_screen.dart'; 10 | 11 | void main() async { 12 | WidgetsFlutterBinding.ensureInitialized(); 13 | 14 | // 初始化服务定位器 15 | await setupServiceLocator(); 16 | 17 | runApp(const MyApp()); 18 | } 19 | 20 | class MyApp extends StatelessWidget { 21 | const MyApp({super.key}); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return MultiProvider( 26 | providers: [ 27 | ChangeNotifierProvider( 28 | create: (_) => getIt(), 29 | ), 30 | ChangeNotifierProvider( 31 | create: (_) => getIt(), 32 | ), 33 | ], 34 | child: Consumer( 35 | builder: (context, themeController, child) { 36 | return MaterialApp( 37 | title: Strings.appName, 38 | theme: AppTheme.light, 39 | darkTheme: AppTheme.dark, 40 | themeMode: themeController.themeMode, 41 | home: const MainScreen(), 42 | routes: { 43 | // '/player': (context) => const PlayerScreen(), 44 | '/search': (context) { 45 | final keyword = 46 | ModalRoute.of(context)?.settings.arguments as String?; 47 | return SearchScreen(initialKeyword: keyword); 48 | }, 49 | }, 50 | ); 51 | }, 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/presentation/layouts/work_layout_strategy.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:asmrapp/data/models/works/work.dart'; 3 | import 'package:asmrapp/presentation/layouts/work_layout_config.dart'; 4 | 5 | /// 作品布局策略 6 | class WorkLayoutStrategy { 7 | const WorkLayoutStrategy(); 8 | 9 | /// 获取设备类型 10 | DeviceType _getDeviceType(BuildContext context) { 11 | return DeviceType.fromWidth(MediaQuery.of(context).size.width); 12 | } 13 | 14 | /// 获取每行的列数 15 | int getColumnsCount(BuildContext context) { 16 | return WorkLayoutConfig.getColumnsCount(_getDeviceType(context)); 17 | } 18 | 19 | /// 获取行间距 20 | double getRowSpacing(BuildContext context) { 21 | return WorkLayoutConfig.getSpacing(_getDeviceType(context)); 22 | } 23 | 24 | /// 获取列间距 25 | double getColumnSpacing(BuildContext context) { 26 | return WorkLayoutConfig.getSpacing(_getDeviceType(context)); 27 | } 28 | 29 | /// 获取内边距 30 | EdgeInsets getPadding(BuildContext context) { 31 | return WorkLayoutConfig.getPadding(_getDeviceType(context)); 32 | } 33 | 34 | /// 将作品列表分组为行 35 | List> groupWorksIntoRows(List works, int columnsCount) { 36 | final List> rows = []; 37 | for (var i = 0; i < works.length; i += columnsCount) { 38 | final end = i + columnsCount; 39 | rows.add(works.sublist(i, end > works.length ? works.length : end)); 40 | } 41 | return rows; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/presentation/models/filter_state.dart: -------------------------------------------------------------------------------- 1 | class FilterState { 2 | final String orderField; 3 | final bool isDescending; 4 | 5 | const FilterState({ 6 | this.orderField = 'create_date', 7 | this.isDescending = true, 8 | }); 9 | 10 | bool get showSortDirection => orderField != 'random'; 11 | 12 | String get sortValue => orderField == 'random' ? 'desc' : (isDescending ? 'desc' : 'asc'); 13 | 14 | FilterState copyWith({ 15 | String? orderField, 16 | bool? isDescending, 17 | }) { 18 | return FilterState( 19 | orderField: orderField ?? this.orderField, 20 | isDescending: isDescending ?? this.isDescending, 21 | ); 22 | } 23 | 24 | // 用于持久化 25 | Map toJson() => { 26 | 'orderField': orderField, 27 | 'isDescending': isDescending, 28 | }; 29 | 30 | // 从持久化恢复 31 | factory FilterState.fromJson(Map json) => FilterState( 32 | orderField: json['orderField'] ?? 'create_date', 33 | isDescending: json['isDescending'] ?? true, 34 | ); 35 | } -------------------------------------------------------------------------------- /lib/presentation/viewmodels/favorites_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:asmrapp/data/models/works/work.dart'; 3 | import 'package:asmrapp/data/models/works/pagination.dart'; 4 | import 'package:asmrapp/data/services/api_service.dart'; 5 | import 'package:asmrapp/utils/logger.dart'; 6 | import 'package:get_it/get_it.dart'; 7 | 8 | class FavoritesViewModel extends ChangeNotifier { 9 | final ApiService _apiService; 10 | List _works = []; 11 | bool _isLoading = false; 12 | String? _error; 13 | Pagination? _pagination; 14 | int _currentPage = 1; 15 | 16 | FavoritesViewModel() : _apiService = GetIt.I(); 17 | 18 | List get works => _works; 19 | bool get isLoading => _isLoading; 20 | String? get error => _error; 21 | int get currentPage => _currentPage; 22 | int? get totalPages => 23 | _pagination?.totalCount != null && _pagination?.pageSize != null 24 | ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() 25 | : null; 26 | 27 | /// 加载指定页面的数据 28 | Future loadPage(int page) async { 29 | if (_isLoading) return; 30 | if (page < 1 || (totalPages != null && page > totalPages!)) return; 31 | 32 | _isLoading = true; 33 | _error = null; 34 | notifyListeners(); 35 | 36 | try { 37 | final response = await _apiService.getFavorites(page: page); 38 | _works = response.works; 39 | _pagination = response.pagination; 40 | _currentPage = page; 41 | AppLogger.info('第$page页收藏列表加载成功: ${response.works.length}个作品'); 42 | } catch (e) { 43 | AppLogger.error('加载收藏列表失败', e); 44 | _error = e.toString(); 45 | } finally { 46 | _isLoading = false; 47 | notifyListeners(); 48 | } 49 | } 50 | 51 | /// 加载收藏列表(用于初始加载和刷新) 52 | Future loadFavorites({bool refresh = false}) async { 53 | await loadPage(1); 54 | } 55 | } -------------------------------------------------------------------------------- /lib/presentation/viewmodels/playlist_works_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:asmrapp/data/models/my_lists/my_playlists/playlist.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:asmrapp/data/models/works/work.dart'; 4 | import 'package:asmrapp/data/models/works/pagination.dart'; 5 | import 'package:asmrapp/data/services/api_service.dart'; 6 | import 'package:asmrapp/utils/logger.dart'; 7 | import 'package:get_it/get_it.dart'; 8 | 9 | class PlaylistWorksViewModel extends ChangeNotifier { 10 | final ApiService _apiService = GetIt.I(); 11 | final Playlist playlist; 12 | 13 | List _works = []; 14 | bool _isLoading = false; 15 | String? _error; 16 | Pagination? _pagination; 17 | int _currentPage = 1; 18 | 19 | PlaylistWorksViewModel(this.playlist); 20 | 21 | List get works => _works; 22 | bool get isLoading => _isLoading; 23 | String? get error => _error; 24 | int get currentPage => _currentPage; 25 | int? get totalPages => _pagination?.totalCount != null && _pagination?.pageSize != null 26 | ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() 27 | : null; 28 | 29 | Future loadWorks({int page = 1}) async { 30 | if (_isLoading) return; 31 | if (page < 1 || (totalPages != null && page > totalPages!)) return; 32 | 33 | _isLoading = true; 34 | _error = null; 35 | notifyListeners(); 36 | 37 | try { 38 | final response = await _apiService.getPlaylistWorks( 39 | playlistId: playlist.id!, 40 | page: page, 41 | ); 42 | 43 | _works = response.works; 44 | _pagination = response.pagination; 45 | _currentPage = page; 46 | AppLogger.info('第$page页播放列表作品加载成功: ${response.works.length}个作品'); 47 | } catch (e) { 48 | AppLogger.error('加载播放列表作品失败', e); 49 | _error = e.toString(); 50 | } finally { 51 | _isLoading = false; 52 | notifyListeners(); 53 | } 54 | } 55 | 56 | Future refresh() => loadWorks(page: 1); 57 | } 58 | -------------------------------------------------------------------------------- /lib/screens/contents/playlists_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:asmrapp/data/models/my_lists/my_playlists/playlist.dart'; 4 | import 'package:asmrapp/screens/contents/playlists/playlists_list_view.dart'; 5 | import 'package:asmrapp/screens/contents/playlists/playlist_works_view.dart'; 6 | import 'package:asmrapp/presentation/viewmodels/playlists_viewmodel.dart'; 7 | 8 | class PlaylistsContent extends StatefulWidget { 9 | const PlaylistsContent({super.key}); 10 | 11 | @override 12 | State createState() => _PlaylistsContentState(); 13 | } 14 | 15 | class _PlaylistsContentState extends State with AutomaticKeepAliveClientMixin { 16 | Playlist? _selectedPlaylist; 17 | 18 | @override 19 | bool get wantKeepAlive => true; 20 | 21 | void _handlePlaylistSelected(Playlist playlist) { 22 | setState(() { 23 | _selectedPlaylist = playlist; 24 | }); 25 | } 26 | 27 | void _handleBack() { 28 | setState(() { 29 | _selectedPlaylist = null; 30 | }); 31 | } 32 | 33 | Future _onWillPop() async { 34 | if (_selectedPlaylist != null) { 35 | _handleBack(); 36 | return false; 37 | } 38 | return true; 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | super.build(context); 44 | 45 | return PopScope( 46 | canPop: _selectedPlaylist == null, 47 | onPopInvokedWithResult: (didPop, result) { 48 | if (!didPop) { 49 | _handleBack(); 50 | } 51 | }, 52 | child: _selectedPlaylist != null 53 | ? PlaylistWorksView( 54 | playlist: _selectedPlaylist!, 55 | onBack: _handleBack, 56 | ) 57 | : PlaylistsListView( 58 | onPlaylistSelected: _handlePlaylistSelected, 59 | ), 60 | ); 61 | } 62 | } -------------------------------------------------------------------------------- /lib/screens/docs/main_screen.md: -------------------------------------------------------------------------------- 1 | # 应用架构说明 2 | 3 | ## MainScreen 架构 4 | 5 | ### 概述 6 | MainScreen 采用集中式的状态管理架构,作为应用的主要页面容器,它负责: 7 | 1. 管理所有主要页面的 ViewModel 8 | 2. 提供统一的状态管理入口 9 | 3. 确保 ViewModel 的单一实例 10 | 11 | ### 核心原则 12 | 13 | 1. **ViewModel 单一实例** 14 | - 所有页面的 ViewModel 都在 MainScreen 中初始化 15 | - 子页面通过 Provider 获取 ViewModel,不创建自己的实例 16 | - 确保状态的一致性和可预测性 17 | 18 | 2. **状态提供机制** 19 | - 使用 MultiProvider 在顶层提供所有 ViewModel 20 | - 子页面使用 context.read 或 Provider.of 获取 ViewModel 21 | - 避免重复创建 ViewModel 实例 22 | 23 | 3. **生命周期管理** 24 | - MainScreen 负责 ViewModel 的创建和销毁 25 | - 在 initState 中初始化所有 ViewModel 26 | - 在 dispose 中释放所有资源 27 | 28 | ### 子页面开发指南 29 | 30 | 1. **ViewModel 访问** ```dart 31 | // 推荐使用 context.read 获取 ViewModel 32 | final viewModel = context.read(); 33 | 34 | // 或者使用 Provider.of(效果相同) 35 | final viewModel = Provider.of(context, listen: false); ``` 36 | 37 | 2. **状态监听** ```dart 38 | // 使用 Consumer 监听状态变化 39 | Consumer( 40 | builder: (context, viewModel, child) { 41 | // 使用 viewModel 的状态 42 | }, 43 | ) ``` 44 | 45 | 3. **注意事项** 46 | - 不要在子页面中创建新的 ViewModel 实例 47 | - 使用 AutomaticKeepAliveClientMixin 保持页面状态 48 | - 在 initState 中进行必要的初始化 49 | 50 | ### 常见问题 51 | 52 | 1. **重复实例问题** 53 | - 症状:状态更新不生效 54 | - 原因:子页面创建了新的 ViewModel 实例 55 | - 解决:使用 MainScreen 提供的 ViewModel 56 | 57 | 2. **状态同步问题** 58 | - 症状:不同页面状态不同步 59 | - 原因:使用了多个 ViewModel 实例 60 | - 解决:确保使用 MainScreen 提供的单一实例 -------------------------------------------------------------------------------- /lib/utils/file_size_formatter.dart: -------------------------------------------------------------------------------- 1 | class FileSizeFormatter { 2 | static String format(int? size) { 3 | if (size == null) return ''; 4 | const kb = 1024; 5 | const mb = kb * 1024; 6 | if (size > mb) { 7 | return '${(size / mb).toStringAsFixed(2)} MB'; 8 | } 9 | return '${(size / kb).toStringAsFixed(2)} KB'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/utils/logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | 3 | class AppLogger { 4 | static final Logger _logger = Logger( 5 | printer: PrettyPrinter( 6 | methodCount: 0, 7 | errorMethodCount: 8, 8 | lineLength: 120, 9 | colors: true, 10 | printEmojis: true, 11 | printTime: true, 12 | ), 13 | ); 14 | 15 | static void init() { 16 | Logger.level = Level.debug; 17 | } 18 | 19 | static void debug(String message) => _logger.d(message); 20 | static void info(String message) => _logger.i(message); 21 | static void warning(String message) => _logger.w(message); 22 | static void error(String message, [Object? error, StackTrace? stackTrace]) => 23 | _logger.e(message, error: error, stackTrace: stackTrace); 24 | } 25 | -------------------------------------------------------------------------------- /lib/widgets/common/tag_chip.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TagChip extends StatelessWidget { 4 | final String text; 5 | final Color? backgroundColor; 6 | final Color? textColor; 7 | final VoidCallback? onTap; 8 | 9 | const TagChip({ 10 | super.key, 11 | required this.text, 12 | this.backgroundColor, 13 | this.textColor, 14 | this.onTap, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return InkWell( 20 | onTap: onTap, 21 | borderRadius: BorderRadius.circular(16), 22 | child: Container( 23 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 24 | decoration: BoxDecoration( 25 | color: backgroundColor ?? Theme.of(context).colorScheme.surfaceContainerHighest, 26 | borderRadius: BorderRadius.circular(4), 27 | ), 28 | child: Text( 29 | text, 30 | style: Theme.of(context).textTheme.bodyMedium?.copyWith( 31 | color: textColor ?? Theme.of(context).colorScheme.onSurfaceVariant, 32 | fontSize: 13, 33 | ), 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/widgets/detail/work_file_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:asmrapp/data/models/files/child.dart'; 3 | import 'package:asmrapp/utils/logger.dart'; 4 | import 'package:asmrapp/utils/file_size_formatter.dart'; 5 | 6 | class WorkFileItem extends StatelessWidget { 7 | final Child file; 8 | final double indentation; 9 | final Function(Child file)? onFileTap; 10 | 11 | const WorkFileItem({ 12 | super.key, 13 | required this.file, 14 | required this.indentation, 15 | this.onFileTap, 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | final bool isAudio = file.type?.toLowerCase() == 'audio'; 21 | final colorScheme = Theme.of(context).colorScheme; 22 | 23 | return Padding( 24 | padding: EdgeInsets.only(left: indentation), 25 | child: ListTile( 26 | title: Text( 27 | file.title ?? '', 28 | style: TextStyle( 29 | color: colorScheme.onSurface, 30 | ), 31 | ), 32 | subtitle: Text( 33 | FileSizeFormatter.format(file.size), 34 | style: TextStyle( 35 | color: colorScheme.onSurfaceVariant, 36 | ), 37 | ), 38 | leading: Icon( 39 | isAudio ? Icons.audio_file : Icons.insert_drive_file, 40 | color: isAudio ? Colors.green : Colors.blue, 41 | ), 42 | dense: true, 43 | onTap: isAudio ? () { 44 | AppLogger.debug('点击音频文件: ${file.title}'); 45 | onFileTap?.call(file); 46 | } : null, 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/widgets/detail/work_files_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:asmrapp/data/models/files/files.dart'; 3 | import 'package:asmrapp/data/models/files/child.dart'; 4 | import 'package:asmrapp/widgets/detail/work_folder_item.dart'; 5 | import 'package:asmrapp/widgets/detail/work_file_item.dart'; 6 | 7 | class WorkFilesList extends StatelessWidget { 8 | final Files files; 9 | final Function(Child file)? onFileTap; 10 | 11 | const WorkFilesList({ 12 | super.key, 13 | required this.files, 14 | this.onFileTap, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | // 重置文件夹展开状态 20 | WorkFolderItem.resetExpandState(); 21 | 22 | return Card( 23 | margin: const EdgeInsets.all(8), 24 | child: Column( 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | children: [ 27 | Padding( 28 | padding: const EdgeInsets.all(16), 29 | child: Text( 30 | '文件列表', 31 | style: Theme.of(context).textTheme.titleMedium?.copyWith( 32 | fontWeight: FontWeight.bold, 33 | ), 34 | ), 35 | ), 36 | Divider( 37 | height: 1, 38 | color: Theme.of(context).colorScheme.surfaceVariant, 39 | ), 40 | ...files.children 41 | ?.map((child) => child.type == 'folder' 42 | ? WorkFolderItem( 43 | folder: child, 44 | indentation: 0, 45 | onFileTap: onFileTap, 46 | ) 47 | : WorkFileItem( 48 | file: child, 49 | indentation: 0, 50 | onFileTap: onFileTap, 51 | )) 52 | .toList() ?? 53 | [], 54 | ], 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/widgets/detail/work_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:asmrapp/data/models/works/work.dart'; 3 | import 'package:asmrapp/data/models/works/tag.dart'; 4 | import 'package:asmrapp/widgets/common/tag_chip.dart'; 5 | import 'package:asmrapp/widgets/detail/work_info_header.dart'; 6 | import 'package:asmrapp/utils/logger.dart'; 7 | 8 | class WorkInfo extends StatelessWidget { 9 | final Work work; 10 | 11 | const WorkInfo({ 12 | super.key, 13 | required this.work, 14 | }); 15 | 16 | String _getLocalizedTagName(Tag tag) { 17 | final zhName = tag.i18n?.zhCn?.name; 18 | if (zhName != null) return zhName; 19 | final jaName = tag.i18n?.jaJp?.name; 20 | if (jaName != null) return jaName; 21 | return tag.name ?? ''; 22 | } 23 | 24 | void _onTagTap(BuildContext context, Tag tag) { 25 | final keyword = tag.name ?? ''; 26 | if (keyword.isEmpty) return; 27 | 28 | AppLogger.debug('点击标签: $keyword'); 29 | Navigator.pushNamed( 30 | context, 31 | '/search', 32 | arguments: keyword, 33 | ); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return Padding( 39 | padding: const EdgeInsets.all(16.0), 40 | child: Column( 41 | crossAxisAlignment: CrossAxisAlignment.start, 42 | children: [ 43 | WorkInfoHeader(work: work), 44 | const SizedBox(height: 8), 45 | if (work.tags != null && work.tags!.isNotEmpty) 46 | Wrap( 47 | spacing: 8, 48 | runSpacing: 8, 49 | children: work.tags! 50 | .map((tag) => TagChip( 51 | text: _getLocalizedTagName(tag), 52 | onTap: () => _onTagTap(context, tag), 53 | )) 54 | .toList(), 55 | ), 56 | ], 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/widgets/detail/work_stats_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:asmrapp/data/models/works/work.dart'; 3 | 4 | class WorkStatsInfo extends StatelessWidget { 5 | final Work work; 6 | 7 | const WorkStatsInfo({ 8 | super.key, 9 | required this.work, 10 | }); 11 | 12 | String _formatDuration(int? seconds) { 13 | if (seconds == null) return ''; 14 | final duration = Duration(seconds: seconds); 15 | final hours = duration.inHours; 16 | final minutes = duration.inMinutes.remainder(60); 17 | 18 | if (hours > 0) { 19 | return '${hours}h ${minutes}m'; 20 | } else { 21 | return '${minutes}m'; 22 | } 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Row( 28 | children: [ 29 | if (work.duration != null) ...[ 30 | Icon( 31 | Icons.access_time, 32 | size: 16, 33 | color: Theme.of(context).colorScheme.onSurfaceVariant, 34 | ), 35 | const SizedBox(width: 4), 36 | Text( 37 | _formatDuration(work.duration), 38 | style: Theme.of(context).textTheme.bodyMedium?.copyWith( 39 | color: Theme.of(context).colorScheme.onSurfaceVariant, 40 | ), 41 | ), 42 | const SizedBox(width: 16), 43 | ], 44 | if ((work.rateCount ?? 0) > 0) ...[ 45 | const Icon(Icons.star, size: 16, color: Colors.amber), 46 | const SizedBox(width: 4), 47 | Text( 48 | (work.rateAverage2dp ?? 0.0).toStringAsFixed(1), 49 | style: Theme.of(context).textTheme.bodyMedium, 50 | ), 51 | const SizedBox(width: 16), 52 | ], 53 | Icon( 54 | Icons.download, 55 | size: 16, 56 | color: Theme.of(context).colorScheme.onSurfaceVariant, 57 | ), 58 | const SizedBox(width: 4), 59 | Text( 60 | '${work.dlCount ?? 0}', 61 | style: Theme.of(context).textTheme.bodyMedium?.copyWith( 62 | color: Theme.of(context).colorScheme.onSurfaceVariant, 63 | ), 64 | ), 65 | ], 66 | ); 67 | } 68 | } -------------------------------------------------------------------------------- /lib/widgets/lyrics/components/lyric_line.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:asmrapp/core/audio/models/subtitle.dart'; 3 | 4 | class LyricLine extends StatelessWidget { 5 | final Subtitle subtitle; 6 | final bool isActive; 7 | final double opacity; 8 | final VoidCallback? onTap; 9 | 10 | const LyricLine({ 11 | super.key, 12 | required this.subtitle, 13 | this.isActive = false, 14 | this.opacity = 1.0, 15 | this.onTap, 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Center( 21 | child: AnimatedOpacity( 22 | duration: const Duration(milliseconds: 300), 23 | opacity: opacity, 24 | child: GestureDetector( 25 | behavior: HitTestBehavior.translucent, 26 | onTap: onTap, 27 | child: Padding( 28 | padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), 29 | child: Text( 30 | subtitle.text, 31 | style: Theme.of(context).textTheme.bodyLarge?.copyWith( 32 | fontSize: 20, 33 | height: 1.3, 34 | color: isActive 35 | ? Theme.of(context).colorScheme.primary 36 | : Theme.of(context).colorScheme.onSurface.withOpacity(0.7), 37 | fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, 38 | ), 39 | textAlign: TextAlign.center, 40 | ), 41 | ), 42 | ), 43 | ), 44 | ); 45 | } 46 | } -------------------------------------------------------------------------------- /lib/widgets/mini_player/mini_player_controls.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; 3 | import 'package:get_it/get_it.dart'; 4 | 5 | class MiniPlayerControls extends StatelessWidget { 6 | const MiniPlayerControls({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | final viewModel = GetIt.I(); 11 | return ListenableBuilder( 12 | listenable: viewModel, 13 | builder: (context, _) { 14 | return IconButton( 15 | icon: Icon( 16 | viewModel.isPlaying ? Icons.pause : Icons.play_arrow, 17 | ), 18 | onPressed: viewModel.playPause, 19 | ); 20 | }, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/widgets/mini_player/mini_player_cover.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:cached_network_image/cached_network_image.dart'; 3 | import 'package:shimmer/shimmer.dart'; 4 | 5 | class MiniPlayerCover extends StatelessWidget { 6 | final String? coverUrl; 7 | final double size; 8 | 9 | const MiniPlayerCover({ 10 | super.key, 11 | this.coverUrl, 12 | this.size = 48, 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | if (coverUrl == null) { 18 | return _buildEmptyPlaceholder(); 19 | } 20 | 21 | return ClipRRect( 22 | borderRadius: BorderRadius.circular(4), 23 | child: CachedNetworkImage( 24 | imageUrl: coverUrl!, 25 | width: size, 26 | height: size, 27 | fit: BoxFit.cover, 28 | placeholder: (context, url) => _buildPlaceholder(context), 29 | errorWidget: (context, url, error) => _buildErrorWidget(), 30 | ), 31 | ); 32 | } 33 | 34 | Widget _buildEmptyPlaceholder() { 35 | return Container( 36 | width: size, 37 | height: size, 38 | decoration: BoxDecoration( 39 | color: Colors.grey[200], 40 | borderRadius: BorderRadius.circular(4), 41 | ), 42 | child: const Icon(Icons.music_note, color: Colors.grey), 43 | ); 44 | } 45 | 46 | Widget _buildPlaceholder(BuildContext context) { 47 | return Shimmer.fromColors( 48 | baseColor: Theme.of(context).colorScheme.surfaceContainerHighest, 49 | highlightColor: Theme.of(context).colorScheme.surface, 50 | child: Container( 51 | width: size, 52 | height: size, 53 | decoration: BoxDecoration( 54 | color: Colors.white, 55 | borderRadius: BorderRadius.circular(4), 56 | ), 57 | ), 58 | ); 59 | } 60 | 61 | Widget _buildErrorWidget() { 62 | return Container( 63 | width: size, 64 | height: size, 65 | decoration: BoxDecoration( 66 | color: Colors.grey[300], 67 | borderRadius: BorderRadius.circular(4), 68 | ), 69 | child: const Icon(Icons.broken_image, color: Colors.grey), 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/widgets/mini_player/mini_player_progress.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get_it/get_it.dart'; 3 | import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; 4 | 5 | class MiniPlayerProgress extends StatelessWidget { 6 | const MiniPlayerProgress({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | final viewModel = GetIt.I(); 11 | return ListenableBuilder( 12 | listenable: viewModel, 13 | builder: (context, _) { 14 | final position = viewModel.position?.inMilliseconds.toDouble() ?? 0.0; 15 | final duration = viewModel.duration?.inMilliseconds.toDouble() ?? 0.0; 16 | final progress = duration > 0 ? position / duration : 0.0; 17 | 18 | return SizedBox( 19 | height: 2, 20 | child: LinearProgressIndicator( 21 | value: progress, 22 | backgroundColor: 23 | Theme.of(context).colorScheme.surfaceContainerHighest, 24 | valueColor: AlwaysStoppedAnimation( 25 | Theme.of(context).colorScheme.primary, 26 | ), 27 | ), 28 | ); 29 | }, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/widgets/pagination_controls.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class PaginationControls extends StatelessWidget { 4 | final int currentPage; 5 | final int? totalPages; 6 | final bool isLoading; 7 | final Function(int) onPageChanged; 8 | 9 | const PaginationControls({ 10 | super.key, 11 | required this.currentPage, 12 | required this.totalPages, 13 | required this.isLoading, 14 | required this.onPageChanged, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Container( 20 | padding: const EdgeInsets.all(16), 21 | child: Row( 22 | mainAxisAlignment: MainAxisAlignment.center, 23 | children: [ 24 | const SizedBox(width: 32), 25 | IconButton( 26 | onPressed: currentPage > 1 && !isLoading 27 | ? () => onPageChanged(currentPage - 1) 28 | : null, 29 | icon: const Icon(Icons.chevron_left), 30 | ), 31 | const SizedBox(width: 16), 32 | Text('$currentPage/${totalPages ?? "?"}'), 33 | const SizedBox(width: 16), 34 | IconButton( 35 | onPressed: 36 | totalPages != null && currentPage < totalPages! && !isLoading 37 | ? () => onPageChanged(currentPage + 1) 38 | : null, 39 | icon: const Icon(Icons.chevron_right), 40 | ), 41 | SizedBox( 42 | width: 32, 43 | child: isLoading 44 | ? const Padding( 45 | padding: EdgeInsets.only(left: 16), 46 | child: SizedBox( 47 | width: 16, 48 | height: 16, 49 | child: CircularProgressIndicator( 50 | strokeWidth: 2, 51 | ), 52 | ), 53 | ) 54 | : null, 55 | ), 56 | ], 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/widgets/player/player_controls.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get_it/get_it.dart'; 3 | import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; 4 | 5 | class PlayerControls extends StatelessWidget { 6 | const PlayerControls({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | final viewModel = GetIt.I(); 11 | 12 | return ListenableBuilder( 13 | listenable: viewModel, 14 | builder: (context, _) { 15 | return Row( 16 | mainAxisAlignment: MainAxisAlignment.center, 17 | children: [ 18 | IconButton( 19 | iconSize: 32, 20 | icon: const Icon(Icons.skip_previous), 21 | onPressed: viewModel.previous, 22 | ), 23 | const SizedBox(width: 16), 24 | Container( 25 | width: 64, 26 | height: 64, 27 | decoration: BoxDecoration( 28 | shape: BoxShape.circle, 29 | color: Theme.of(context).primaryColor, 30 | ), 31 | child: IconButton( 32 | iconSize: 32, 33 | color: Colors.white, 34 | icon: Icon( 35 | viewModel.isPlaying ? Icons.pause : Icons.play_arrow, 36 | ), 37 | onPressed: viewModel.playPause, 38 | ), 39 | ), 40 | const SizedBox(width: 16), 41 | IconButton( 42 | iconSize: 32, 43 | icon: const Icon(Icons.skip_next), 44 | onPressed: viewModel.next, 45 | ), 46 | ], 47 | ); 48 | }, 49 | ); 50 | } 51 | } -------------------------------------------------------------------------------- /lib/widgets/player/player_work_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:marquee/marquee.dart'; 3 | import 'package:asmrapp/core/audio/models/playback_context.dart'; 4 | 5 | class PlayerWorkInfo extends StatelessWidget { 6 | final PlaybackContext? context; 7 | 8 | const PlayerWorkInfo({ 9 | super.key, 10 | required this.context, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | width: double.infinity, 17 | padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), 18 | child: Column( 19 | crossAxisAlignment: CrossAxisAlignment.start, 20 | children: [ 21 | SizedBox( 22 | height: Theme.of(context).textTheme.titleMedium!.fontSize! * 1.5, 23 | child: Marquee( 24 | text: this.context?.work.title ?? '未知作品', 25 | style: Theme.of(context).textTheme.titleMedium?.copyWith( 26 | fontWeight: FontWeight.w600, 27 | ), 28 | scrollAxis: Axis.horizontal, 29 | crossAxisAlignment: CrossAxisAlignment.start, 30 | blankSpace: 50.0, 31 | velocity: 30.0, 32 | pauseAfterRound: const Duration(seconds: 2), 33 | startPadding: 10.0, 34 | accelerationDuration: const Duration(seconds: 1), 35 | accelerationCurve: Curves.linear, 36 | decelerationDuration: const Duration(milliseconds: 500), 37 | decelerationCurve: Curves.easeOut, 38 | ), 39 | ), 40 | const SizedBox(height: 2), 41 | Text( 42 | this.context?.work.vas 43 | ?.map((va) => va['name'] as String?) 44 | .where((name) => name != null) 45 | .join('、') ?? 46 | '未知演员', 47 | style: Theme.of(context).textTheme.bodyMedium?.copyWith( 48 | color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), 49 | ), 50 | maxLines: 1, 51 | overflow: TextOverflow.ellipsis, 52 | ), 53 | ], 54 | ), 55 | ); 56 | } 57 | } -------------------------------------------------------------------------------- /lib/widgets/work_card/components/work_footer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:asmrapp/data/models/works/work.dart'; 3 | 4 | class WorkFooter extends StatelessWidget { 5 | final Work work; 6 | 7 | const WorkFooter({ 8 | super.key, 9 | required this.work, 10 | }); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Row( 15 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 16 | children: [ 17 | Text( 18 | work.release ?? '', 19 | style: Theme.of(context).textTheme.bodySmall?.copyWith( 20 | fontSize: 10, 21 | ), 22 | ), 23 | Text( 24 | '销量 ${work.dlCount ?? 0}', 25 | style: Theme.of(context).textTheme.bodySmall?.copyWith( 26 | fontSize: 10, 27 | ), 28 | ), 29 | ], 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/widgets/work_card/components/work_info_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:asmrapp/data/models/works/work.dart'; 3 | import 'work_title.dart'; 4 | import 'work_tags_panel.dart'; 5 | import 'work_footer.dart'; 6 | 7 | class WorkInfoSection extends StatelessWidget { 8 | final Work work; 9 | 10 | const WorkInfoSection({ 11 | super.key, 12 | required this.work, 13 | }); 14 | 15 | String _formatDuration(int? seconds) { 16 | if (seconds == null) return ''; 17 | final duration = Duration(seconds: seconds); 18 | final hours = duration.inHours; 19 | final minutes = duration.inMinutes.remainder(60); 20 | 21 | if (hours > 0) { 22 | return '${hours}h ${minutes}m'; 23 | } else { 24 | return '${minutes}m'; 25 | } 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return Padding( 31 | padding: const EdgeInsets.all(8.0), 32 | child: Column( 33 | crossAxisAlignment: CrossAxisAlignment.start, 34 | children: [ 35 | WorkTitle(work: work), 36 | const SizedBox(height: 4), 37 | Row( 38 | children: [ 39 | if (work.duration != null) ...[ 40 | Icon( 41 | Icons.access_time, 42 | size: 14, 43 | color: Theme.of(context).colorScheme.onSurfaceVariant, 44 | ), 45 | const SizedBox(width: 4), 46 | Text( 47 | _formatDuration(work.duration), 48 | style: Theme.of(context).textTheme.bodyMedium?.copyWith( 49 | fontSize: 12, 50 | color: Theme.of(context).colorScheme.onSurfaceVariant, 51 | ), 52 | ), 53 | ], 54 | ], 55 | ), 56 | const SizedBox(height: 8), 57 | WorkTagsPanel(work: work), 58 | const SizedBox(height: 4), 59 | const Spacer(), 60 | WorkFooter(work: work), 61 | ], 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/widgets/work_card/components/work_title.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:asmrapp/data/models/works/work.dart'; 3 | 4 | class WorkTitle extends StatelessWidget { 5 | final Work work; 6 | 7 | const WorkTitle({ 8 | super.key, 9 | required this.work, 10 | }); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Text( 15 | work.title ?? '', 16 | style: Theme.of(context).textTheme.titleMedium?.copyWith( 17 | fontSize: 14, 18 | ), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/widgets/work_card/work_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:asmrapp/data/models/works/work.dart'; 3 | import 'components/work_cover_image.dart'; 4 | import 'components/work_info_section.dart'; 5 | 6 | class WorkCard extends StatelessWidget { 7 | final Work work; 8 | final VoidCallback? onTap; 9 | 10 | const WorkCard({ 11 | super.key, 12 | required this.work, 13 | this.onTap, 14 | }); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final isDark = Theme.of(context).brightness == Brightness.dark; 19 | 20 | return Card( 21 | clipBehavior: Clip.antiAlias, 22 | elevation: isDark ? 0 : 1, 23 | color: isDark 24 | ? Theme.of(context).colorScheme.surfaceVariant 25 | : Theme.of(context).colorScheme.surface, 26 | child: InkWell( 27 | onTap: onTap, 28 | child: Column( 29 | crossAxisAlignment: CrossAxisAlignment.start, 30 | children: [ 31 | WorkCoverImage( 32 | imageUrl: work.mainCoverUrl ?? '', 33 | workId: work.id ?? 0, 34 | sourceId: work.sourceId ?? '', 35 | ), 36 | Expanded( 37 | child: WorkInfoSection(work: work), 38 | ), 39 | ], 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/widgets/work_grid.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:asmrapp/data/models/works/work.dart'; 3 | import 'package:asmrapp/widgets/work_row.dart'; 4 | import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; 5 | 6 | class WorkGrid extends StatelessWidget { 7 | final List works; 8 | final void Function(Work work)? onWorkTap; 9 | final WorkLayoutStrategy layoutStrategy; 10 | 11 | const WorkGrid({ 12 | super.key, 13 | required this.works, 14 | this.onWorkTap, 15 | this.layoutStrategy = const WorkLayoutStrategy(), 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | final columnsCount = layoutStrategy.getColumnsCount(context); 21 | final rows = layoutStrategy.groupWorksIntoRows(works, columnsCount); 22 | final rowSpacing = layoutStrategy.getRowSpacing(context); 23 | final columnSpacing = layoutStrategy.getColumnSpacing(context); 24 | 25 | return SliverList( 26 | delegate: SliverChildBuilderDelegate( 27 | (context, index) { 28 | if (index >= rows.length) return null; 29 | return Padding( 30 | padding: EdgeInsets.only( 31 | bottom: index < rows.length - 1 ? rowSpacing : 0), 32 | child: WorkRow( 33 | works: rows[index], 34 | onWorkTap: onWorkTap, 35 | spacing: columnSpacing, 36 | ), 37 | ); 38 | }, 39 | childCount: rows.length, 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/widgets/work_grid/components/grid_empty.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class GridEmpty extends StatelessWidget { 4 | final String? message; 5 | final Widget? customWidget; 6 | 7 | const GridEmpty({ 8 | super.key, 9 | this.message, 10 | this.customWidget, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | if (customWidget != null) { 16 | return customWidget!; 17 | } 18 | 19 | return Center( 20 | child: Column( 21 | mainAxisAlignment: MainAxisAlignment.center, 22 | children: [ 23 | Icon( 24 | Icons.inbox_outlined, 25 | size: 48, 26 | color: Theme.of(context).colorScheme.outline, 27 | ), 28 | const SizedBox(height: 16), 29 | Text( 30 | message ?? '暂无内容', 31 | style: Theme.of(context).textTheme.bodyLarge?.copyWith( 32 | color: Theme.of(context).colorScheme.outline, 33 | ), 34 | ), 35 | ], 36 | ), 37 | ); 38 | } 39 | } -------------------------------------------------------------------------------- /lib/widgets/work_grid/components/grid_error.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class GridError extends StatelessWidget { 4 | final String error; 5 | final VoidCallback? onRetry; 6 | 7 | const GridError({ 8 | super.key, 9 | required this.error, 10 | this.onRetry, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Center( 16 | child: Column( 17 | mainAxisAlignment: MainAxisAlignment.center, 18 | children: [ 19 | Icon( 20 | Icons.error_outline, 21 | size: 48, 22 | color: Theme.of(context).colorScheme.error, 23 | ), 24 | const SizedBox(height: 16), 25 | Text( 26 | error, 27 | style: Theme.of(context).textTheme.bodyLarge, 28 | textAlign: TextAlign.center, 29 | ), 30 | if (onRetry != null) ...[ 31 | const SizedBox(height: 16), 32 | FilledButton.icon( 33 | onPressed: onRetry, 34 | icon: const Icon(Icons.refresh), 35 | label: const Text('重试'), 36 | ), 37 | ], 38 | ], 39 | ), 40 | ); 41 | } 42 | } -------------------------------------------------------------------------------- /lib/widgets/work_grid/components/grid_loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shimmer/shimmer.dart'; 3 | 4 | class GridLoading extends StatelessWidget { 5 | const GridLoading({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Shimmer.fromColors( 10 | baseColor: Theme.of(context).colorScheme.surfaceContainerHighest, 11 | highlightColor: Theme.of(context).colorScheme.surface, 12 | child: GridView.builder( 13 | padding: const EdgeInsets.all(16), 14 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 15 | crossAxisCount: 2, 16 | childAspectRatio: 0.75, 17 | crossAxisSpacing: 16, 18 | mainAxisSpacing: 16, 19 | ), 20 | itemCount: 6, 21 | itemBuilder: (context, index) { 22 | return Container( 23 | decoration: BoxDecoration( 24 | color: Colors.white, 25 | borderRadius: BorderRadius.circular(8), 26 | ), 27 | ); 28 | }, 29 | ), 30 | ); 31 | } 32 | } -------------------------------------------------------------------------------- /lib/widgets/work_grid/models/grid_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class GridConfig { 4 | final ScrollPhysics? physics; 5 | final bool enablePagination; 6 | final bool showLoadingOnEmpty; 7 | final Duration scrollDuration; 8 | final Curve scrollCurve; 9 | final EdgeInsets? padding; 10 | 11 | const GridConfig({ 12 | this.physics, 13 | this.enablePagination = true, 14 | this.showLoadingOnEmpty = true, 15 | this.scrollDuration = const Duration(milliseconds: 300), 16 | this.scrollCurve = Curves.easeOut, 17 | this.padding, 18 | }); 19 | 20 | static const GridConfig defaultConfig = GridConfig(); 21 | } -------------------------------------------------------------------------------- /lib/widgets/work_row.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:asmrapp/data/models/works/work.dart'; 3 | import 'package:asmrapp/widgets/work_card/work_card.dart'; 4 | 5 | class WorkRow extends StatelessWidget { 6 | final List works; 7 | final void Function(Work work)? onWorkTap; 8 | final double spacing; 9 | 10 | const WorkRow({ 11 | super.key, 12 | required this.works, 13 | this.onWorkTap, 14 | this.spacing = 8.0, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return IntrinsicHeight( 20 | child: Row( 21 | crossAxisAlignment: CrossAxisAlignment.stretch, 22 | children: [ 23 | // 第一个卡片 24 | Expanded( 25 | child: works.isNotEmpty 26 | ? WorkCard( 27 | work: works[0], 28 | onTap: onWorkTap != null ? () => onWorkTap!(works[0]) : null, 29 | ) 30 | : const SizedBox.shrink(), 31 | ), 32 | SizedBox(width: spacing), 33 | // 第二个卡片或占位符 34 | Expanded( 35 | child: works.length > 1 36 | ? WorkCard( 37 | work: works[1], 38 | onTap: onWorkTap != null ? () => onWorkTap!(works[1]) : null, 39 | ) 40 | : const SizedBox.shrink(), // 空占位符,保持两列布局 41 | ), 42 | ], 43 | ), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | 10 | void fl_register_plugins(FlPluginRegistry* registry) { 11 | } 12 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | ) 7 | 8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 9 | ) 10 | 11 | set(PLUGIN_BUNDLED_LIBRARIES) 12 | 13 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 18 | endforeach(plugin) 19 | 20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 23 | endforeach(ffi_plugin) 24 | -------------------------------------------------------------------------------- /linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import audio_service 9 | import audio_session 10 | import just_audio 11 | import package_info_plus 12 | import path_provider_foundation 13 | import shared_preferences_foundation 14 | import sqflite_darwin 15 | import wakelock_plus 16 | 17 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 18 | AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) 19 | AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) 20 | JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) 21 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 22 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 23 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 24 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 25 | WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) 26 | } 27 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = asmrapp 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.asmrapp 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:asmrapp/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | asmrapp 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Yuro", 3 | "short_name": "Yuro", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | 11 | void RegisterPlugins(flutter::PluginRegistry* registry) { 12 | PermissionHandlerWindowsPluginRegisterWithRegistrar( 13 | registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); 14 | } 15 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | permission_handler_windows 7 | ) 8 | 9 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 10 | ) 11 | 12 | set(PLUGIN_BUNDLED_LIBRARIES) 13 | 14 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 15 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 16 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 19 | endforeach(plugin) 20 | 21 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 24 | endforeach(ffi_plugin) 25 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"asmrapp", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmroneapp/Yuro/2817087af4a04007b0f02ab44fa19a0d226c2433/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | unsigned int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length == 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | --------------------------------------------------------------------------------