├── .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