├── lib ├── models │ ├── follow_user_item.dart │ ├── version_model.dart │ ├── db │ │ ├── follow_user_tag.dart │ │ ├── follow_user_tag.g.dart │ │ ├── history.dart │ │ ├── follow_user.dart │ │ ├── history.g.dart │ │ └── follow_user.g.dart │ ├── sync_client_info_model.dart │ └── account │ │ ├── douyin_user_info.dart │ │ └── bilibili_user_info_page.dart ├── widgets │ ├── ui │ │ └── after_post_frame.dart │ ├── keep_alive_wrapper.dart │ ├── settings │ │ ├── settings_card.dart │ │ ├── settings_switch.dart │ │ ├── settings_action.dart │ │ └── settings_menu.dart │ ├── status │ │ ├── app_loadding_widget.dart │ │ ├── app_loading_widget.dart │ │ ├── app_empty_widget.dart │ │ └── app_error_widget.dart │ ├── filter_button.dart │ ├── desktop_refresh_button.dart │ ├── shadow_card.dart │ ├── net_image.dart │ └── none_border_circular_textfield.dart ├── modules │ ├── home │ │ ├── home_list_controller.dart │ │ ├── home_list_view.dart │ │ ├── home_controller.dart │ │ └── home_page.dart │ ├── category │ │ ├── detail │ │ │ ├── category_detail_controller.dart │ │ │ └── category_detail_page.dart │ │ ├── category_list_controller.dart │ │ ├── category_controller.dart │ │ └── category_page.dart │ ├── settings │ │ ├── danmu_shield │ │ │ └── danmu_shield_controller.dart │ │ └── indexed_settings │ │ │ └── indexed_settings_controller.dart │ ├── mine │ │ ├── history │ │ │ └── history_controller.dart │ │ └── account │ │ │ └── bilibili │ │ │ ├── web_login_page.dart │ │ │ └── web_login_controller.dart │ ├── live_room │ │ └── player │ │ │ ├── base_player.dart │ │ │ └── player_states.dart │ ├── search │ │ ├── search_list_controller.dart │ │ ├── douyin │ │ │ └── douyin_search_controller.dart │ │ └── search_controller.dart │ ├── sync │ │ ├── remote_sync │ │ │ └── webdav │ │ │ │ └── webdav_client.dart │ │ └── local_sync │ │ │ └── scan_qr │ │ │ └── sync_scan_qr_controller.dart │ ├── test │ │ └── test_page.dart │ ├── other │ │ └── debug_log_page.dart │ └── indexed │ │ └── indexed_controller.dart ├── app │ ├── custom_throttle.dart │ ├── utils │ │ ├── archive.dart │ │ ├── duration_2str.dart │ │ ├── listen_fourth_button.dart │ │ └── document.dart │ ├── event_bus.dart │ ├── constant.dart │ └── sites.dart ├── requests │ ├── http_error.dart │ ├── webdav_client.dart │ └── common_request.dart ├── routes │ └── route_path.dart └── services │ ├── history_service.dart │ ├── douyin_account_service.dart │ └── bilibili_account_service.dart ├── linux ├── .gitignore ├── runner │ ├── main.cc │ ├── my_application.h │ └── CMakeLists.txt ├── packaging │ ├── appimage │ │ └── make_config.yaml │ └── deb │ │ └── make_config.yaml └── flutter │ ├── generated_plugin_registrant.h │ ├── generated_plugins.cmake │ └── generated_plugin_registrant.cc ├── .fvmrc ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── feature.yml └── workflows │ └── checks.yaml ├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── icon-29.png │ │ │ ├── icon-40.png │ │ │ ├── icon-76.png │ │ │ ├── icon-1024.png │ │ │ ├── icon-20@2x.png │ │ │ ├── icon-20@3x.png │ │ │ ├── icon-29@2x.png │ │ │ ├── icon-29@3x.png │ │ │ ├── icon-40@2x.png │ │ │ ├── icon-40@3x.png │ │ │ ├── icon-60@2x.png │ │ │ ├── icon-60@3x.png │ │ │ ├── icon-76@2x.png │ │ │ ├── icon-20-ipad.png │ │ │ ├── icon-29-ipad.png │ │ │ ├── icon-83.5@2x.png │ │ │ ├── icon-20@2x-ipad.png │ │ │ └── icon-29@2x-ipad.png │ │ └── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ ├── AppDelegate.swift │ └── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist └── .gitignore ├── assets ├── logo.png ├── logo_400.png ├── images │ ├── huya.png │ ├── logo.png │ ├── douyin.png │ ├── douyu.png │ ├── bilibili.png │ └── bilibili_2.png ├── logo_circle.png ├── icons │ ├── app_icon.ico │ ├── icon_resume.svg │ ├── icon_pause.svg │ ├── icon_fullscreen.svg │ ├── icon_refresh.svg │ ├── icon_volume.svg │ └── icon_scalewindow.svg ├── screenshot_dark.png ├── screenshot_light.png ├── app_version.json └── statement.txt ├── packages └── simple_live_core │ ├── README.md │ ├── lib │ ├── src │ │ ├── common │ │ │ ├── convert_helper.dart │ │ │ ├── core_error.dart │ │ │ └── core_log.dart │ │ ├── model │ │ │ ├── live_category_result.dart │ │ │ ├── live_play_url.dart │ │ │ ├── live_play_quality.dart │ │ │ ├── live_search_result.dart │ │ │ ├── live_anchor_item.dart │ │ │ ├── live_room_item.dart │ │ │ ├── live_category.dart │ │ │ ├── tars │ │ │ │ ├── get_cdn_token_req.dart │ │ │ │ └── get_cdn_token_resp.dart │ │ │ └── live_room_detail.dart │ │ ├── interface │ │ │ └── live_danmaku.dart │ │ └── danmaku │ │ │ └── proto │ │ │ └── douyin.pbenum.dart │ └── simple_live_core.dart │ ├── pubspec.yaml │ └── .gitignore ├── macos ├── Runner │ ├── Configs │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ ├── Warnings.xcconfig │ │ └── AppInfo.xcconfig │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── icon-128.png │ │ │ ├── icon-16.png │ │ │ ├── icon-256.png │ │ │ ├── icon-32.png │ │ │ ├── icon-512.png │ │ │ ├── icon-16@2x.png │ │ │ ├── icon-32@2x.png │ │ │ ├── icon-128@2x.png │ │ │ ├── icon-256@2x.png │ │ │ ├── icon-512@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── MainFlutterWindow.swift │ ├── Release.entitlements │ ├── DebugProfile.entitlements │ └── Info.plist ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── packaging │ └── dmg │ │ └── make_config.yaml ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner.xcodeproj │ └── project.xcworkspace │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── RunnerTests │ └── RunnerTests.swift └── Podfile ├── windows ├── runner │ ├── resources │ │ └── app_icon.ico │ ├── resource.h │ ├── runner.exe.manifest │ ├── utils.h │ ├── flutter_window.h │ ├── main.cpp │ ├── CMakeLists.txt │ ├── utils.cpp │ └── flutter_window.cpp ├── packaging │ ├── msix │ │ └── make_config.yaml │ └── exe │ │ ├── make_config.yaml │ │ └── inno_setup.iss ├── .gitignore └── flutter │ ├── generated_plugin_registrant.h │ ├── generated_plugins.cmake │ └── generated_plugin_registrant.cc ├── android ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── values │ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── xml │ │ │ │ │ └── network_security_config.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── xycz │ │ │ │ └── simple_live │ │ │ │ └── MainActivity.kt │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── proguard-rules.pro │ └── build.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle.kts └── build.gradle.kts ├── test └── widget_test.dart └── .gitignore /lib/models/follow_user_item.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /.fvmrc: -------------------------------------------------------------------------------- 1 | { 2 | "flutter": "3.38.3" 3 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/assets/logo.png -------------------------------------------------------------------------------- /packages/simple_live_core/README.md: -------------------------------------------------------------------------------- 1 | # simple_live_core 2 | 3 | 项目核心库,实现获取各个网站的信息及弹幕。 4 | -------------------------------------------------------------------------------- /assets/logo_400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/assets/logo_400.png -------------------------------------------------------------------------------- /assets/images/huya.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/assets/images/huya.png -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/assets/images/logo.png -------------------------------------------------------------------------------- /assets/logo_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/assets/logo_circle.png -------------------------------------------------------------------------------- /assets/icons/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/assets/icons/app_icon.ico -------------------------------------------------------------------------------- /assets/images/douyin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/assets/images/douyin.png -------------------------------------------------------------------------------- /assets/images/douyu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/assets/images/douyu.png -------------------------------------------------------------------------------- /assets/images/bilibili.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/assets/images/bilibili.png -------------------------------------------------------------------------------- /assets/screenshot_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/assets/screenshot_dark.png -------------------------------------------------------------------------------- /assets/screenshot_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/assets/screenshot_light.png -------------------------------------------------------------------------------- /assets/images/bilibili_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/assets/images/bilibili_2.png -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/common/convert_helper.dart: -------------------------------------------------------------------------------- 1 | T? asT(dynamic value) { 2 | if (value is T) { 3 | return value; 4 | } 5 | return null; 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-512.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-16@2x.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-32@2x.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-128@2x.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-256@2x.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-512@2x.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GH4NG/dart_simple_live/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /assets/app_version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.8.1219", 3 | "version_num": 181219, 4 | "prerelease": false, 5 | "download_url": "https://github.com/GH4NG/dart_simple_live/releases" 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/xycz/simple_live/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.xycz.simple_live 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity : FlutterActivity() 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /linux/runner/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /macos/packaging/dmg/make_config.yaml: -------------------------------------------------------------------------------- 1 | title: Simple Live 2 | contents: 3 | - x: 448 4 | y: 344 5 | type: link 6 | path: "/Applications" 7 | - x: 192 8 | y: 344 9 | type: file 10 | path: Simple Live.app 11 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -Dconsole.encoding=UTF-8 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /linux/packaging/appimage/make_config.yaml: -------------------------------------------------------------------------------- 1 | display_name: Simple Live 2 | 3 | icon: assets/logo.png 4 | 5 | keywords: 6 | - Simple Live 7 | 8 | generic_name: Simple Live 9 | 10 | categories: 11 | - AudioVideo 12 | 13 | startup_notify: true 14 | 15 | include: [] 16 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /windows/packaging/msix/make_config.yaml: -------------------------------------------------------------------------------- 1 | display_name: Simple Live 2 | publisher_display_name: xiaoyaocz 3 | identity_name: com.xycz.simplelive 4 | logo_path: assets/logo_400.png 5 | capabilities: internetClient 6 | languages: zh-cn 7 | install_certificate: "false" 8 | signtool_options: /td SHA256 -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | validateDistributionUrl=true 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://downloads.gradle.org/distributions/gradle-8.13-bin.zip 7 | networkTimeout=10000 8 | -------------------------------------------------------------------------------- /macos/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 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/icons/icon_resume.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/model/live_category_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:simple_live_core/src/model/live_room_item.dart'; 2 | 3 | class LiveCategoryResult { 4 | final bool hasMore; 5 | final List items; 6 | LiveCategoryResult({ 7 | required this.hasMore, 8 | required this.items, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /app/build/ 3 | /.gradle 4 | /captures/ 5 | /gradlew 6 | /gradlew.bat 7 | /local.properties 8 | GeneratedPluginRegistrant.java 9 | .cxx/ 10 | 11 | # Remember to never publicly share your keystore. 12 | # See https://flutter.dev/to/reference-keystore 13 | key.properties 14 | **/*.keystore 15 | **/*.jks 16 | -------------------------------------------------------------------------------- /windows/packaging/exe/make_config.yaml: -------------------------------------------------------------------------------- 1 | script_template: 'inno_setup.iss' 2 | display_name: Simple Live 3 | app_id: 45F6FA98-DA23-4795-8685-60F607317A1F 4 | publisher: xiaoyaocz 5 | publisher_url: https://github.com/xiaoyaocz/dart_simple_live 6 | create_desktop_icon: true 7 | setup_icon_file: assets/icons/app_icon.ico 8 | locales: 9 | - en 10 | - zh 11 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 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 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /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 de.prosiebensat1digital.** { *; } 9 | -dontwarn io.flutter.embedding.** 10 | -ignorewarnings -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/icons/icon_pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/model/live_play_url.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class LivePlayUrl { 4 | /// 播放地址 5 | final List urls; 6 | 7 | /// 请求头 8 | final Map? headers; 9 | 10 | LivePlayUrl({ 11 | required this.urls, 12 | this.headers, 13 | }); 14 | 15 | @override 16 | String toString() { 17 | return json.encode({ 18 | "urls": urls, 19 | "headers": headers.toString(), 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /linux/packaging/deb/make_config.yaml: -------------------------------------------------------------------------------- 1 | display_name: Simple-Live 2 | package_name: simple-live 3 | 4 | maintainer: 5 | name: xiaoyaocz 6 | email: xiaoyaocz@52uwp.com 7 | 8 | priority: optional 9 | 10 | section: x11 11 | 12 | installed_size: 24400 13 | 14 | essential: false 15 | 16 | icon: assets/logo.png 17 | 18 | keywords: 19 | - Simple Live 20 | 21 | generic_name: Simple-Live 22 | 23 | categories: 24 | - AudioVideo 25 | - Media 26 | 27 | startup_notify: true 28 | -------------------------------------------------------------------------------- /assets/icons/icon_fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | com.apple.security.files.user-selected.read-write 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /lib/widgets/ui/after_post_frame.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | mixin AfterFirstFrameMixin on State { 4 | bool _afterFirstFrame = false; 5 | 6 | @override 7 | void initState() { 8 | super.initState(); 9 | 10 | WidgetsBinding.instance.addPostFrameCallback((_) { 11 | _afterFirstFrame = true; 12 | }); 13 | } 14 | 15 | void afterFirstFrame(void Function() callback) { 16 | if (_afterFirstFrame) { 17 | callback(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/model/live_play_quality.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class LivePlayQuality { 4 | /// 清晰度 5 | final String quality; 6 | 7 | /// 清晰度信息 8 | final dynamic data; 9 | 10 | final int sort; 11 | 12 | LivePlayQuality({ 13 | required this.quality, 14 | required this.data, 15 | this.sort = 0, 16 | }); 17 | 18 | @override 19 | String toString() { 20 | return json.encode({ 21 | "quality": quality, 22 | "data": data.toString(), 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /linux/runner/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, 7 | my_application, 8 | MY, 9 | APPLICATION, 10 | GtkApplication) 11 | 12 | /** 13 | * my_application_new: 14 | * 15 | * Creates a new Flutter-based application. 16 | * 17 | * Returns: a new #MyApplication. 18 | */ 19 | MyApplication* my_application_new(); 20 | 21 | #endif // FLUTTER_MY_APPLICATION_H_ 22 | -------------------------------------------------------------------------------- /lib/modules/home/home_list_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:simple_live_app/app/controller/base_controller.dart'; 2 | import 'package:simple_live_app/app/sites.dart'; 3 | import 'package:simple_live_core/simple_live_core.dart'; 4 | 5 | class HomeListController extends BasePageController { 6 | final Site site; 7 | HomeListController(this.site); 8 | 9 | @override 10 | Future> getData(int page, int pageSize) async { 11 | var result = await site.liveSite.getRecommendRooms(page: page); 12 | 13 | return result.items; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | com.apple.security.network.client 12 | 13 | com.apple.security.files.user-selected.read-write 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/interface/live_danmaku.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:simple_live_core/src/model/live_message.dart'; 4 | 5 | class LiveDanmaku { 6 | Function(LiveMessage msg)? onMessage; 7 | Function(String msg)? onClose; 8 | Function()? onReady; 9 | 10 | /// 心跳时间 11 | int heartbeatTime = 0; 12 | 13 | /// 发生心跳 14 | void heartbeat() {} 15 | 16 | /// 开始接收信息 17 | Future start(dynamic args) { 18 | return Future.value(); 19 | } 20 | 21 | /// 停止接收信息 22 | Future stop() { 23 | return Future.value(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/model/live_search_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:simple_live_core/src/model/live_anchor_item.dart'; 2 | import 'package:simple_live_core/src/model/live_room_item.dart'; 3 | 4 | class LiveSearchRoomResult { 5 | final bool hasMore; 6 | final List items; 7 | LiveSearchRoomResult({ 8 | required this.hasMore, 9 | required this.items, 10 | }); 11 | } 12 | 13 | class LiveSearchAnchorResult { 14 | final bool hasMore; 15 | final List items; 16 | LiveSearchAnchorResult({ 17 | required this.hasMore, 18 | required this.items, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 11 | } 12 | 13 | func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { 14 | GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /lib/widgets/keep_alive_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class KeepAliveWrapper extends StatefulWidget { 4 | final Widget child; 5 | 6 | const KeepAliveWrapper({super.key, required this.child}); 7 | 8 | @override 9 | State createState() => _KeepAliveWrapperState(); 10 | } 11 | 12 | class _KeepAliveWrapperState extends State 13 | with AutomaticKeepAliveClientMixin { 14 | @override 15 | Widget build(BuildContext context) { 16 | super.build(context); 17 | return widget.child; 18 | } 19 | 20 | @override 21 | bool get wantKeepAlive => true; 22 | } 23 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /assets/statement.txt: -------------------------------------------------------------------------------- 1 | 在使用本软件之前,请您仔细阅读以下内容,并确保您充分理解并同意以下条款: 2 | 1、本软件为开源软件,您可以免费获取并使用该软件。 3 | 2、本软件完全基于您个人意愿使用,您应该对自己的使用行为和所有结果承担全部责任。 4 | 3、本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。 5 | 4、本软件并不保证与所有操作系统或硬件设备兼容。本软件作者或贡献者不对因使用本软件而产生的任何技术或安全问题承担责任。 6 | 5、本软件作者或贡献者不承担因使用本软件而造成的任何直接、间接、特殊或后果性的损失或损害的责任,包括但不限于财产损失、商业利润损失、信息或数据丢失或损坏等。 7 | 6、本软件使用者应遵守国家相关法律法规和使用规范,不得利用本软件从事任何违法违规行为。如因使用本软件而导致的违法行为,使用者应承担相应的法律责任。 8 | 7、本软件不会收集、存储、使用任何用户的个人信息,包括但不限于姓名、地址、电子邮件地址、电话号码等。在使用本软件过程中,不会进行任何形式的个人信息采集。如用户提供任何个人信息,将被视为用户已自愿提供,并且用户将自行承担由此产生的所有法律责任。 9 | 8、本软件作者或贡献者保留随时修改、增加、删除本免责声明中的内容而不另行通知的权利。 10 | 9、如果本软件存在侵犯您的合法权益的情况,请及时与作者联系,作者将会及时删除有关内容。 11 | 如您不同意本免责声明中的任何内容,请勿使用本软件。使用本软件即代表您已完全理解并同意上述内容。 -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = Simple Live 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.xycz.simpleLiveApp 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.xycz. All rights reserved. 15 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/model/live_anchor_item.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class LiveAnchorItem { 4 | /// 房间ID 5 | final String roomId; 6 | 7 | /// 封面 8 | final String avatar; 9 | 10 | /// 用户名 11 | final String userName; 12 | 13 | /// 直播中 14 | final bool liveStatus; 15 | 16 | LiveAnchorItem({ 17 | required this.roomId, 18 | required this.avatar, 19 | required this.userName, 20 | required this.liveStatus, 21 | }); 22 | 23 | @override 24 | String toString() { 25 | return json.encode({ 26 | "roomId": roomId, 27 | "avatar": avatar, 28 | "userName": userName, 29 | "liveStatus": liveStatus, 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/modules/category/detail/category_detail_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:simple_live_app/app/controller/base_controller.dart'; 2 | import 'package:simple_live_app/app/sites.dart'; 3 | import 'package:simple_live_core/simple_live_core.dart'; 4 | 5 | class CategoryDetailController extends BasePageController { 6 | final Site site; 7 | final LiveSubCategory subCategory; 8 | CategoryDetailController({ 9 | required this.site, 10 | required this.subCategory, 11 | }); 12 | 13 | @override 14 | Future> getData(int page, int pageSize) async { 15 | var result = await site.liveSite.getCategoryRooms(subCategory, page: page); 16 | return result.items; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /lib/app/custom_throttle.dart: -------------------------------------------------------------------------------- 1 | /// 这个类的目的是简化 throttle 的操作,以便更好的理解代码 2 | /// 主要作用:节流,如果在很短时间内都会调用同一个方法,除了第一个方法有用以外 3 | /// 剩下的方法将会被舍弃,在 [eachDelayMillis] 时间后,才会允许下一次调用 4 | /// 会保存一个方法,在最后还会调用一次,和普通的 throttle 不太一样 5 | class DelayedThrottle { 6 | bool isInvoking = false; 7 | int eachDelayMillis; 8 | Future Function()? storeFunc; 9 | 10 | DelayedThrottle(this.eachDelayMillis); 11 | 12 | void invoke(Future Function() longCostFunc) { 13 | if (isInvoking) { 14 | storeFunc = longCostFunc; 15 | return; 16 | } 17 | storeFunc = null; 18 | isInvoking = true; 19 | longCostFunc().then((value) { 20 | Future.delayed(Duration(milliseconds: eachDelayMillis), () { 21 | isInvoking = false; 22 | if (storeFunc != null) { 23 | invoke(storeFunc!); 24 | } 25 | }); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/simple_live_core/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: simple_live_core 2 | version: 1.0.4 3 | description: '聚合直播核心库' 4 | repository: https://github.com/xiaoyaocz/dart_simple_live 5 | publish_to: 'none' 6 | environment: 7 | sdk: '>=3.10.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | dio: ^5.9.0 13 | logger: ^2.6.2 14 | web_socket_channel: ^3.0.3 15 | html_unescape: ^2.0.0 16 | protobuf: ^4.2.0 17 | crypto: ^3.0.6 18 | brotli: ^0.6.0 19 | tars_flutter: 20 | git: 21 | url: https://github.com/dbjiasb/TarsFlutter.git 22 | ref: main 23 | fixnum: ^1.1.1 24 | dart_quickjs: 25 | git: 26 | url: https://github.com/xiaoyaocz/dart_quickjs 27 | ref: main 28 | 29 | dev_dependencies: 30 | flutter_lints: ^6.0.0 31 | flutter_test: 32 | sdk: flutter 33 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/model/live_room_item.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class LiveRoomItem { 4 | /// 房间ID 5 | final String roomId; 6 | 7 | /// 标题 8 | final String title; 9 | 10 | /// 封面 11 | final String cover; 12 | 13 | /// 分区名称 14 | final String areaName; 15 | 16 | /// 用户名 17 | final String userName; 18 | 19 | /// 人气/在线人数 20 | final int online; 21 | LiveRoomItem({ 22 | required this.roomId, 23 | required this.title, 24 | required this.cover, 25 | required this.areaName, 26 | required this.userName, 27 | this.online = 0, 28 | }); 29 | 30 | @override 31 | String toString() { 32 | return json.encode({ 33 | "roomId": roomId, 34 | "title": title, 35 | "cover": cover, 36 | "areaName": areaName, 37 | "userName": userName, 38 | "online": online, 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/widgets/settings/settings_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:simple_live_app/app/app_style.dart'; 3 | 4 | class SettingsCard extends StatelessWidget { 5 | final Widget child; 6 | const SettingsCard({required this.child, super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Material( 11 | color: Theme.of(context).brightness == Brightness.dark 12 | ? Colors.grey.withAlpha(50) 13 | : Colors.white70, 14 | shape: RoundedRectangleBorder( 15 | borderRadius: AppStyle.radius8, 16 | side: BorderSide( 17 | color: Colors.grey.withAlpha(25), 18 | ), 19 | ), 20 | child: DecoratedBox( 21 | decoration: BoxDecoration( 22 | borderRadius: AppStyle.radius8, 23 | ), 24 | child: child, 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | val flutterSdkPath = 3 | run { 4 | val properties = java.util.Properties() 5 | file("local.properties").inputStream().use { properties.load(it) } 6 | val flutterSdkPath = properties.getProperty("flutter.sdk") 7 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } 8 | flutterSdkPath 9 | } 10 | 11 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | } 19 | 20 | plugins { 21 | id("dev.flutter.flutter-plugin-loader") version "1.0.0" 22 | id("com.android.application") version "8.12.1" apply false 23 | id("org.jetbrains.kotlin.android") version "2.2.0" apply false 24 | } 25 | 26 | include(":app") 27 | -------------------------------------------------------------------------------- /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 | 13.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/simple_live_core.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | export 'src/interface/live_site.dart'; 4 | export 'src/interface/live_danmaku.dart'; 5 | export 'src/huya_site.dart'; 6 | export 'src/bilibili_site.dart'; 7 | export 'src/douyu_site.dart'; 8 | export 'src/douyin_site.dart'; 9 | export 'src/common/core_log.dart'; 10 | export 'src/model/live_message.dart'; 11 | export 'src/danmaku/bilibili_danmaku.dart'; 12 | export 'src/danmaku/douyu_danmaku.dart'; 13 | export 'src/danmaku/huya_danmaku.dart'; 14 | export 'src/danmaku/douyin_danmaku.dart'; 15 | 16 | export 'src/model/live_category_result.dart'; 17 | export 'src/model/live_category.dart'; 18 | export 'src/model/live_play_quality.dart'; 19 | export 'src/model/live_room_detail.dart'; 20 | export 'src/model/live_room_item.dart'; 21 | export 'src/model/live_search_result.dart'; 22 | export 'src/model/live_anchor_item.dart'; 23 | export 'src/model/live_play_url.dart'; 24 | -------------------------------------------------------------------------------- /lib/modules/settings/danmu_shield/danmu_shield_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:simple_live_app/app/controller/app_settings_controller.dart'; 5 | import 'package:simple_live_app/app/controller/base_controller.dart'; 6 | 7 | class DanmuShieldController extends BaseController { 8 | final TextEditingController textEditingController = TextEditingController(); 9 | final AppSettingsController settingsController = 10 | Get.find(); 11 | void add() { 12 | if (textEditingController.text.isEmpty) { 13 | SmartDialog.showToast("请输入关键词"); 14 | return; 15 | } 16 | 17 | settingsController.addShieldList(textEditingController.text.trim()); 18 | textEditingController.text = ""; 19 | } 20 | 21 | void remove(String item) { 22 | settingsController.removeShieldList(item); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/requests/http_error.dart: -------------------------------------------------------------------------------- 1 | class HttpError extends Error { 2 | /// 错误码 3 | final int statusCode; 4 | 5 | /// 错误信息 6 | final String message; 7 | 8 | HttpError( 9 | this.message, { 10 | this.statusCode = 0, 11 | }); 12 | @override 13 | String toString() { 14 | if (statusCode != 0) { 15 | return statusCodeToString(statusCode); 16 | } 17 | 18 | return message; 19 | } 20 | 21 | String statusCodeToString(int statusCode) { 22 | switch (statusCode) { 23 | case 400: 24 | return "错误的请求(400)"; 25 | case 401: 26 | return "无权限访问资源(401)"; 27 | case 403: 28 | return "无权限访问资源(403)"; 29 | case 404: 30 | return "服务器找不到请求的资源(404)"; 31 | case 500: 32 | return "服务器出现错误(500)"; 33 | case 502: 34 | return "服务器出现错误(502)"; 35 | case 503: 36 | return "服务器出现错误(503)"; 37 | default: 38 | return "连接服务器失败,请稍后再试($statusCode)"; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/model/live_category.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class LiveCategory { 4 | final String name; 5 | final String id; 6 | final List children; 7 | LiveCategory({ 8 | required this.id, 9 | required this.name, 10 | required this.children, 11 | }); 12 | 13 | @override 14 | String toString() { 15 | return json.encode({ 16 | "name": name, 17 | "id": id, 18 | "children": children, 19 | }); 20 | } 21 | } 22 | 23 | class LiveSubCategory { 24 | final String name; 25 | final String? pic; 26 | final String id; 27 | final String parentId; 28 | LiveSubCategory({ 29 | required this.id, 30 | required this.name, 31 | required this.parentId, 32 | this.pic, 33 | }); 34 | 35 | @override 36 | String toString() { 37 | return json.encode({ 38 | "name": name, 39 | "id": id, 40 | "parentId": parentId, 41 | "pic": pic, 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/modules/mine/history/history_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:simple_live_app/app/controller/base_controller.dart'; 2 | import 'package:simple_live_app/app/utils.dart'; 3 | import 'package:simple_live_app/models/db/history.dart'; 4 | import 'package:simple_live_app/services/db_service.dart'; 5 | 6 | class HistoryController extends BasePageController { 7 | @override 8 | Future> getData(int page, int pageSize) { 9 | if (page > 1) { 10 | return Future.value([]); 11 | } 12 | return Future.value(DBService.instance.getHistories()); 13 | } 14 | 15 | Future clean() async { 16 | var result = await Utils.showAlertDialog("确定要清空观看记录吗?", title: "清空观看记录"); 17 | if (!result) { 18 | return; 19 | } 20 | await DBService.instance.historyBox.clear(); 21 | refreshData(); 22 | } 23 | 24 | Future removeItem(History item) async { 25 | await DBService.instance.historyBox.delete(item.id); 26 | refreshData(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/modules/live_room/player/base_player.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:simple_live_app/modules/live_room/player/player_states.dart'; 6 | 7 | export 'package:simple_live_app/modules/live_room/player/player_states.dart'; 8 | 9 | abstract class BasePlayer { 10 | Stream get stateStream; 11 | 12 | PlayerState lastState = const PlayerState(); 13 | 14 | Future init(); 15 | 16 | Widget? videoWidget( 17 | Key key, 18 | double? aspectRatio, 19 | BoxFit fit, 20 | ); 21 | 22 | Future dispose(); 23 | 24 | Future loadVideo( 25 | String url, { 26 | Map? headers, 27 | bool play = true, 28 | }); 29 | 30 | Future play(); 31 | 32 | Future setVolume(double volume); 33 | 34 | Future pause(); 35 | 36 | Future stop(); 37 | 38 | Future playOrPause(); 39 | 40 | Future snapshot(); 41 | } 42 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/common/core_error.dart: -------------------------------------------------------------------------------- 1 | class CoreError extends Error { 2 | /// 错误码 3 | final int statusCode; 4 | 5 | /// 错误信息 6 | final String message; 7 | 8 | CoreError( 9 | this.message, { 10 | this.statusCode = 0, 11 | }); 12 | @override 13 | String toString() { 14 | if (statusCode != 0) { 15 | return statusCodeToString(statusCode); 16 | } 17 | 18 | return message; 19 | } 20 | 21 | String statusCodeToString(int statusCode) { 22 | switch (statusCode) { 23 | case 400: 24 | return "错误的请求(400)"; 25 | case 401: 26 | return "无权限访问资源(401)"; 27 | case 403: 28 | return "无权限访问资源(403)"; 29 | case 404: 30 | return "服务器找不到请求的资源(404)"; 31 | case 500: 32 | return "服务器出现错误(500)"; 33 | case 502: 34 | return "服务器出现错误(502)"; 35 | case 503: 36 | return "服务器出现错误(503)"; 37 | default: 38 | return "连接服务器失败,请稍后再试($statusCode)"; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/widgets/status/app_loadding_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:simple_live_app/app/app_style.dart'; 5 | 6 | class AppLoaddingWidget extends StatelessWidget { 7 | const AppLoaddingWidget({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Center( 12 | child: Container( 13 | padding: AppStyle.edgeInsetsA12, 14 | decoration: BoxDecoration( 15 | shape: BoxShape.circle, 16 | color: Theme.of(context).cardColor, 17 | boxShadow: Get.isDarkMode 18 | ? [] 19 | : [ 20 | BoxShadow( 21 | blurRadius: 4, 22 | color: Colors.grey.withAlpha(50), 23 | ), 24 | ], 25 | ), 26 | child: const CupertinoActivityIndicator( 27 | radius: 10, 28 | ), 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/widgets/status/app_loading_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:simple_live_app/app/app_style.dart'; 5 | 6 | class AppLoadingWidget extends StatelessWidget { 7 | const AppLoadingWidget({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Center( 12 | child: Container( 13 | padding: AppStyle.edgeInsetsA12, 14 | decoration: BoxDecoration( 15 | shape: BoxShape.circle, 16 | color: Theme.of(context).cardColor, 17 | boxShadow: Get.isDarkMode 18 | ? [] 19 | : [ 20 | BoxShadow( 21 | blurRadius: 4, 22 | color: Colors.grey.withAlpha(50), 23 | ), 24 | ], 25 | ), 26 | child: const CupertinoActivityIndicator( 27 | radius: 10, 28 | ), 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | dynamic_color 7 | fvp 8 | media_kit_libs_linux 9 | media_kit_video 10 | screen_retriever_linux 11 | url_launcher_linux 12 | volume_controller 13 | window_manager 14 | ) 15 | 16 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 17 | ) 18 | 19 | set(PLUGIN_BUNDLED_LIBRARIES) 20 | 21 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 23 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 26 | endforeach(plugin) 27 | 28 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 29 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 30 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 31 | endforeach(ffi_plugin) 32 | -------------------------------------------------------------------------------- /lib/app/utils/archive.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:archive/archive_io.dart'; 4 | import 'package:path/path.dart'; 5 | 6 | extension ArchiveExt on Archive { 7 | void addDirectoryToArchive(String dirPath, String parentPath) { 8 | final dir = Directory(dirPath); 9 | final entities = dir.listSync(recursive: false); 10 | for (final entity in entities) { 11 | final relativePath = relative(entity.path, from: parentPath); 12 | if (entity is File) { 13 | final data = entity.readAsBytesSync(); 14 | final archiveFile = ArchiveFile(relativePath, data.length, data); 15 | addFile(archiveFile); 16 | } else if (entity is Directory) { 17 | addDirectoryToArchive(entity.path, parentPath); 18 | } 19 | } 20 | } 21 | 22 | void add(String name, T raw) { 23 | final data = json.encode(raw); 24 | addFile( 25 | ArchiveFile( 26 | name, 27 | data.length, 28 | utf8.encode(data), 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/app/utils/duration_2str.dart: -------------------------------------------------------------------------------- 1 | extension DurationStringExtensions on String { 2 | /// 将 "HH:MM:SS" 格式的字符串转换为 Duration 3 | Duration toDuration() { 4 | final parts = split(':'); 5 | if (parts.length != 3) { 6 | throw FormatException('Invalid duration format: $this'); 7 | } 8 | 9 | final hours = int.tryParse(parts[0]) ?? 0; 10 | final minutes = int.tryParse(parts[1]) ?? 0; 11 | final seconds = int.tryParse(parts[2]) ?? 0; 12 | 13 | return Duration(hours: hours, minutes: minutes, seconds: seconds); 14 | } 15 | } 16 | 17 | extension DurationExtensions on Duration { 18 | /// 将 Duration 转换为紧凑格式的字符串(如 "2h30m15s") 19 | String toHMSString() { 20 | final hours = inHours; // 计算总小时数 21 | final minutes = inMinutes.remainder(60); // 计算剩余分钟数 22 | final seconds = inSeconds.remainder(60); // 计算剩余秒数 23 | 24 | // 格式化分钟和秒为两位数 25 | final minutesStr = minutes.toString().padLeft(2, '0'); 26 | final secondsStr = seconds.toString().padLeft(2, '0'); 27 | 28 | return '$hours:$minutesStr:$secondsStr'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /linux/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} 10 | "main.cc" 11 | "my_application.cc" 12 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 13 | ) 14 | 15 | # Apply the standard set of build settings. This can be removed for applications 16 | # that need different build settings. 17 | apply_standard_settings(${BINARY_NAME}) 18 | 19 | # Add preprocessor definitions for the application ID. 20 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 21 | 22 | # Add dependency libraries. Add any application-specific dependencies here. 23 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 24 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 25 | 26 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 27 | -------------------------------------------------------------------------------- /lib/models/version_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | T? asT(dynamic value) { 4 | if (value is T) { 5 | return value; 6 | } 7 | return null; 8 | } 9 | 10 | class VersionModel { 11 | VersionModel({ 12 | required this.version, 13 | required this.versionNum, 14 | this.versionDesc, 15 | required this.downloadUrl, 16 | }); 17 | 18 | factory VersionModel.fromJson(Map json) => VersionModel( 19 | version: asT(json['version'])!, 20 | versionNum: asT(json['version_num'])!, 21 | versionDesc: asT(json['version_desc']), 22 | downloadUrl: asT(json['download_url'])!, 23 | ); 24 | 25 | String version; 26 | int versionNum; 27 | String? versionDesc; 28 | String downloadUrl; 29 | 30 | @override 31 | String toString() { 32 | return jsonEncode(this); 33 | } 34 | 35 | Map toJson() => { 36 | 'version': version, 37 | 'version_num': versionNum, 38 | if (versionDesc != null) 'version_desc': versionDesc, 39 | 'download_url': downloadUrl, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /lib/app/event_bus.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:simple_live_app/app/log.dart'; 4 | 5 | /// 全局事件 6 | class EventBus { 7 | /// 点击了底部导航 8 | static const String kBottomNavigationBarClicked = 9 | "BottomNavigationBarClicked"; 10 | 11 | /// 用户按了Esc 12 | static const String kEscapePressed = "EscapePressed"; 13 | static EventBus? _instance; 14 | 15 | static EventBus get instance { 16 | _instance ??= EventBus(); 17 | return _instance!; 18 | } 19 | 20 | final Map _streams = {}; 21 | 22 | /// 触发事件 23 | void emit(String name, T data) { 24 | if (!_streams.containsKey(name)) { 25 | _streams.addAll({name: StreamController.broadcast()}); 26 | } 27 | Log.d("Emit Event:$name\r\n$data"); 28 | 29 | _streams[name]!.add(data); 30 | } 31 | 32 | /// 监听事件 33 | StreamSubscription listen(String name, Function(dynamic)? onData) { 34 | if (!_streams.containsKey(name)) { 35 | _streams.addAll({name: StreamController.broadcast()}); 36 | } 37 | return _streams[name]!.stream.listen(onData); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/models/db/follow_user_tag.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive/hive.dart'; 2 | 3 | part 'follow_user_tag.g.dart'; 4 | 5 | @HiveType(typeId: 3) 6 | class FollowUserTag { 7 | @HiveField(1) 8 | String id; 9 | 10 | // 用户自定义tag 11 | @HiveField(2) 12 | String tag; 13 | 14 | // followUserId 15 | @HiveField(3) 16 | List userId; 17 | 18 | FollowUserTag({ 19 | required this.id, 20 | required this.tag, 21 | required this.userId, 22 | }); 23 | 24 | factory FollowUserTag.fromJson(Map json) { 25 | return FollowUserTag( 26 | id: json['id'], 27 | tag: json['tag'], 28 | userId: List.from(json['userId']), 29 | ); 30 | } 31 | 32 | Map toJson() { 33 | return { 34 | 'id': id, 35 | 'tag': tag, 36 | 'userId': userId, 37 | }; 38 | } 39 | 40 | FollowUserTag copyWith({ 41 | String? id, 42 | String? tag, 43 | List? userId, 44 | }) { 45 | return FollowUserTag( 46 | id: id ?? this.id, 47 | tag: tag ?? this.tag, 48 | userId: userId ?? this.userId, 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /assets/icons/icon_refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/modules/search/search_list_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:simple_live_app/app/controller/base_controller.dart'; 3 | import 'package:simple_live_app/app/sites.dart'; 4 | 5 | class SearchListController extends BasePageController { 6 | String keyword = ""; 7 | 8 | /// 搜索模式,0=直播间,1=主播 9 | var searchMode = 0.obs; 10 | final Site site; 11 | SearchListController( 12 | this.site, 13 | ); 14 | 15 | @override 16 | Future refreshData() async { 17 | if (keyword.isEmpty) { 18 | return; 19 | } 20 | return await super.refreshData(); 21 | } 22 | 23 | @override 24 | Future getData(int page, int pageSize) async { 25 | if (keyword.isEmpty) { 26 | return []; 27 | } 28 | if (searchMode.value == 1) { 29 | // 搜索主播 30 | var result = await site.liveSite.searchAnchors(keyword, page: page); 31 | return result.items; 32 | } 33 | var result = await site.liveSite.searchRooms(keyword, page: page); 34 | 35 | return result.items; 36 | } 37 | 38 | void clear() { 39 | pageEmpty.value = false; 40 | list.clear(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/models/sync_client_info_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | T? asT(dynamic value) { 4 | if (value is T) { 5 | return value; 6 | } 7 | return null; 8 | } 9 | 10 | class SyncClientInfoModel { 11 | SyncClientInfoModel({ 12 | required this.name, 13 | required this.version, 14 | required this.address, 15 | required this.port, 16 | required this.type, 17 | }); 18 | 19 | factory SyncClientInfoModel.fromJson(Map json) => 20 | SyncClientInfoModel( 21 | type: asT(json['type'])!, 22 | name: asT(json['name'])!, 23 | version: asT(json['version'])!, 24 | address: asT(json['address'])!, 25 | port: asT(json['port'])!, 26 | ); 27 | String type; 28 | String name; 29 | String version; 30 | String address; 31 | int port; 32 | 33 | @override 34 | String toString() { 35 | return jsonEncode(this); 36 | } 37 | 38 | Map toJson() => { 39 | 'name': name, 40 | 'version': version, 41 | 'address': address, 42 | 'port': port, 43 | 'type': type, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /lib/app/utils/listen_fourth_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | 3 | /// 鼠标侧键点击手势识别器 4 | /// - https://github.com/flutter/flutter/issues/115641 5 | /// - https://github.com/witnet/my-wit-wallet/pull/261 6 | class FourthButtonTapGestureRecognizer extends BaseTapGestureRecognizer { 7 | GestureTapDownCallback? onTapDown; 8 | 9 | @override 10 | void handleTapDown({required PointerDownEvent down}) { 11 | final TapDownDetails details = TapDownDetails( 12 | globalPosition: down.position, 13 | localPosition: down.localPosition, 14 | kind: getKindForPointer(down.pointer), 15 | ); 16 | switch (down.buttons) { 17 | case 8: 18 | if (onTapDown != null) { 19 | invokeCallback('onTapDown', () => onTapDown!(details)); 20 | } 21 | break; 22 | default: 23 | } 24 | } 25 | 26 | @override 27 | void handleTapCancel({ 28 | required PointerDownEvent down, 29 | PointerCancelEvent? cancel, 30 | required String reason, 31 | }) {} 32 | 33 | @override 34 | void handleTapUp({ 35 | required PointerDownEvent down, 36 | required PointerUpEvent up, 37 | }) {} 38 | } 39 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:simple_live_app/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/widgets/status/app_empty_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:simple_live_app/app/app_style.dart'; 3 | import 'package:lottie/lottie.dart'; 4 | 5 | class AppEmptyWidget extends StatelessWidget { 6 | final Function()? onRefresh; 7 | const AppEmptyWidget({this.onRefresh, super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Center( 12 | child: GestureDetector( 13 | onTap: () { 14 | onRefresh?.call(); 15 | }, 16 | child: Padding( 17 | padding: AppStyle.edgeInsetsA12, 18 | child: Column( 19 | mainAxisSize: MainAxisSize.min, 20 | children: [ 21 | LottieBuilder.asset( 22 | 'assets/lotties/empty.json', 23 | width: 200, 24 | height: 200, 25 | repeat: false, 26 | ), 27 | const Text( 28 | "这里什么都没有", 29 | textAlign: TextAlign.center, 30 | style: TextStyle(fontSize: 12, color: Colors.grey), 31 | ), 32 | ], 33 | ), 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | connectivity_plus 7 | dynamic_color 8 | flutter_inappwebview_windows 9 | fvp 10 | media_kit_libs_windows_video 11 | media_kit_video 12 | permission_handler_windows 13 | screen_brightness_windows 14 | screen_retriever_windows 15 | share_plus 16 | url_launcher_windows 17 | volume_controller 18 | window_manager 19 | ) 20 | 21 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 22 | ) 23 | 24 | set(PLUGIN_BUNDLED_LIBRARIES) 25 | 26 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 27 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 28 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 29 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 30 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 31 | endforeach(plugin) 32 | 33 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 34 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 35 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 36 | endforeach(ffi_plugin) 37 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | pubspec.lock 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | .vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .packages 33 | .pub-cache/ 34 | .pub/ 35 | /build/ 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | **/android/**/gradle-wrapper.jar 48 | .gradle/ 49 | **/android/captures/ 50 | **/android/gradlew 51 | **/android/gradlew.bat 52 | **/android/**/GeneratedPluginRegistrant.java 53 | **/android/key.properties 54 | *.jks 55 | local.properties 56 | **/.cxx/ 57 | 58 | # FVM Version Cache 59 | .fvm/ -------------------------------------------------------------------------------- /lib/modules/category/category_list_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:simple_live_app/app/controller/base_controller.dart'; 3 | import 'package:simple_live_app/app/sites.dart'; 4 | import 'package:simple_live_core/simple_live_core.dart'; 5 | 6 | class CategoryListController extends BasePageController { 7 | final Site site; 8 | CategoryListController(this.site); 9 | 10 | @override 11 | Future> getData(int page, int pageSize) async { 12 | var result = await site.liveSite.getCategories(); 13 | 14 | return result.map(AppLiveCategory.fromLiveCategory).toList(); 15 | } 16 | } 17 | 18 | class AppLiveCategory extends LiveCategory { 19 | var showAll = false.obs; 20 | AppLiveCategory({ 21 | required super.id, 22 | required super.name, 23 | required super.children, 24 | }) { 25 | showAll.value = children.length < 19; 26 | } 27 | 28 | List get take15 => children.take(15).toList(); 29 | 30 | factory AppLiveCategory.fromLiveCategory(LiveCategory item) { 31 | return AppLiveCategory( 32 | children: item.children, 33 | id: item.id, 34 | name: item.name, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/widgets/status/app_error_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:simple_live_app/app/app_style.dart'; 3 | import 'package:lottie/lottie.dart'; 4 | 5 | class AppErrorWidget extends StatelessWidget { 6 | final Function()? onRefresh; 7 | final String errorMsg; 8 | const AppErrorWidget({this.errorMsg = "", this.onRefresh, super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Center( 13 | child: GestureDetector( 14 | onTap: () { 15 | onRefresh?.call(); 16 | }, 17 | child: Padding( 18 | padding: AppStyle.edgeInsetsA12, 19 | child: Column( 20 | mainAxisSize: MainAxisSize.min, 21 | children: [ 22 | LottieBuilder.asset( 23 | 'assets/lotties/error.json', 24 | width: 260, 25 | repeat: false, 26 | ), 27 | Text( 28 | "$errorMsg\r\n点击刷新", 29 | textAlign: TextAlign.center, 30 | style: const TextStyle(fontSize: 12, color: Colors.grey), 31 | ), 32 | ], 33 | ), 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/simple_live_core/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | pubspec.lock 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | .vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .packages 33 | .pub-cache/ 34 | .pub/ 35 | /build/ 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | **/android/**/gradle-wrapper.jar 48 | .gradle/ 49 | **/android/captures/ 50 | **/android/gradlew 51 | **/android/gradlew.bat 52 | **/android/**/GeneratedPluginRegistrant.java 53 | **/android/key.properties 54 | *.jks 55 | local.properties 56 | **/.cxx/ 57 | 58 | # FVM Version Cache 59 | .fvm/ -------------------------------------------------------------------------------- /lib/modules/settings/indexed_settings/indexed_settings_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:simple_live_app/app/controller/app_settings_controller.dart'; 3 | 4 | class IndexedSettingsController extends GetxController { 5 | RxList siteSort = RxList(); 6 | RxList homeSort = RxList(); 7 | @override 8 | void onInit() { 9 | siteSort = AppSettingsController.instance.siteSort; 10 | homeSort = AppSettingsController.instance.homeSort; 11 | super.onInit(); 12 | } 13 | 14 | void updateSiteSort(int oldIndex, int newIndex) { 15 | if (oldIndex < newIndex) { 16 | newIndex -= 1; 17 | } 18 | final String item = siteSort.removeAt(oldIndex); 19 | siteSort.insert(newIndex, item); 20 | // ignore: invalid_use_of_protected_member 21 | AppSettingsController.instance.setSiteSort(siteSort.value); 22 | } 23 | 24 | void updateHomeSort(int oldIndex, int newIndex) { 25 | if (oldIndex < newIndex) { 26 | newIndex -= 1; 27 | } 28 | final String item = homeSort.removeAt(oldIndex); 29 | homeSort.insert(newIndex, item); 30 | // ignore: invalid_use_of_protected_member 31 | AppSettingsController.instance.setHomeSort(homeSort.value); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/widgets/filter_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:simple_live_app/app/app_style.dart'; 3 | 4 | class FilterButton extends StatelessWidget { 5 | final bool selected; 6 | final String text; 7 | final Function()? onTap; 8 | const FilterButton({ 9 | this.selected = false, 10 | required this.text, 11 | this.onTap, 12 | super.key, 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return InkWell( 18 | borderRadius: AppStyle.radius24, 19 | onTap: onTap, 20 | child: Container( 21 | padding: AppStyle.edgeInsetsH12.copyWith(top: 4, bottom: 4), 22 | decoration: BoxDecoration( 23 | border: Border.all( 24 | color: selected 25 | ? Theme.of(context).textTheme.bodyMedium!.color! 26 | : Colors.grey, 27 | ), 28 | borderRadius: AppStyle.radius24, 29 | ), 30 | child: Text( 31 | text, 32 | style: selected 33 | ? Theme.of(context).textTheme.bodyMedium 34 | : Theme.of(context).textTheme.bodyMedium!.copyWith( 35 | color: Colors.grey, 36 | ), 37 | ), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/app/constant.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:remixicon/remixicon.dart'; 3 | 4 | class Constant { 5 | static const String kUpdateFollow = "UpdateFollow"; 6 | static const String kUpdateHistory = "UpdateHistory"; 7 | 8 | static final Map allHomePages = { 9 | "recommend": HomePageItem( 10 | iconData: Remix.home_smile_line, 11 | title: "首页", 12 | index: 0, 13 | ), 14 | "follow": HomePageItem( 15 | iconData: Remix.heart_line, 16 | title: "关注", 17 | index: 1, 18 | ), 19 | "category": HomePageItem( 20 | iconData: Remix.apps_line, 21 | title: "分类", 22 | index: 2, 23 | ), 24 | "user": HomePageItem( 25 | iconData: Remix.user_smile_line, 26 | title: "我的", 27 | index: 3, 28 | ), 29 | }; 30 | 31 | static const String kBiliBili = "bilibili"; 32 | static const String kDouyu = "douyu"; 33 | static const String kHuya = "huya"; 34 | static const String kDouyin = "douyin"; 35 | } 36 | 37 | class HomePageItem { 38 | final IconData iconData; 39 | final String title; 40 | final int index; 41 | HomePageItem({ 42 | required this.iconData, 43 | required this.title, 44 | required this.index, 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /lib/models/account/douyin_user_info.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | T? asT(dynamic value) { 4 | if (value is T) { 5 | return value; 6 | } 7 | return null; 8 | } 9 | 10 | class DouyinUserInfoModel { 11 | DouyinUserInfoModel({ 12 | this.id, 13 | this.nickname, 14 | this.shortId, 15 | this.sign, 16 | this.birthday, 17 | this.gender, 18 | }); 19 | 20 | factory DouyinUserInfoModel.fromJson(Map json) => 21 | DouyinUserInfoModel( 22 | id: asT(json['id_str']), 23 | nickname: asT(json['nickname']), 24 | shortId: asT(json['short_id']), 25 | sign: asT(json['sign']), 26 | birthday: asT(json['birthday']), 27 | gender: asT(json['gender']), 28 | ); 29 | 30 | String? id; 31 | String? nickname; 32 | String? shortId; 33 | String? sign; 34 | String? birthday; 35 | String? gender; 36 | 37 | @override 38 | String toString() { 39 | return jsonEncode(this); 40 | } 41 | 42 | Map toJson() => { 43 | 'id': id, 44 | 'nickname': nickname, 45 | 'short_id': shortId, 46 | 'sign': sign, 47 | 'birthday': birthday, 48 | 'gender': gender, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /lib/modules/home/home_list_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:get/get.dart'; 4 | import 'package:simple_live_app/app/app_style.dart'; 5 | import 'package:simple_live_app/modules/home/home_list_controller.dart'; 6 | import 'package:simple_live_app/widgets/keep_alive_wrapper.dart'; 7 | import 'package:simple_live_app/widgets/live_room_card.dart'; 8 | import 'package:simple_live_app/widgets/page_grid_view.dart'; 9 | 10 | class HomeListView extends StatelessWidget { 11 | final String tag; 12 | const HomeListView(this.tag, {super.key}); 13 | HomeListController get controller => Get.find(tag: tag); 14 | @override 15 | Widget build(BuildContext context) { 16 | var c = MediaQuery.of(context).size.width ~/ 200; 17 | if (c < 2) { 18 | c = 2; 19 | } 20 | return KeepAliveWrapper( 21 | child: PageGridView( 22 | pageController: controller, 23 | padding: AppStyle.edgeInsetsA12, 24 | refreshOnStart: true, 25 | mainAxisSpacing: 12, 26 | crossAxisSpacing: 12, 27 | crossAxisCount: c, 28 | itemBuilder: (_, i) { 29 | var item = controller.list[i]; 30 | return LiveRoomCard(controller.site, item); 31 | }, 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/widgets/desktop_refresh_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:simple_live_app/app/app_style.dart'; 3 | 4 | class DesktopRefreshButton extends StatelessWidget { 5 | final bool refreshing; 6 | final Function()? onPressed; 7 | const DesktopRefreshButton({ 8 | required this.refreshing, 9 | this.onPressed, 10 | super.key, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | decoration: BoxDecoration( 17 | color: Theme.of(context).cardColor, 18 | borderRadius: AppStyle.radius48, 19 | boxShadow: [ 20 | BoxShadow( 21 | color: Colors.grey.withAlpha(50), 22 | blurRadius: 4, 23 | ), 24 | ], 25 | ), 26 | width: 40, 27 | height: 40, 28 | child: refreshing 29 | ? const Center( 30 | child: SizedBox( 31 | width: 20, 32 | height: 20, 33 | child: CircularProgressIndicator( 34 | strokeWidth: 2, 35 | ), 36 | ), 37 | ) 38 | : IconButton( 39 | onPressed: onPressed, 40 | icon: const Icon(Icons.refresh), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/widgets/settings/settings_switch.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:simple_live_app/app/app_style.dart'; 3 | 4 | class SettingsSwitch extends StatelessWidget { 5 | final bool value; 6 | final String title; 7 | final String? subtitle; 8 | final Function(bool) onChanged; 9 | const SettingsSwitch({ 10 | required this.value, 11 | required this.title, 12 | this.subtitle, 13 | required this.onChanged, 14 | super.key, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return SwitchListTile( 20 | title: Text( 21 | title, 22 | style: Theme.of(context).textTheme.bodyLarge, 23 | ), 24 | shape: RoundedRectangleBorder( 25 | borderRadius: AppStyle.radius8, 26 | ), 27 | trackOutlineColor: const WidgetStatePropertyAll(Colors.transparent), 28 | //visualDensity: VisualDensity.compact, 29 | contentPadding: AppStyle.edgeInsetsL16.copyWith(right: 8), 30 | subtitle: subtitle != null 31 | ? Text( 32 | subtitle!, 33 | style: Theme.of( 34 | context, 35 | ).textTheme.bodySmall!.copyWith(color: Colors.grey), 36 | ) 37 | : null, 38 | value: value, 39 | onChanged: onChanged, 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/danmaku/proto/douyin.pbenum.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Generated code. Do not modify. 3 | // source: douyin.proto 4 | // 5 | // @dart = 2.12 6 | 7 | // ignore_for_file: annotate_overrides, camel_case_types 8 | // ignore_for_file: constant_identifier_names, library_prefixes 9 | // ignore_for_file: non_constant_identifier_names, prefer_final_fields 10 | // ignore_for_file: unnecessary_import, unnecessary_this, unused_import 11 | 12 | import 'dart:core' as $core; 13 | 14 | import 'package:protobuf/protobuf.dart' as $pb; 15 | 16 | class CommentTypeTag extends $pb.ProtobufEnum { 17 | static const CommentTypeTag COMMENTTYPETAGUNKNOWN = CommentTypeTag._(0, _omitEnumNames ? '' : 'COMMENTTYPETAGUNKNOWN'); 18 | static const CommentTypeTag COMMENTTYPETAGSTAR = CommentTypeTag._(1, _omitEnumNames ? '' : 'COMMENTTYPETAGSTAR'); 19 | 20 | static const $core.List values = [ 21 | COMMENTTYPETAGUNKNOWN, 22 | COMMENTTYPETAGSTAR, 23 | ]; 24 | 25 | static final $core.Map<$core.int, CommentTypeTag> _byValue = $pb.ProtobufEnum.initByValue(values); 26 | static CommentTypeTag? valueOf($core.int value) => _byValue[value]; 27 | 28 | const CommentTypeTag._($core.int v, $core.String n) : super(v, n); 29 | } 30 | 31 | 32 | const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); 33 | -------------------------------------------------------------------------------- /lib/widgets/shadow_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:simple_live_app/app/app_style.dart'; 4 | 5 | class ShadowCard extends StatelessWidget { 6 | final Widget child; 7 | final double radius; 8 | final Function()? onTap; 9 | const ShadowCard({ 10 | required this.child, 11 | this.radius = 8.0, 12 | this.onTap, 13 | super.key, 14 | }); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return DecoratedBox( 19 | decoration: BoxDecoration( 20 | borderRadius: BorderRadius.circular(radius), 21 | boxShadow: Get.isDarkMode 22 | ? [] 23 | : [ 24 | BoxShadow( 25 | blurRadius: 4, 26 | color: Colors.grey.withAlpha(50), 27 | ), 28 | ], 29 | ), 30 | child: Material( 31 | color: Theme.of(context).cardColor, 32 | borderRadius: BorderRadius.circular(radius), 33 | child: InkWell( 34 | borderRadius: BorderRadius.circular(radius), 35 | onTap: onTap, 36 | child: DecoratedBox( 37 | decoration: BoxDecoration( 38 | borderRadius: AppStyle.radius8, 39 | ), 40 | child: child, 41 | ), 42 | ), 43 | ), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/modules/category/detail/category_detail_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:get/get.dart'; 4 | import 'package:simple_live_app/app/app_style.dart'; 5 | import 'package:simple_live_app/modules/category/detail/category_detail_controller.dart'; 6 | import 'package:simple_live_app/widgets/keep_alive_wrapper.dart'; 7 | import 'package:simple_live_app/widgets/live_room_card.dart'; 8 | import 'package:simple_live_app/widgets/page_grid_view.dart'; 9 | 10 | class CategoryDetailPage extends GetView { 11 | const CategoryDetailPage({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | var c = MediaQuery.of(context).size.width ~/ 200; 16 | if (c < 2) { 17 | c = 2; 18 | } 19 | return Scaffold( 20 | appBar: AppBar( 21 | title: Text(controller.subCategory.name), 22 | ), 23 | body: KeepAliveWrapper( 24 | child: PageGridView( 25 | pageController: controller, 26 | padding: AppStyle.edgeInsetsA12, 27 | refreshOnStart: true, 28 | mainAxisSpacing: 12, 29 | crossAxisSpacing: 12, 30 | crossAxisCount: c, 31 | itemBuilder: (_, i) { 32 | var item = controller.list[i]; 33 | return LiveRoomCard(controller.site, item); 34 | }, 35 | ), 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: 功能请求 2 | title: '[Feature] 请填写简短易读的标题' 3 | description: '请求开发新的功能/优化某项功能' 4 | assignees: GH4NG 5 | labels: 6 | - 'enhancement' 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | 你的Issue大概率会搁置很久,除非是那种我能随手解决的。 12 | 暂时不会添加新的平台支持(因为我自己用不上)。如果你有开发能力,建议的自行开发后提交个PR。 13 | 14 | - type: checkboxes 15 | id: terms 16 | attributes: 17 | label: 非重复的Issue 18 | description: 先去Issues列表中查找下是否存在相似的Issue,如果有了就不要重复发了 19 | options: 20 | - label: 已确认没有相似的Issue 21 | required: true 22 | 23 | - type: textarea 24 | id: description 25 | validations: 26 | required: true 27 | attributes: 28 | label: 功能描述 29 | description: 请填写功能描述,如果你有多个建议、请求,请分开提交Issue 30 | 31 | - type: checkboxes 32 | id: platform 33 | attributes: 34 | label: 平台 35 | description: 这个功能针对什么平台 36 | options: 37 | - label: Android 38 | - label: iOS 39 | - label: MacOS 40 | - label: Windows 41 | - label: Linux 42 | - label: Android TV 43 | 44 | - type: textarea 45 | id: additional 46 | attributes: 47 | label: 附加信息 48 | description: 可以附加相关截图、视频或链接(好让我知道要我抄哪个APP) 49 | -------------------------------------------------------------------------------- /lib/models/db/follow_user_tag.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'follow_user_tag.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class FollowUserTagAdapter extends TypeAdapter { 10 | @override 11 | final int typeId = 3; 12 | 13 | @override 14 | FollowUserTag read(BinaryReader reader) { 15 | final numOfFields = reader.readByte(); 16 | final fields = { 17 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 18 | }; 19 | return FollowUserTag( 20 | id: fields[1] as String, 21 | tag: fields[2] as String, 22 | userId: (fields[3] as List).cast(), 23 | ); 24 | } 25 | 26 | @override 27 | void write(BinaryWriter writer, FollowUserTag obj) { 28 | writer 29 | ..writeByte(3) 30 | ..writeByte(1) 31 | ..write(obj.id) 32 | ..writeByte(2) 33 | ..write(obj.tag) 34 | ..writeByte(3) 35 | ..write(obj.userId); 36 | } 37 | 38 | @override 39 | int get hashCode => typeId.hashCode; 40 | 41 | @override 42 | bool operator ==(Object other) => 43 | identical(this, other) || 44 | other is FollowUserTagAdapter && 45 | runtimeType == other.runtimeType && 46 | typeId == other.typeId; 47 | } 48 | -------------------------------------------------------------------------------- /lib/requests/webdav_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:webdav_client/webdav_client.dart'; 5 | 6 | class DAVClient { 7 | late Client client; 8 | Completer pingCompleter = Completer(); 9 | 10 | DAVClient( 11 | String webDAVUri, 12 | String webDAVUser, 13 | String webDAVPassword, 14 | ) { 15 | client = newClient( 16 | webDAVUri, 17 | user: webDAVUser, 18 | password: webDAVPassword, 19 | ); 20 | client 21 | ..setHeaders( 22 | { 23 | 'accept-charset': 'utf-8', 24 | 'Content-Type': 'text/xml', 25 | }, 26 | ) 27 | ..setConnectTimeout(8000) 28 | ..setSendTimeout(8000) 29 | ..setReceiveTimeout(8000); 30 | pingCompleter.complete(_ping()); 31 | } 32 | 33 | Future _ping() async { 34 | try { 35 | await client.ping(); 36 | return true; 37 | } catch (_) { 38 | return false; 39 | } 40 | } 41 | 42 | // 强制统一 43 | String get root => "/simple_live_app"; 44 | 45 | String get backupFile => "$root/backup.zip"; 46 | 47 | Future backup(Uint8List data) async { 48 | await client.mkdir(root); 49 | await client.write(backupFile, data); 50 | return true; 51 | } 52 | 53 | Future> recovery() async { 54 | await client.mkdir(root); 55 | final data = await client.read(backupFile); 56 | return data; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/app/sites.dart: -------------------------------------------------------------------------------- 1 | import 'package:simple_live_app/app/constant.dart'; 2 | import 'package:simple_live_app/app/controller/app_settings_controller.dart'; 3 | import 'package:simple_live_core/simple_live_core.dart'; 4 | 5 | class Sites { 6 | static final Map allSites = { 7 | Constant.kBiliBili: Site( 8 | id: Constant.kBiliBili, 9 | logo: "assets/images/bilibili_2.png", 10 | name: "哔哩哔哩", 11 | liveSite: BiliBiliSite(), 12 | ), 13 | Constant.kDouyu: Site( 14 | id: Constant.kDouyu, 15 | logo: "assets/images/douyu.png", 16 | name: "斗鱼直播", 17 | liveSite: DouyuSite(), 18 | ), 19 | Constant.kHuya: Site( 20 | id: Constant.kHuya, 21 | logo: "assets/images/huya.png", 22 | name: "虎牙直播", 23 | liveSite: HuyaSite(), 24 | ), 25 | Constant.kDouyin: Site( 26 | id: Constant.kDouyin, 27 | logo: "assets/images/douyin.png", 28 | name: "抖音直播", 29 | liveSite: DouyinSite(), 30 | ), 31 | }; 32 | 33 | static List get supportSites { 34 | return AppSettingsController.instance.siteSort 35 | .map((key) => allSites[key]!) 36 | .toList(); 37 | } 38 | } 39 | 40 | class Site { 41 | final String id; 42 | final String name; 43 | final String logo; 44 | final LiveSite liveSite; 45 | Site({ 46 | required this.id, 47 | required this.liveSite, 48 | required this.logo, 49 | required this.name, 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"simple_live_app", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/checks.yaml: -------------------------------------------------------------------------------- 1 | name: Flutter checks 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - dev 7 | workflow_dispatch: 8 | 9 | jobs: 10 | linting: 11 | name: Linting & Formatting 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4.1.1 16 | 17 | - name: Fetch version info 18 | run: | 19 | # Extract flutter_version from .fvmrc 20 | FLUTTER_VERSION=$(jq -r '.flutter' .fvmrc) 21 | echo "FLUTTER_VERSION=${FLUTTER_VERSION}" >> "$GITHUB_ENV" 22 | shell: bash 23 | 24 | - name: Set up Flutter 25 | uses: subosito/flutter-action@v2.16.0 26 | with: 27 | channel: 'stable' 28 | flutter-version: ${{ env.FLUTTER_VERSION }} 29 | cache: true 30 | cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' 31 | cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:' 32 | 33 | - name: Get dependencies 34 | run: flutter pub get 35 | 36 | - name: Setup Dart 37 | uses: dart-lang/setup-dart@v1 38 | 39 | - name: Analyze Dart 40 | uses: invertase/github-action-dart-analyzer@v3 41 | with: 42 | custom-lint: true 43 | -------------------------------------------------------------------------------- /lib/modules/category/category_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'package:get/get.dart'; 6 | import 'package:simple_live_app/app/event_bus.dart'; 7 | import 'package:simple_live_app/app/sites.dart'; 8 | import 'package:simple_live_app/modules/category/category_list_controller.dart'; 9 | 10 | class CategoryController extends GetxController 11 | with GetSingleTickerProviderStateMixin { 12 | late TabController tabController; 13 | CategoryController() { 14 | tabController = TabController( 15 | length: Sites.supportSites.length, 16 | vsync: this, 17 | ); 18 | } 19 | StreamSubscription? streamSubscription; 20 | @override 21 | void onInit() { 22 | streamSubscription = EventBus.instance.listen( 23 | EventBus.kBottomNavigationBarClicked, 24 | (index) { 25 | if (index == 2) { 26 | refreshOrScrollTop(); 27 | } 28 | }, 29 | ); 30 | for (var site in Sites.supportSites) { 31 | Get.put(CategoryListController(site), tag: site.id); 32 | } 33 | 34 | super.onInit(); 35 | } 36 | 37 | void refreshOrScrollTop() { 38 | var tabIndex = tabController.index; 39 | Get.find( 40 | tag: Sites.supportSites[tabIndex].id, 41 | ).scrollToTopOrRefresh(); 42 | } 43 | 44 | @override 45 | void onClose() { 46 | streamSubscription?.cancel(); 47 | super.onClose(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/modules/sync/remote_sync/webdav/webdav_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:webdav_client/webdav_client.dart'; 5 | 6 | class DAVClient { 7 | late Client client; 8 | Completer pingCompleter = Completer(); 9 | 10 | DAVClient( 11 | String webDAVUri, 12 | String webDAVUser, 13 | String webDAVPassword, 14 | ) { 15 | client = newClient( 16 | webDAVUri, 17 | user: webDAVUser, 18 | password: webDAVPassword, 19 | ); 20 | client 21 | ..setHeaders( 22 | { 23 | 'accept-charset': 'utf-8', 24 | 'Content-Type': 'text/xml', 25 | }, 26 | ) 27 | ..setConnectTimeout(8000) 28 | ..setSendTimeout(8000) 29 | ..setReceiveTimeout(8000); 30 | pingCompleter.complete(_ping()); 31 | } 32 | 33 | Future _ping() async { 34 | try { 35 | await client.ping(); 36 | return true; 37 | } catch (_) { 38 | return false; 39 | } 40 | } 41 | 42 | // 强制统一 43 | String get root => "/simple_live_app"; 44 | 45 | String get backupFile => "$root/backup.zip"; 46 | 47 | Future backup(Uint8List data) async { 48 | await client.mkdir(root); 49 | await client.write(backupFile, data); 50 | return true; 51 | } 52 | 53 | Future> recovery() async { 54 | await client.mkdir(root); 55 | final data = await client.read(backupFile); 56 | return data; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/requests/common_request.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:simple_live_app/models/version_model.dart'; 4 | import 'package:simple_live_app/requests/http_client.dart'; 5 | 6 | /// 通用的请求 7 | class CommonRequest { 8 | Future checkUpdate() async { 9 | try { 10 | return await checkUpdateGitMirror(); 11 | } catch (e) { 12 | return await checkUpdateJsDelivr(); 13 | } 14 | } 15 | 16 | /// 检查更新 17 | Future checkUpdateGitMirror() async { 18 | var result = await HttpClient.instance.getJson( 19 | "https://raw.gitmirror.com/GH4NG/dart_simple_live/master/assets/app_version.json", 20 | queryParameters: { 21 | "ts": DateTime.now().millisecondsSinceEpoch, 22 | }, 23 | ); 24 | if (result is Map) { 25 | return VersionModel.fromJson(result as Map); 26 | } 27 | return VersionModel.fromJson(json.decode(result)); 28 | } 29 | 30 | /// 检查更新 31 | Future checkUpdateJsDelivr() async { 32 | var result = await HttpClient.instance.getJson( 33 | "https://cdn.jsdelivr.net/gh/GH4NG/dart_simple_live@master/assets/app_version.json", 34 | queryParameters: { 35 | "ts": DateTime.now().millisecondsSinceEpoch, 36 | }, 37 | ); 38 | if (result is Map) { 39 | return VersionModel.fromJson(result as Map); 40 | } 41 | return VersionModel.fromJson(json.decode(result)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/models/account/bilibili_user_info_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | T? asT(dynamic value) { 4 | if (value is T) { 5 | return value; 6 | } 7 | return null; 8 | } 9 | 10 | class BiliBiliUserInfoModel { 11 | BiliBiliUserInfoModel({ 12 | this.mid, 13 | this.uname, 14 | this.userid, 15 | this.sign, 16 | this.birthday, 17 | this.sex, 18 | this.nickFree, 19 | this.rank, 20 | }); 21 | 22 | factory BiliBiliUserInfoModel.fromJson(Map json) => 23 | BiliBiliUserInfoModel( 24 | mid: asT(json['mid']), 25 | uname: asT(json['uname']), 26 | userid: asT(json['userid']), 27 | sign: asT(json['sign']), 28 | birthday: asT(json['birthday']), 29 | sex: asT(json['sex']), 30 | nickFree: asT(json['nick_free']), 31 | rank: asT(json['rank']), 32 | ); 33 | 34 | int? mid; 35 | String? uname; 36 | String? userid; 37 | String? sign; 38 | String? birthday; 39 | String? sex; 40 | bool? nickFree; 41 | String? rank; 42 | 43 | @override 44 | String toString() { 45 | return jsonEncode(this); 46 | } 47 | 48 | Map toJson() => { 49 | 'mid': mid, 50 | 'uname': uname, 51 | 'userid': userid, 52 | 'sign': sign, 53 | 'birthday': birthday, 54 | 'sex': sex, 55 | 'nick_free': nickFree, 56 | 'rank': rank, 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /lib/app/utils/document.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:simple_live_app/app/log.dart'; 4 | 5 | // 扩展 Directory 类,添加清空文件夹的功能并验证是否为文件夹 6 | extension DirectoryCleaner on Directory { 7 | Future clear() async { 8 | // 首先判断是否为文件夹 9 | if (await exists() && FileSystemEntity.isDirectorySync(path)) { 10 | // 列出文件夹中的所有文件和子文件夹 11 | List files = listSync(); 12 | 13 | // 遍历文件列表并删除每个文件或子文件夹 14 | for (FileSystemEntity file in files) { 15 | if (file is File) { 16 | await file.delete(); 17 | Log.i('删除文件: ${file.path}'); 18 | } else if (file is Directory) { 19 | await Directory(file.path).delete(recursive: true); 20 | Log.i('删除文件夹: ${file.path}'); 21 | } 22 | } 23 | 24 | Log.i('文件夹清空完成'); 25 | } else { 26 | Log.i('$path 不是一个有效的文件夹'); 27 | } 28 | } 29 | 30 | // 阻塞主线程 31 | void clearSync() { 32 | if (existsSync() && FileSystemEntity.isDirectorySync(path)) { 33 | List files = listSync(); 34 | for (FileSystemEntity file in files) { 35 | if (file is File) { 36 | file.deleteSync(); 37 | Log.i('删除文件: ${file.path}'); 38 | } else if (file is Directory) { 39 | Directory(file.path).deleteSync(recursive: true); 40 | Log.i('删除文件夹: ${file.path}'); 41 | } 42 | } 43 | 44 | Log.i('文件夹清空完成'); 45 | } else { 46 | Log.i('$path 不是一个有效的文件夹'); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.15' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/model/tars/get_cdn_token_req.dart: -------------------------------------------------------------------------------- 1 | import 'package:tars_flutter/tars/codec/tars_displayer.dart'; 2 | import 'package:tars_flutter/tars/codec/tars_input_stream.dart'; 3 | import 'package:tars_flutter/tars/codec/tars_output_stream.dart'; 4 | import 'package:tars_flutter/tars/codec/tars_struct.dart'; 5 | 6 | class GetCdnTokenReq extends TarsStruct { 7 | String url = ""; 8 | 9 | String cdnType = ""; 10 | 11 | String streamName = ""; 12 | 13 | int presenterUid = 0; 14 | 15 | @override 16 | void readFrom(TarsInputStream inputStream) { 17 | url = inputStream.read(url, 0, false); 18 | cdnType = inputStream.read(cdnType, 1, false); 19 | streamName = inputStream.read(streamName, 2, false); 20 | presenterUid = inputStream.read(presenterUid, 3, false); 21 | } 22 | 23 | @override 24 | void writeTo(TarsOutputStream outputStream) { 25 | outputStream 26 | ..write(url, 0) 27 | ..write(cdnType, 1) 28 | ..write(streamName, 2) 29 | ..write(presenterUid, 3); 30 | } 31 | 32 | @override 33 | Object deepCopy() { 34 | return GetCdnTokenReq() 35 | ..url = url 36 | ..cdnType = cdnType 37 | ..streamName = streamName 38 | ..presenterUid = presenterUid; 39 | } 40 | 41 | @override 42 | void displayAsString(StringBuffer sb, int level) { 43 | TarsDisplayer(sb, level: level) 44 | ..DisplayString(url, "url") 45 | ..DisplayString(cdnType, "cdnType") 46 | ..DisplayString(streamName, "streamName") 47 | ..DisplayInt(presenterUid, "presenterUid"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/modules/home/home_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'package:get/get.dart'; 6 | import 'package:simple_live_app/app/event_bus.dart'; 7 | import 'package:simple_live_app/app/sites.dart'; 8 | import 'package:simple_live_app/modules/home/home_list_controller.dart'; 9 | import 'package:simple_live_app/routes/route_path.dart'; 10 | 11 | class HomeController extends GetxController 12 | with GetSingleTickerProviderStateMixin { 13 | late TabController tabController; 14 | HomeController() { 15 | tabController = TabController( 16 | length: Sites.supportSites.length, 17 | vsync: this, 18 | ); 19 | } 20 | 21 | StreamSubscription? streamSubscription; 22 | 23 | @override 24 | void onInit() { 25 | streamSubscription = EventBus.instance.listen( 26 | EventBus.kBottomNavigationBarClicked, 27 | (index) { 28 | if (index == 0) { 29 | refreshOrScrollTop(); 30 | } 31 | }, 32 | ); 33 | for (var site in Sites.supportSites) { 34 | Get.put(HomeListController(site), tag: site.id); 35 | } 36 | 37 | super.onInit(); 38 | } 39 | 40 | void refreshOrScrollTop() { 41 | var tabIndex = tabController.index; 42 | Get.find( 43 | tag: Sites.supportSites[tabIndex].id, 44 | ).scrollToTopOrRefresh(); 45 | } 46 | 47 | void toSearch() { 48 | Get.toNamed(RoutePath.kSearch); 49 | } 50 | 51 | @override 52 | void onClose() { 53 | streamSubscription?.cancel(); 54 | super.onClose(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /assets/icons/icon_volume.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/models/db/history.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive/hive.dart'; 2 | import 'package:simple_live_app/app/utils/duration_2str.dart'; 3 | import 'package:simple_live_app/app/utils/dynamic_filter.dart'; 4 | 5 | part 'history.g.dart'; 6 | 7 | @HiveType(typeId: 2) 8 | class History implements Mappable { 9 | History({ 10 | required this.id, 11 | required this.roomId, 12 | required this.siteId, 13 | required this.userName, 14 | required this.face, 15 | required this.updateTime, 16 | this.watchDuration = "00:00:00", 17 | }); 18 | 19 | ///id=siteId_roomId 20 | @HiveField(0) 21 | String id; 22 | 23 | @HiveField(1) 24 | String roomId; 25 | 26 | @HiveField(2) 27 | String siteId; 28 | 29 | @HiveField(3) 30 | String userName; 31 | 32 | @HiveField(4) 33 | String face; 34 | 35 | @HiveField(5) 36 | DateTime updateTime; 37 | 38 | @HiveField(6) 39 | String? watchDuration; // "00:00:00" 40 | 41 | Duration get duration => watchDuration!.toDuration(); //for filter 42 | 43 | factory History.fromJson(Map json) => History( 44 | id: json["id"], 45 | roomId: json["roomId"], 46 | siteId: json["siteId"], 47 | userName: json["userName"], 48 | face: json["face"], 49 | updateTime: DateTime.parse(json["updateTime"]), 50 | watchDuration: json["watchDuration"] ?? "00:00:00", 51 | ); 52 | 53 | Map toJson() => { 54 | "id": id, 55 | "roomId": roomId, 56 | "siteId": siteId, 57 | "userName": userName, 58 | "face": face, 59 | "updateTime": updateTime.toString(), 60 | "watchDuration": watchDuration ?? "00:00:00", 61 | }; 62 | 63 | @override 64 | Map toMap() => toJson(); 65 | } 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/widgets/net_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class NetImage extends StatelessWidget { 5 | final String picUrl; 6 | final double? width; 7 | final double? height; 8 | final BoxFit? fit; 9 | final double borderRadius; 10 | const NetImage( 11 | this.picUrl, { 12 | this.width, 13 | this.height, 14 | this.fit = BoxFit.cover, 15 | this.borderRadius = 0, 16 | super.key, 17 | }); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | if (picUrl.isEmpty) { 22 | return Image.asset( 23 | 'assets/images/logo.png', 24 | width: width, 25 | height: height, 26 | ); 27 | } 28 | var pic = picUrl; 29 | if (pic.startsWith("//")) { 30 | pic = 'https:$pic'; 31 | } 32 | return ClipRRect( 33 | borderRadius: BorderRadius.circular(borderRadius), 34 | child: ExtendedImage.network( 35 | pic, 36 | fit: fit, 37 | height: height, 38 | width: width, 39 | shape: BoxShape.rectangle, 40 | borderRadius: BorderRadius.circular(borderRadius), 41 | loadStateChanged: (e) { 42 | if (e.extendedImageLoadState == LoadState.loading) { 43 | return const Icon( 44 | Icons.image, 45 | color: Colors.grey, 46 | size: 24, 47 | ); 48 | } 49 | if (e.extendedImageLoadState == LoadState.failed) { 50 | return const Icon( 51 | Icons.broken_image, 52 | color: Colors.grey, 53 | size: 24, 54 | ); 55 | } 56 | return null; 57 | }, 58 | ), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/widgets/settings/settings_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:simple_live_app/app/app_style.dart'; 4 | 5 | class SettingsAction extends StatelessWidget { 6 | final String title; 7 | final String? subtitle; 8 | final Function()? onTap; 9 | final String? value; 10 | final Widget? leading; 11 | 12 | const SettingsAction({ 13 | required this.title, 14 | this.value, 15 | this.onTap, 16 | this.subtitle, 17 | this.leading, 18 | super.key, 19 | }); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return ListTile( 24 | // visualDensity: VisualDensity.compact, 25 | leading: leading, 26 | title: Text( 27 | title, 28 | style: Theme.of(context).textTheme.bodyLarge, 29 | ), 30 | shape: RoundedRectangleBorder( 31 | borderRadius: AppStyle.radius8, 32 | ), 33 | contentPadding: AppStyle.edgeInsetsL16.copyWith(right: 8), 34 | subtitle: subtitle == null 35 | ? null 36 | : Text( 37 | subtitle!, 38 | style: Get.textTheme.bodySmall!.copyWith(color: Colors.grey), 39 | ), 40 | trailing: Row( 41 | mainAxisSize: MainAxisSize.min, 42 | children: [ 43 | if (value != null) 44 | Text( 45 | value!, 46 | style: Theme.of( 47 | context, 48 | ).textTheme.bodyMedium!.copyWith(color: Colors.grey), 49 | ), 50 | AppStyle.hGap4, 51 | const Icon( 52 | Icons.chevron_right, 53 | color: Colors.grey, 54 | ), 55 | ], 56 | ), 57 | onTap: onTap, 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/models/db/follow_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:hive/hive.dart'; 3 | 4 | part 'follow_user.g.dart'; 5 | 6 | @HiveType(typeId: 1) 7 | class FollowUser { 8 | FollowUser({ 9 | required this.id, 10 | required this.roomId, 11 | required this.siteId, 12 | required this.userName, 13 | required this.face, 14 | required this.addTime, 15 | this.watchDuration = "00:00:00", 16 | this.tag = "全部", 17 | }); 18 | 19 | ///id=siteId_roomId 20 | @HiveField(0) 21 | String id; 22 | 23 | @HiveField(1) 24 | String roomId; 25 | 26 | @HiveField(2) 27 | String siteId; 28 | 29 | @HiveField(3) 30 | String userName; 31 | 32 | @HiveField(4) 33 | String face; 34 | 35 | @HiveField(5) 36 | DateTime addTime; 37 | 38 | @HiveField(6) 39 | String? watchDuration; // "00:00:00" 40 | 41 | @HiveField(7) 42 | String tag; 43 | 44 | /// 直播状态 45 | /// 0=未知(加载中) 1=未开播 2=直播中 46 | Rx liveStatus = 0.obs; 47 | 48 | /// 开播时间戳 49 | String? liveStartTime; 50 | 51 | String liveTitle = ''; 52 | String liveAreaName = ''; 53 | 54 | factory FollowUser.fromJson(Map json) => FollowUser( 55 | id: json['id'], 56 | roomId: json['roomId'], 57 | siteId: json['siteId'], 58 | userName: json['userName'], 59 | face: json['face'], 60 | addTime: DateTime.parse(json['addTime']), 61 | watchDuration: json["watchDuration"] ?? "00:00:00", 62 | tag: json["tag"] ?? "全部", 63 | ); 64 | 65 | Map toJson() => { 66 | 'id': id, 67 | 'roomId': roomId, 68 | 'siteId': siteId, 69 | 'userName': userName, 70 | 'face': face, 71 | 'addTime': addTime.toString(), 72 | "watchDuration": watchDuration ?? "00:00:00", 73 | "tag": tag, 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /lib/models/db/history.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'history.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class HistoryAdapter extends TypeAdapter { 10 | @override 11 | final int typeId = 2; 12 | 13 | @override 14 | History read(BinaryReader reader) { 15 | final numOfFields = reader.readByte(); 16 | final fields = { 17 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 18 | }; 19 | return History( 20 | id: fields[0] as String, 21 | roomId: fields[1] as String, 22 | siteId: fields[2] as String, 23 | userName: fields[3] as String, 24 | face: fields[4] as String, 25 | updateTime: fields[5] as DateTime, 26 | watchDuration: fields[6] as String?, 27 | ); 28 | } 29 | 30 | @override 31 | void write(BinaryWriter writer, History obj) { 32 | writer 33 | ..writeByte(7) 34 | ..writeByte(0) 35 | ..write(obj.id) 36 | ..writeByte(1) 37 | ..write(obj.roomId) 38 | ..writeByte(2) 39 | ..write(obj.siteId) 40 | ..writeByte(3) 41 | ..write(obj.userName) 42 | ..writeByte(4) 43 | ..write(obj.face) 44 | ..writeByte(5) 45 | ..write(obj.updateTime) 46 | ..writeByte(6) 47 | ..write(obj.watchDuration); 48 | } 49 | 50 | @override 51 | int get hashCode => typeId.hashCode; 52 | 53 | @override 54 | bool operator ==(Object other) => 55 | identical(this, other) || 56 | other is HistoryAdapter && 57 | runtimeType == other.runtimeType && 58 | typeId == other.typeId; 59 | } 60 | -------------------------------------------------------------------------------- /lib/modules/mine/account/bilibili/web_login_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:simple_live_app/modules/mine/account/bilibili/web_login_controller.dart'; 5 | 6 | class BiliBiliWebLoginPage extends GetView { 7 | const BiliBiliWebLoginPage({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: AppBar( 13 | title: const Text("哔哩哔哩账号登录"), 14 | actions: [ 15 | TextButton.icon( 16 | onPressed: controller.toQRLogin, 17 | icon: const Icon(Icons.qr_code), 18 | label: const Text("二维码登录"), 19 | ), 20 | ], 21 | ), 22 | body: InAppWebView( 23 | onWebViewCreated: controller.onWebViewCreated, 24 | onLoadStop: controller.onLoadStop, 25 | initialSettings: InAppWebViewSettings( 26 | userAgent: 27 | "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Edg/118.0.0.0", 28 | useShouldOverrideUrlLoading: true, 29 | ), 30 | shouldOverrideUrlLoading: (webController, navigationAction) async { 31 | var uri = navigationAction.request.url; 32 | if (uri == null) { 33 | return NavigationActionPolicy.ALLOW; 34 | } 35 | if (uri.host == "m.bilibili.com" || uri.host == "www.bilibili.com") { 36 | await controller.logged(); 37 | return NavigationActionPolicy.CANCEL; 38 | } 39 | return NavigationActionPolicy.ALLOW; 40 | }, 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/modules/test/test_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:simple_live_app/app/app_style.dart'; 3 | import 'package:simple_live_app/app/log.dart'; 4 | import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; 5 | 6 | class TestPage extends StatelessWidget { 7 | const TestPage({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | Future function1() async { 12 | const msg = '测试功能一'; 13 | SmartDialog.showToast('测试功能一'); 14 | Log.d(msg); 15 | } 16 | 17 | void function2() { 18 | SmartDialog.showToast('测试功能二'); 19 | Log.d('测试功能二'); 20 | } 21 | 22 | void function3() { 23 | SmartDialog.showToast('测试功能三'); 24 | Log.d('测试功能三'); 25 | } 26 | 27 | return Scaffold( 28 | appBar: AppBar( 29 | title: const Text('功能测试页'), 30 | ), 31 | body: Padding( 32 | padding: const EdgeInsets.all(24), 33 | child: Column( 34 | crossAxisAlignment: CrossAxisAlignment.stretch, 35 | children: [ 36 | ElevatedButton.icon( 37 | onPressed: function1, 38 | icon: const Icon(Icons.check_circle_outline), 39 | label: const Text('测试功能一'), 40 | ), 41 | AppStyle.hGap16, 42 | ElevatedButton.icon( 43 | onPressed: function2, 44 | icon: const Icon(Icons.check_circle_outline), 45 | label: const Text('测试功能二'), 46 | ), 47 | AppStyle.hGap16, 48 | ElevatedButton.icon( 49 | onPressed: function3, 50 | icon: const Icon(Icons.check_circle_outline), 51 | label: const Text('测试功能三'), 52 | ), 53 | ], 54 | ), 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /assets/icons/icon_scalewindow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/modules/mine/account/bilibili/web_login_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:simple_live_app/app/controller/base_controller.dart'; 4 | import 'package:simple_live_app/app/log.dart'; 5 | import 'package:simple_live_app/routes/route_path.dart'; 6 | import 'package:simple_live_app/services/bilibili_account_service.dart'; 7 | 8 | class BiliBiliWebLoginController extends BaseController { 9 | InAppWebViewController? webViewController; 10 | final CookieManager cookieManager = CookieManager.instance(); 11 | void onWebViewCreated(InAppWebViewController controller) { 12 | webViewController = controller; 13 | webViewController!.loadUrl( 14 | urlRequest: URLRequest( 15 | url: WebUri("https://passport.bilibili.com/login"), 16 | ), 17 | ); 18 | } 19 | 20 | Future toQRLogin() async { 21 | await Get.toNamed(RoutePath.kBiliBiliQRLogin); 22 | Get.back(); 23 | } 24 | 25 | Future onLoadStop(InAppWebViewController controller, Uri? uri) async { 26 | if (uri == null) { 27 | return; 28 | } 29 | if (uri.host == "m.bilibili.com") { 30 | logged(); 31 | } 32 | } 33 | 34 | Future logged() async { 35 | try { 36 | var cookies = await cookieManager.getCookies( 37 | url: WebUri("https://bilibili.com"), 38 | ); 39 | if (cookies.isEmpty) { 40 | return false; 41 | } 42 | var cookieStr = cookies.map((e) => "${e.name}=${e.value}").join(";"); 43 | Log.i(cookieStr); 44 | BiliBiliAccountService.instance.setCookie(cookieStr); 45 | await BiliBiliAccountService.instance.loadUserInfo(); 46 | Get.back(); 47 | return true; 48 | } catch (e) { 49 | return false; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/model/live_room_detail.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class LiveRoomDetail { 4 | /// 房间ID 5 | final String roomId; 6 | 7 | /// 房间标题 8 | final String title; 9 | 10 | /// 封面 11 | final String cover; 12 | 13 | /// 分区名称 14 | final String areaName; 15 | 16 | /// 用户名 17 | final String userName; 18 | 19 | /// 头像 20 | final String userAvatar; 21 | 22 | /// 在线 23 | final int online; 24 | 25 | /// 介绍 26 | final String? introduction; 27 | 28 | /// 公告 29 | final String? notice; 30 | 31 | /// 状态 32 | final bool status; 33 | 34 | /// 附加信息 35 | final dynamic data; 36 | 37 | /// 弹幕附加信息 38 | final dynamic danmakuData; 39 | 40 | /// 是否录播 41 | final bool isRecord; 42 | 43 | /// 链接 44 | final String url; 45 | 46 | /// 显示时间 47 | final String? showTime; 48 | 49 | LiveRoomDetail({ 50 | required this.roomId, 51 | required this.title, 52 | required this.cover, 53 | required this.areaName, 54 | required this.userName, 55 | required this.userAvatar, 56 | required this.online, 57 | this.introduction, 58 | this.notice, 59 | required this.status, 60 | this.data, 61 | this.danmakuData, 62 | required this.url, 63 | this.isRecord = false, 64 | this.showTime, 65 | }); 66 | 67 | @override 68 | String toString() { 69 | return json.encode({ 70 | "roomId": roomId, 71 | "title": title, 72 | "cover": cover, 73 | "areaName": areaName, 74 | "userName": userName, 75 | "userAvatar": userAvatar, 76 | "online": online, 77 | "introduction": introduction, 78 | "notice": notice, 79 | "status": status, 80 | "data": data.toString(), 81 | "danmakuData": danmakuData.toString(), 82 | "url": url, 83 | "isRecord": isRecord, 84 | "showTime": showTime, 85 | }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/models/db/follow_user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'follow_user.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class FollowUserAdapter extends TypeAdapter { 10 | @override 11 | final int typeId = 1; 12 | 13 | @override 14 | FollowUser read(BinaryReader reader) { 15 | final numOfFields = reader.readByte(); 16 | final fields = { 17 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 18 | }; 19 | return FollowUser( 20 | id: fields[0] as String, 21 | roomId: fields[1] as String, 22 | siteId: fields[2] as String, 23 | userName: fields[3] as String, 24 | face: fields[4] as String, 25 | addTime: fields[5] as DateTime, 26 | watchDuration: fields[6] as String?, 27 | tag: fields[7] ?? "", 28 | ); 29 | } 30 | 31 | @override 32 | void write(BinaryWriter writer, FollowUser obj) { 33 | writer 34 | ..writeByte(8) 35 | ..writeByte(0) 36 | ..write(obj.id) 37 | ..writeByte(1) 38 | ..write(obj.roomId) 39 | ..writeByte(2) 40 | ..write(obj.siteId) 41 | ..writeByte(3) 42 | ..write(obj.userName) 43 | ..writeByte(4) 44 | ..write(obj.face) 45 | ..writeByte(5) 46 | ..write(obj.addTime) 47 | ..writeByte(6) 48 | ..write(obj.watchDuration) 49 | ..writeByte(7) 50 | ..write(obj.tag); 51 | } 52 | 53 | @override 54 | int get hashCode => typeId.hashCode; 55 | 56 | @override 57 | bool operator ==(Object other) => 58 | identical(this, other) || 59 | other is FollowUserAdapter && 60 | runtimeType == other.runtimeType && 61 | typeId == other.typeId; 62 | } 63 | -------------------------------------------------------------------------------- /lib/modules/category/category_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:simple_live_app/app/app_style.dart'; 4 | import 'package:simple_live_app/app/sites.dart'; 5 | import 'package:simple_live_app/modules/category/category_controller.dart'; 6 | import 'package:simple_live_app/modules/category/category_list_view.dart'; 7 | 8 | class CategoryPage extends GetView { 9 | const CategoryPage({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | titleSpacing: 8, 16 | title: TabBar( 17 | controller: controller.tabController, 18 | padding: EdgeInsets.zero, 19 | tabAlignment: TabAlignment.center, 20 | tabs: Sites.supportSites 21 | .map( 22 | (e) => Tab( 23 | //text: e.name, 24 | child: Row( 25 | children: [ 26 | Image.asset( 27 | e.logo, 28 | width: 24, 29 | semanticLabel: '${e.name} logo', 30 | ), 31 | AppStyle.hGap8, 32 | Text(e.name), 33 | ], 34 | ), 35 | ), 36 | ) 37 | .toList(), 38 | labelPadding: AppStyle.edgeInsetsH20, 39 | isScrollable: true, 40 | indicatorSize: TabBarIndicatorSize.label, 41 | ), 42 | ), 43 | body: TabBarView( 44 | controller: controller.tabController, 45 | children: Sites.supportSites 46 | .map( 47 | (e) => CategoryListView( 48 | e.id, 49 | ), 50 | ) 51 | .toList(), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "size": "16x16", 5 | "idiom": "mac", 6 | "filename": "icon-16.png", 7 | "scale": "1x" 8 | }, 9 | { 10 | "size": "16x16", 11 | "idiom": "mac", 12 | "filename": "icon-16@2x.png", 13 | "scale": "2x" 14 | }, 15 | { 16 | "size": "32x32", 17 | "idiom": "mac", 18 | "filename": "icon-32.png", 19 | "scale": "1x" 20 | }, 21 | { 22 | "size": "32x32", 23 | "idiom": "mac", 24 | "filename": "icon-32@2x.png", 25 | "scale": "2x" 26 | }, 27 | { 28 | "size": "128x128", 29 | "idiom": "mac", 30 | "filename": "icon-128.png", 31 | "scale": "1x" 32 | }, 33 | { 34 | "size": "128x128", 35 | "idiom": "mac", 36 | "filename": "icon-128@2x.png", 37 | "scale": "2x" 38 | }, 39 | { 40 | "size": "256x256", 41 | "idiom": "mac", 42 | "filename": "icon-256.png", 43 | "scale": "1x" 44 | }, 45 | { 46 | "size": "256x256", 47 | "idiom": "mac", 48 | "filename": "icon-256@2x.png", 49 | "scale": "2x" 50 | }, 51 | { 52 | "size": "512x512", 53 | "idiom": "mac", 54 | "filename": "icon-512.png", 55 | "scale": "1x" 56 | }, 57 | { 58 | "size": "512x512", 59 | "idiom": "mac", 60 | "filename": "icon-512@2x.png", 61 | "scale": "2x" 62 | } 63 | ], 64 | "info": { 65 | "version": 1, 66 | "author": "icon.wuruihong.com" 67 | } 68 | } -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /lib/modules/live_room/player/player_states.dart: -------------------------------------------------------------------------------- 1 | class PlayerState { 2 | final List playlist; 3 | 4 | final bool buffering; 5 | 6 | final String audioParams; 7 | 8 | final String audioTrack; 9 | 10 | final String videoParams; 11 | 12 | final String videoTrack; 13 | 14 | final int? width; 15 | 16 | final int? height; 17 | 18 | final bool? isVertical; 19 | 20 | final double? fps; 21 | 22 | const PlayerState({ 23 | this.playlist = const [], 24 | this.buffering = false, 25 | this.audioParams = "", 26 | this.audioTrack = "", 27 | this.videoParams = "", 28 | this.videoTrack = "", 29 | this.width, 30 | this.height, 31 | this.isVertical, 32 | this.fps, 33 | }); 34 | 35 | PlayerState copyWith({ 36 | List? playlist, 37 | bool? buffering, 38 | String? audioParams, 39 | String? audioTrack, 40 | String? videoParams, 41 | String? videoTrack, 42 | int? width, 43 | int? height, 44 | bool? isVertical, 45 | double? fps, 46 | }) { 47 | return PlayerState( 48 | playlist: playlist ?? this.playlist, 49 | buffering: buffering ?? this.buffering, 50 | audioParams: audioParams ?? this.audioParams, 51 | audioTrack: audioTrack ?? this.audioTrack, 52 | videoParams: videoParams ?? this.videoParams, 53 | videoTrack: videoTrack ?? this.videoTrack, 54 | width: width ?? this.width, 55 | height: height ?? this.height, 56 | isVertical: isVertical ?? this.isVertical, 57 | fps: fps ?? this.fps, 58 | ); 59 | } 60 | 61 | @override 62 | String toString() => 63 | 'Player(' 64 | 'playlist: $playlist, ' 65 | 'buffering: $buffering, ' 66 | 'audioParams: $audioParams, ' 67 | 'audioTrack: $audioTrack, ' 68 | 'videoParams: $videoParams, ' 69 | 'videoTrack: $videoTrack, ' 70 | 'width: $width, ' 71 | 'height: $height, ' 72 | 'isVertical: $isVertical, ' 73 | 'fps: $fps' 74 | ')'; 75 | } 76 | -------------------------------------------------------------------------------- /lib/modules/home/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:simple_live_app/app/app_style.dart'; 4 | import 'package:simple_live_app/app/sites.dart'; 5 | import 'package:simple_live_app/modules/home/home_controller.dart'; 6 | import 'package:simple_live_app/modules/home/home_list_view.dart'; 7 | 8 | class HomePage extends GetView { 9 | const HomePage({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | titleSpacing: 8, 16 | title: TabBar( 17 | controller: controller.tabController, 18 | labelPadding: AppStyle.edgeInsetsH20, 19 | isScrollable: true, 20 | indicatorSize: TabBarIndicatorSize.label, 21 | tabAlignment: TabAlignment.center, 22 | tabs: Sites.supportSites 23 | .map( 24 | (e) => Tab( 25 | //text: e.name, 26 | child: Row( 27 | children: [ 28 | Image.asset( 29 | e.logo, 30 | width: 24, 31 | semanticLabel: '${e.name} logo', 32 | ), 33 | AppStyle.hGap8, 34 | Text(e.name), 35 | ], 36 | ), 37 | ), 38 | ) 39 | .toList(), 40 | ), 41 | actions: [ 42 | IconButton( 43 | onPressed: controller.toSearch, 44 | icon: const Icon(Icons.search), 45 | ), 46 | ], 47 | ), 48 | body: TabBarView( 49 | controller: controller.tabController, 50 | children: Sites.supportSites 51 | .map( 52 | (e) => HomeListView( 53 | e.id, 54 | ), 55 | ) 56 | .toList(), 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /windows/packaging/exe/inno_setup.iss: -------------------------------------------------------------------------------- 1 | [Setup] 2 | AppId={{APP_ID}} 3 | AppVersion={{APP_VERSION}} 4 | AppName={{DISPLAY_NAME}} 5 | AppPublisher={{PUBLISHER_NAME}} 6 | AppPublisherURL={{PUBLISHER_URL}} 7 | AppSupportURL={{PUBLISHER_URL}} 8 | AppUpdatesURL={{PUBLISHER_URL}} 9 | DefaultDirName={{INSTALL_DIR_NAME}} 10 | DisableProgramGroupPage=yes 11 | OutputDir=. 12 | OutputBaseFilename={{OUTPUT_BASE_FILENAME}} 13 | Compression=lzma 14 | SolidCompression=yes 15 | SetupIconFile={{SETUP_ICON_FILE}} 16 | WizardStyle=modern 17 | PrivilegesRequired={{PRIVILEGES_REQUIRED}} 18 | ArchitecturesAllowed=x64 19 | ArchitecturesInstallIn64BitMode=x64 20 | 21 | [Code] 22 | procedure KillOldProcess; 23 | var ResultCode: Integer; 24 | begin 25 | Exec('taskkill', '/F /IM {{EXECUTABLE_NAME}}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); 26 | end; 27 | 28 | function InitializeSetup(): Boolean; 29 | begin 30 | KillOldProcess; 31 | Result := True; 32 | end; 33 | 34 | [Languages] 35 | Name: "english"; MessagesFile: "compiler:Default.isl" 36 | Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl" 37 | 38 | [Tasks] 39 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce 40 | Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 41 | 42 | [Files] 43 | Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 44 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 45 | 46 | [Icons] 47 | Name: "{autoprograms}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}" 48 | Name: "{autodesktop}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; Tasks: desktopicon 49 | Name: "{userstartup}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; WorkingDir: "{app}"; Tasks: launchAtStartup 50 | 51 | [Run] 52 | Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: runascurrentuser nowait postinstall skipifsilent -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | unsigned int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length == 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /lib/modules/other/debug_log_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:get/get.dart'; 5 | import 'package:path_provider/path_provider.dart'; 6 | import 'package:share_plus/share_plus.dart'; 7 | import 'package:simple_live_app/app/app_style.dart'; 8 | import 'package:simple_live_app/app/log.dart'; 9 | 10 | class DebugLogPage extends StatelessWidget { 11 | const DebugLogPage({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text("Log"), 18 | actions: [ 19 | IconButton( 20 | onPressed: () async { 21 | var msg = Log.debugLogs 22 | .map((x) => "${x.datetime}\r\n${x.content}") 23 | .join('\r\n\r\n'); 24 | var dir = await getApplicationDocumentsDirectory(); 25 | var logFile = File( 26 | '${dir.path}/${DateTime.now().millisecondsSinceEpoch}.log', 27 | ); 28 | await logFile.writeAsString(msg); 29 | 30 | SharePlus.instance.share( 31 | ShareParams( 32 | files: [XFile(logFile.path)], 33 | ), 34 | ); 35 | }, 36 | icon: const Icon(Icons.save), 37 | ), 38 | IconButton( 39 | onPressed: () { 40 | Log.debugLogs.clear(); 41 | }, 42 | icon: const Icon(Icons.clear_all), 43 | ), 44 | ], 45 | ), 46 | body: Obx( 47 | () => ListView.separated( 48 | itemCount: Log.debugLogs.length, 49 | separatorBuilder: (_, i) => const Divider(), 50 | padding: AppStyle.edgeInsetsA12, 51 | itemBuilder: (_, i) { 52 | var item = Log.debugLogs[i]; 53 | return SelectableText( 54 | "${item.datetime.toString()}\r\n${item.content}", 55 | style: TextStyle( 56 | color: item.color, 57 | fontSize: 12, 58 | ), 59 | ); 60 | }, 61 | ), 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/routes/route_path.dart: -------------------------------------------------------------------------------- 1 | /// 路由路径 2 | class RoutePath { 3 | /// 首页 4 | static const kIndex = "/index"; 5 | 6 | /// 搜索 7 | static const kSearch = "/search"; 8 | 9 | /// 分类详情 10 | static const kCategoryDetail = "/category/detail"; 11 | 12 | /// 直播间 13 | static const kLiveRoomDetail = "/room/detail"; 14 | 15 | /// 弹幕设置 16 | static const kSettingsDanmu = "/settings/danmu"; 17 | 18 | /// 定时关闭设置 19 | static const kSettingsAutoExit = "/settings/auto_exit"; 20 | 21 | /// 直播间设置 22 | static const kSettingsPlay = "/settings/play"; 23 | 24 | /// 播放器设置 25 | static const kSettingsPlayer = "/settings/player"; 26 | 27 | /// 弹幕关键词屏蔽 28 | static const kSettingsDanmuShield = "/settings/danmu/shield"; 29 | 30 | /// 其他设置 31 | static const kSettingsOther = "/settings/other"; 32 | 33 | /// 赞助 34 | static const kSponsor = "/sponsor"; 35 | 36 | /// 历史记录 37 | static const kHistory = "/user/history"; 38 | 39 | /// 我的关注 40 | static const kFollowUser = "/user/follow"; 41 | 42 | /// 关注用户-信息详情 43 | static const kFollowInfo = "/user/follow/info"; 44 | 45 | /// 工具箱 46 | static const kTools = "/other/tools"; 47 | 48 | /// 主页设置 49 | static const kSettingsIndexed = "/settings/indexed"; 50 | 51 | /// 外观设置 52 | static const kAppstyleSetting = "/settings/appstyle"; 53 | 54 | /// 账号管理 55 | static const kSettingsAccount = "/settings/account"; 56 | 57 | /// 关注设置 58 | static const kSettingsFollow = "/settings/follow"; 59 | 60 | /// BiliBili Web登录 61 | static const kBiliBiliWebLogin = "/settings/account/bilibili/web_login"; 62 | 63 | /// BiliBili 二维码登录 64 | static const kBiliBiliQRLogin = "/settings/account/bilibili/qr_login"; 65 | 66 | /// 数据同步 67 | static const kLocalSync = "/local_sync"; 68 | 69 | /// 数据同步 70 | static const kSync = "/sync"; 71 | 72 | /// 扫描 73 | static const kSyncScan = "/sync/scan"; 74 | 75 | /// 同步设备 76 | static const kLocalSyncDevice = "/sync/device"; 77 | 78 | /// 远程同步-房间 79 | static const kRemoteSyncRoom = "/remote_sync/room"; 80 | 81 | /// 远程同步-WebDAV 82 | static const kRemoteSyncWebDav = "/remote_sync/webDAV"; 83 | 84 | /// 远程同步-WebDAVConfig 85 | static const kRemoteSyncWebDavConfig = "/remote_sync/webDAVConfig"; 86 | 87 | /// 测试页面 88 | static const kTest = "/test"; 89 | } 90 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/common/core_log.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | 3 | enum RequestLogType { 4 | /// 输出所有请求信息 5 | /// 包括请求的URL,请求的参数,请求的头,请求的体,响应的头,响应的内容,耗时 6 | all, 7 | 8 | /// 简洁的输出 9 | /// 仅输出请求的URL和响应的状态码 10 | short, 11 | 12 | /// 不输出请求信息 13 | none, 14 | } 15 | 16 | class CoreLog { 17 | /// 是否启用日志 18 | static bool enableLog = true; 19 | 20 | /// 请求日志模式 21 | static RequestLogType requestLogType = RequestLogType.all; 22 | 23 | static Function(Level, String)? onPrintLog; 24 | static Logger logger = Logger( 25 | printer: PrettyPrinter( 26 | methodCount: 0, 27 | errorMethodCount: 8, 28 | lineLength: 120, 29 | colors: true, 30 | printEmojis: true, 31 | ), 32 | ); 33 | 34 | static void d(String message) { 35 | if (!enableLog) { 36 | return; 37 | } 38 | onPrintLog?.call(Level.debug, message); 39 | if (onPrintLog == null) { 40 | logger.d("${DateTime.now().toString()}\n$message"); 41 | } 42 | } 43 | 44 | static void i(String message) { 45 | if (!enableLog) { 46 | return; 47 | } 48 | onPrintLog?.call(Level.info, message); 49 | if (onPrintLog == null) { 50 | logger.i("${DateTime.now().toString()}\n$message"); 51 | } 52 | } 53 | 54 | static void e(String message, StackTrace stackTrace) { 55 | if (!enableLog) { 56 | return; 57 | } 58 | onPrintLog?.call(Level.error, message); 59 | if (onPrintLog == null) { 60 | logger.e( 61 | "${DateTime.now().toString()}\n$message", 62 | stackTrace: stackTrace, 63 | ); 64 | } 65 | } 66 | 67 | static void error(dynamic e) { 68 | if (!enableLog) { 69 | return; 70 | } 71 | onPrintLog?.call(Level.error, e.toString()); 72 | if (onPrintLog == null) { 73 | logger.e( 74 | "${DateTime.now().toString()}\n${e.toString()}", 75 | error: e, 76 | stackTrace: (e is Error) ? e.stackTrace : StackTrace.current, 77 | ); 78 | } 79 | } 80 | 81 | static void w(String message) { 82 | if (!enableLog) { 83 | return; 84 | } 85 | onPrintLog?.call(Level.warning, message); 86 | if (onPrintLog == null) { 87 | logger.w("${DateTime.now().toString()}\n$message"); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/simple_live_core/lib/src/model/tars/get_cdn_token_resp.dart: -------------------------------------------------------------------------------- 1 | import 'package:tars_flutter/tars/codec/tars_displayer.dart'; 2 | import 'package:tars_flutter/tars/codec/tars_input_stream.dart'; 3 | import 'package:tars_flutter/tars/codec/tars_output_stream.dart'; 4 | import 'package:tars_flutter/tars/codec/tars_struct.dart'; 5 | 6 | class GetCdnTokenResp extends TarsStruct { 7 | String url = ""; 8 | 9 | String cdnType = ""; 10 | 11 | String streamName = ""; 12 | 13 | int presenterUid = 0; 14 | 15 | String antiCode = ""; 16 | 17 | String sTime = ""; 18 | 19 | String flvAntiCode = ""; 20 | 21 | String hlsAntiCode = ""; 22 | 23 | @override 24 | void readFrom(TarsInputStream inputStream) { 25 | url = inputStream.read(url, 0, false); 26 | cdnType = inputStream.read(cdnType, 1, false); 27 | streamName = inputStream.read(streamName, 2, false); 28 | presenterUid = inputStream.read(presenterUid, 3, false); 29 | antiCode = inputStream.read(antiCode, 4, false); 30 | sTime = inputStream.read(sTime, 5, false); 31 | flvAntiCode = inputStream.read(flvAntiCode, 6, false); 32 | hlsAntiCode = inputStream.read(hlsAntiCode, 7, false); 33 | } 34 | 35 | @override 36 | void writeTo(TarsOutputStream outputStream) { 37 | outputStream 38 | ..write(url, 0) 39 | ..write(cdnType, 1) 40 | ..write(streamName, 2) 41 | ..write(presenterUid, 3) 42 | ..write(antiCode, 4) 43 | ..write(sTime, 5) 44 | ..write(flvAntiCode, 6) 45 | ..write(hlsAntiCode, 7); 46 | } 47 | 48 | @override 49 | Object deepCopy() { 50 | return GetCdnTokenResp() 51 | ..url = url 52 | ..cdnType = cdnType 53 | ..streamName = streamName 54 | ..presenterUid = presenterUid 55 | ..antiCode = antiCode 56 | ..sTime = sTime 57 | ..flvAntiCode = flvAntiCode 58 | ..hlsAntiCode = hlsAntiCode; 59 | } 60 | 61 | @override 62 | void displayAsString(StringBuffer sb, int level) { 63 | TarsDisplayer(sb, level: level) 64 | ..DisplayString(url, "url") 65 | ..DisplayString(cdnType, "cdnType") 66 | ..DisplayString(streamName, "streamName") 67 | ..DisplayInt(presenterUid, "presenterUid") 68 | ..DisplayString(antiCode, "antiCode") 69 | ..DisplayString(sTime, "sTime") 70 | ..DisplayString(flvAntiCode, "flvAntiCode") 71 | ..DisplayString(hlsAntiCode, "hlsAntiCode"); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | void fl_register_plugins(FlPluginRegistry* registry) { 19 | g_autoptr(FlPluginRegistrar) dynamic_color_registrar = 20 | fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); 21 | dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); 22 | g_autoptr(FlPluginRegistrar) fvp_registrar = 23 | fl_plugin_registry_get_registrar_for_plugin(registry, "FvpPlugin"); 24 | fvp_plugin_register_with_registrar(fvp_registrar); 25 | g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = 26 | fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); 27 | media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); 28 | g_autoptr(FlPluginRegistrar) media_kit_video_registrar = 29 | fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); 30 | media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); 31 | g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = 32 | fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); 33 | screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); 34 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 35 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 36 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 37 | g_autoptr(FlPluginRegistrar) volume_controller_registrar = 38 | fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin"); 39 | volume_controller_plugin_register_with_registrar(volume_controller_registrar); 40 | g_autoptr(FlPluginRegistrar) window_manager_registrar = 41 | fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); 42 | window_manager_plugin_register_with_registrar(window_manager_registrar); 43 | } 44 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.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 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/modules/search/douyin/douyin_search_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 4 | import 'package:get/get.dart'; 5 | import 'package:simple_live_app/app/controller/base_controller.dart'; 6 | import 'package:simple_live_app/app/sites.dart'; 7 | import 'package:simple_live_app/routes/app_navigation.dart'; 8 | import 'package:simple_live_app/routes/route_path.dart'; 9 | import 'package:simple_live_core/simple_live_core.dart'; 10 | import 'package:url_launcher/url_launcher_string.dart'; 11 | 12 | class DouyinSearchController extends BaseController { 13 | InAppWebViewController? webViewController; 14 | 15 | void onWebViewCreated(InAppWebViewController controller) { 16 | webViewController = controller; 17 | } 18 | 19 | RxList list = [].obs; 20 | 21 | String keyword = ""; 22 | 23 | /// 搜索模式,0=直播间,1=主播 24 | var searchMode = 0.obs; 25 | final Site site; 26 | DouyinSearchController( 27 | this.site, 28 | ); 29 | 30 | var searchUrl = "https://www.douyin.com/search/dnf?type=live"; 31 | 32 | void reloadWebView() { 33 | if (keyword.isEmpty) { 34 | return; 35 | } 36 | searchUrl = 37 | "https://www.douyin.com/search/${Uri.encodeComponent(keyword)}?type=live"; 38 | if (Platform.isAndroid || Platform.isIOS) { 39 | webViewController!.loadUrl( 40 | urlRequest: URLRequest( 41 | url: WebUri(searchUrl), 42 | ), 43 | ); 44 | } 45 | } 46 | 47 | Future onLoadStop(InAppWebViewController controller, Uri? uri) async { 48 | pageLoading.value = false; 49 | } 50 | 51 | Future onLoadStart(InAppWebViewController controller, Uri? uri) async { 52 | pageLoading.value = true; 53 | } 54 | 55 | Future onCreateWindow( 56 | InAppWebViewController controller, 57 | CreateWindowAction createWindowAction, 58 | ) async { 59 | if (createWindowAction.request.url?.host == "live.douyin.com") { 60 | { 61 | var regExp = RegExp(r"live\.douyin\.com/([\d|\w]+)"); 62 | var id = 63 | regExp 64 | .firstMatch(createWindowAction.request.url.toString()) 65 | ?.group(1) ?? 66 | ""; 67 | 68 | AppNavigator.toLiveRoomDetail(site: site, roomId: id); 69 | return false; 70 | } 71 | } 72 | 73 | return false; 74 | } 75 | 76 | void openBrowser() { 77 | launchUrlString(searchUrl); 78 | Get.offAndToNamed(RoutePath.kTools); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/services/history_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:get/get.dart'; 4 | import 'package:simple_live_app/app/constant.dart'; 5 | import 'package:simple_live_app/app/event_bus.dart'; 6 | import 'package:simple_live_app/app/log.dart'; 7 | import 'package:simple_live_app/app/utils/duration_2str.dart'; 8 | import 'package:simple_live_app/models/db/history.dart'; 9 | 10 | import 'package:simple_live_app/services/db_service.dart'; 11 | 12 | class HistoryService extends GetxService { 13 | static HistoryService get instance => Get.find(); 14 | final Stopwatch _stopwatch = Stopwatch(); 15 | var _elapsed = Duration.zero; 16 | Duration _oldWatchedDuration = Duration.zero; 17 | History? curLiveRoomHistory; 18 | //两分钟自动保存一次,防止用户直接关闭app,丢失数据 19 | final _saveInterval = const Duration(minutes: 2); 20 | Timer? _timer; // 定时器 21 | 22 | // 开始计时 23 | void start(History history) { 24 | _loadHistory(history); 25 | _stopwatch.start(); 26 | _timer = Timer.periodic(_saveInterval, (timer) { 27 | _updateHistory(); 28 | }); 29 | } 30 | 31 | // reset 32 | void reset(String roomId) { 33 | _updateHistory(); 34 | _stopwatch.reset(); 35 | History? history = DBService.instance.getHistory(roomId); 36 | _loadHistory(history!); 37 | } 38 | 39 | // 停止计时 40 | void stop() { 41 | _stopwatch.stop(); 42 | _updateHistory(); 43 | _stopwatch.reset(); 44 | _elapsed = Duration.zero; 45 | // 取消定时器 46 | _timer?.cancel(); 47 | _timer = null; 48 | curLiveRoomHistory = null; 49 | Log.i("本次观看时长:$_elapsed"); 50 | } 51 | 52 | void _loadHistory(History history) { 53 | curLiveRoomHistory = DBService.instance.getHistory(history.id); 54 | // 首次观看则创建 55 | if (curLiveRoomHistory == null) { 56 | curLiveRoomHistory = history; 57 | DBService.instance.addOrUpdateHistory(history); 58 | } 59 | _oldWatchedDuration = curLiveRoomHistory!.duration; 60 | } 61 | 62 | // updateHistory 63 | void _updateHistory() { 64 | if (curLiveRoomHistory == null) { 65 | return; 66 | } 67 | // 累加到当前历史记录 68 | _elapsed = _stopwatch.elapsed; 69 | Duration curTime = _oldWatchedDuration + _elapsed; 70 | Log.i( 71 | "已观看时间:${_oldWatchedDuration.toHMSString()}_增加时间:${_elapsed.toHMSString()}", 72 | ); 73 | curLiveRoomHistory?.watchDuration = curTime.toHMSString(); 74 | curLiveRoomHistory?.updateTime = DateTime.now(); 75 | DBService.instance.addOrUpdateHistory(curLiveRoomHistory!); 76 | EventBus.instance.emit(Constant.kUpdateFollow, curLiveRoomHistory); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | allprojects { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | } 9 | 10 | val newBuildDir: Directory = 11 | rootProject.layout.buildDirectory 12 | .dir("../../build") 13 | .get() 14 | rootProject.layout.buildDirectory.value(newBuildDir) 15 | 16 | subprojects { 17 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) 18 | project.layout.buildDirectory.value(newSubprojectBuildDir) 19 | } 20 | 21 | subprojects { 22 | afterEvaluate { 23 | if (project.extensions.findByName("android") != null) { 24 | val androidExtension = 25 | project.extensions.getByName("android") as com.android.build.gradle.BaseExtension 26 | 27 | if (androidExtension.namespace == null) { 28 | androidExtension.namespace = project.group.toString() 29 | } 30 | 31 | androidExtension.compileOptions { 32 | sourceCompatibility = JavaVersion.VERSION_17 33 | targetCompatibility = JavaVersion.VERSION_17 34 | } 35 | 36 | project.tasks.withType().configureEach { 37 | compilerOptions { 38 | jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) 39 | } 40 | } 41 | 42 | val pluginCompileSdkStr = androidExtension.compileSdkVersion 43 | val pluginCompileSdk = pluginCompileSdkStr 44 | ?.removePrefix("android-") 45 | ?.toIntOrNull() 46 | if (pluginCompileSdk != null && pluginCompileSdk < 31) { 47 | project.logger.error( 48 | "Warning: Overriding compileSdk version in Flutter plugin: ${project.name} " + 49 | "from $pluginCompileSdk to 31 (to work around https://issuetracker.google.com/issues/199180389).\n" + 50 | "If there is not a new version of ${project.name}, consider filing an issue against ${project.name} " + 51 | "to increase their compileSdk to the latest (otherwise try updating to the latest version)." 52 | ) 53 | androidExtension.setCompileSdkVersion(31) 54 | } 55 | } 56 | 57 | project.buildDir = File(rootProject.buildDir, project.name) 58 | } 59 | } 60 | 61 | subprojects { 62 | project.evaluationDependsOn(":app") 63 | } 64 | 65 | tasks.register("clean") { 66 | delete(rootProject.layout.buildDirectory) 67 | } 68 | -------------------------------------------------------------------------------- /lib/modules/indexed/indexed_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | import 'package:get/get.dart'; 4 | import 'package:simple_live_app/app/constant.dart'; 5 | import 'package:simple_live_app/app/controller/app_settings_controller.dart'; 6 | import 'package:simple_live_app/app/event_bus.dart'; 7 | import 'package:simple_live_app/app/utils.dart'; 8 | import 'package:simple_live_app/modules/category/category_controller.dart'; 9 | import 'package:simple_live_app/modules/category/category_page.dart'; 10 | import 'package:simple_live_app/modules/home/home_controller.dart'; 11 | import 'package:simple_live_app/modules/home/home_page.dart'; 12 | import 'package:simple_live_app/modules/follow_user/follow_user_controller.dart'; 13 | import 'package:simple_live_app/modules/follow_user/follow_user_page.dart'; 14 | import 'package:simple_live_app/modules/mine/mine_page.dart'; 15 | 16 | class IndexedController extends GetxController { 17 | RxList items = RxList([]); 18 | 19 | var index = 0.obs; 20 | RxList pages = RxList([ 21 | const SizedBox(), 22 | const SizedBox(), 23 | const SizedBox(), 24 | const SizedBox(), 25 | ]); 26 | 27 | void setIndex(int i) { 28 | if (pages[i] is SizedBox) { 29 | switch (items[i].index) { 30 | case 0: 31 | Get.put(HomeController()); 32 | pages[i] = const HomePage(); 33 | break; 34 | case 1: 35 | Get.put(FollowUserController()); 36 | pages[i] = const FollowUserPage(); 37 | break; 38 | case 2: 39 | Get.put(CategoryController()); 40 | pages[i] = const CategoryPage(); 41 | break; 42 | case 3: 43 | pages[i] = const MinePage(); 44 | break; 45 | default: 46 | } 47 | } else { 48 | if (index.value == i) { 49 | EventBus.instance.emit( 50 | EventBus.kBottomNavigationBarClicked, 51 | items[i].index, 52 | ); 53 | } 54 | } 55 | 56 | index.value = i; 57 | } 58 | 59 | @override 60 | void onInit() { 61 | Future.delayed(Duration.zero, showFirstRun); 62 | items.value = AppSettingsController.instance.homeSort 63 | .map((key) => Constant.allHomePages[key]!) 64 | .toList(); 65 | setIndex(0); 66 | super.onInit(); 67 | } 68 | 69 | Future showFirstRun() async { 70 | var settingsController = Get.find(); 71 | if (settingsController.firstRun) { 72 | settingsController.setNoFirstRun(); 73 | await Utils.showStatement(); 74 | Utils.checkUpdate(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/modules/sync/local_sync/scan_qr/sync_scan_qr_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:mobile_scanner/mobile_scanner.dart'; 5 | import 'package:simple_live_app/app/controller/base_controller.dart'; 6 | import 'package:simple_live_app/app/log.dart'; 7 | import 'package:simple_live_app/app/utils.dart'; 8 | import 'package:simple_live_app/routes/route_path.dart'; 9 | 10 | class SyncScanQRController extends BaseController { 11 | final MobileScannerController scannerController = MobileScannerController(); 12 | bool pause = false; 13 | Future onBarcodeDetected(BarcodeCapture capture) async { 14 | if (pause || capture.barcodes.isEmpty) { 15 | return; 16 | } 17 | 18 | final barcode = capture.barcodes.first; 19 | final code = barcode.rawValue ?? ''; 20 | Log.d('Scan result: $code'); 21 | 22 | pause = true; 23 | // 扫码成功后暂停摄像头 24 | await scannerController.stop(); 25 | // 处理扫码结果 26 | if (code.isEmpty) { 27 | pause = false; 28 | await _resumeScanning(); 29 | return; 30 | } 31 | 32 | if (code.length == 5) { 33 | Get.offAndToNamed(RoutePath.kRemoteSyncRoom, arguments: code); 34 | return; 35 | } 36 | 37 | final addressList = code.split(';'); 38 | if (addressList.length >= 2) { 39 | //弹窗选择 40 | await showPickerAddress(addressList); 41 | } else { 42 | Get.back(result: code); 43 | } 44 | } 45 | 46 | void toggleTorch() { 47 | scannerController.toggleTorch(); 48 | } 49 | 50 | void switchCamera() { 51 | scannerController.switchCamera(); 52 | } 53 | 54 | Future _resumeScanning() async { 55 | pause = false; 56 | await scannerController.start(); 57 | } 58 | 59 | Future showPickerAddress(List addressList) async { 60 | SmartDialog.showToast("扫描到多个地址,请选择一个连接"); 61 | var address = await Utils.showBottomSheet( 62 | title: '请选择地址', 63 | child: ListView.builder( 64 | itemBuilder: (_, i) { 65 | return ListTile( 66 | title: Text(addressList[i]), 67 | onTap: () { 68 | Get.back(result: addressList[i]); 69 | }, 70 | ); 71 | }, 72 | itemCount: addressList.length, 73 | ), 74 | ); 75 | if (address != null && address.isNotEmpty) { 76 | Get.back(result: address); 77 | } else { 78 | await _resumeScanning(); 79 | } 80 | } 81 | 82 | @override 83 | void onClose() { 84 | scannerController.dispose(); 85 | 86 | super.onClose(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/widgets/settings/settings_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:simple_live_app/app/app_style.dart'; 4 | 5 | class SettingsMenu extends StatelessWidget { 6 | final String title; 7 | final String? subtitle; 8 | final Map valueMap; 9 | final T value; 10 | 11 | final Function(T)? onChanged; 12 | const SettingsMenu({ 13 | required this.title, 14 | required this.value, 15 | required this.valueMap, 16 | this.subtitle, 17 | this.onChanged, 18 | super.key, 19 | }); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return ListTile( 24 | visualDensity: VisualDensity.compact, 25 | title: Text(title, style: Theme.of(context).textTheme.bodyLarge), 26 | shape: RoundedRectangleBorder(borderRadius: AppStyle.radius8), 27 | contentPadding: AppStyle.edgeInsetsL16.copyWith(right: 8), 28 | subtitle: subtitle == null 29 | ? null 30 | : Text( 31 | subtitle!, 32 | style: Get.textTheme.bodySmall!.copyWith(color: Colors.grey), 33 | ), 34 | trailing: Row( 35 | mainAxisSize: MainAxisSize.min, 36 | children: [ 37 | Text( 38 | valueMap[value]!.tr, 39 | style: Theme.of( 40 | context, 41 | ).textTheme.bodyMedium!.copyWith(color: Colors.grey), 42 | ), 43 | AppStyle.hGap4, 44 | const Icon(Icons.chevron_right, color: Colors.grey), 45 | ], 46 | ), 47 | onTap: () => openMenu(context), 48 | ); 49 | } 50 | 51 | void openMenu(BuildContext context) { 52 | showModalBottomSheet( 53 | context: context, 54 | showDragHandle: true, 55 | useSafeArea: true, //useSafeArea似乎无效 56 | builder: (_) => SafeArea( 57 | top: false, 58 | child: SingleChildScrollView( 59 | child: RadioGroup( 60 | groupValue: value, 61 | onChanged: (e) { 62 | Get.back(); 63 | onChanged?.call(e as T); 64 | }, 65 | child: Column( 66 | mainAxisSize: MainAxisSize.min, 67 | children: valueMap.keys 68 | .map( 69 | (e) => RadioListTile( 70 | value: e, 71 | title: Text( 72 | (valueMap[e]?.tr) ?? "???", 73 | style: Get.textTheme.bodyMedium, 74 | ), 75 | ), 76 | ) 77 | .toList(), 78 | ), 79 | ), 80 | ), 81 | ), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/services/douyin_account_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 4 | import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; 5 | import 'package:get/get.dart'; 6 | import 'package:simple_live_app/app/constant.dart'; 7 | import 'package:simple_live_app/app/sites.dart'; 8 | import 'package:simple_live_app/models/account/douyin_user_info.dart'; 9 | import 'package:simple_live_app/services/local_storage_service.dart'; 10 | import 'package:simple_live_app/requests/http_client.dart'; 11 | import 'package:simple_live_core/simple_live_core.dart'; 12 | 13 | class DouyinAccountService extends GetxService { 14 | static DouyinAccountService get instance => Get.find(); 15 | 16 | var logged = false.obs; 17 | var cookie = ""; 18 | var name = "未登录".obs; 19 | 20 | @override 21 | void onInit() { 22 | cookie = LocalStorageService.instance.getValue( 23 | LocalStorageService.kDouyinCookie, 24 | "", 25 | ); 26 | logged.value = cookie.isNotEmpty; 27 | loadUserInfo(); 28 | super.onInit(); 29 | } 30 | 31 | Future loadUserInfo() async { 32 | if (cookie.isEmpty) { 33 | return; 34 | } 35 | try { 36 | final result = await HttpClient.instance.getJson( 37 | "https://live.douyin.com/webcast/user/me/", 38 | queryParameters: { 39 | "aid": "6383", 40 | }, 41 | header: { 42 | "Cookie": cookie, 43 | }, 44 | ); 45 | 46 | if (result["status_code"] == 0) { 47 | var info = DouyinUserInfoModel.fromJson(result["data"]); 48 | name.value = info.nickname ?? "未登录"; 49 | setSite(); 50 | } else { 51 | SmartDialog.showToast("抖音登录已失效,请重新登录"); 52 | logout(); 53 | } 54 | } catch (e) { 55 | SmartDialog.showToast("获取抖音登录用户信息失败,可前往账号管理重试"); 56 | } 57 | } 58 | 59 | void setSite() { 60 | (Sites.allSites[Constant.kDouyin]!.liveSite as DouyinSite).cookie = cookie; 61 | } 62 | 63 | void setCookie(String cookie) { 64 | this.cookie = cookie; 65 | LocalStorageService.instance.setValue( 66 | LocalStorageService.kDouyinCookie, 67 | cookie, 68 | ); 69 | logged.value = cookie.isNotEmpty; 70 | } 71 | 72 | Future logout() async { 73 | cookie = ""; 74 | name.value = "未登录"; 75 | setSite(); 76 | LocalStorageService.instance.setValue( 77 | LocalStorageService.kDouyinCookie, 78 | "", 79 | ); 80 | logged.value = false; 81 | 82 | if (Platform.isAndroid || Platform.isIOS) { 83 | CookieManager cookieManager = CookieManager.instance(); 84 | await cookieManager.deleteAllCookies(); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import connectivity_plus 9 | import device_info_plus 10 | import dynamic_color 11 | import file_picker 12 | import flutter_inappwebview_macos 13 | import fvp 14 | import media_kit_libs_macos_video 15 | import media_kit_video 16 | import mobile_scanner 17 | import network_info_plus 18 | import package_info_plus 19 | import path_provider_foundation 20 | import screen_brightness_macos 21 | import screen_retriever_macos 22 | import share_plus 23 | import url_launcher_macos 24 | import video_player_avfoundation 25 | import volume_controller 26 | import wakelock_plus 27 | import window_manager 28 | 29 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 30 | ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) 31 | DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) 32 | DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) 33 | FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) 34 | InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) 35 | FvpPlugin.register(with: registry.registrar(forPlugin: "FvpPlugin")) 36 | MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) 37 | MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) 38 | MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) 39 | NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) 40 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 41 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 42 | ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin")) 43 | ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) 44 | SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) 45 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 46 | FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) 47 | VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin")) 48 | WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) 49 | WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) 50 | } 51 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | void RegisterPlugins(flutter::PluginRegistry* registry) { 24 | ConnectivityPlusWindowsPluginRegisterWithRegistrar( 25 | registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); 26 | DynamicColorPluginCApiRegisterWithRegistrar( 27 | registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); 28 | FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( 29 | registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); 30 | FvpPluginCApiRegisterWithRegistrar( 31 | registry->GetRegistrarForPlugin("FvpPluginCApi")); 32 | MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( 33 | registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); 34 | MediaKitVideoPluginCApiRegisterWithRegistrar( 35 | registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); 36 | PermissionHandlerWindowsPluginRegisterWithRegistrar( 37 | registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); 38 | ScreenBrightnessWindowsPluginRegisterWithRegistrar( 39 | registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); 40 | ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( 41 | registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); 42 | SharePlusWindowsPluginCApiRegisterWithRegistrar( 43 | registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); 44 | UrlLauncherWindowsRegisterWithRegistrar( 45 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 46 | VolumeControllerPluginCApiRegisterWithRegistrar( 47 | registry->GetRegistrarForPlugin("VolumeControllerPluginCApi")); 48 | WindowManagerPluginRegisterWithRegistrar( 49 | registry->GetRegistrarForPlugin("WindowManagerPlugin")); 50 | } 51 | -------------------------------------------------------------------------------- /lib/services/bilibili_account_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 4 | import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; 5 | import 'package:get/get.dart'; 6 | import 'package:simple_live_app/app/constant.dart'; 7 | import 'package:simple_live_app/app/sites.dart'; 8 | import 'package:simple_live_app/models/account/bilibili_user_info_page.dart'; 9 | import 'package:simple_live_app/requests/http_client.dart'; 10 | import 'package:simple_live_app/services/local_storage_service.dart'; 11 | import 'package:simple_live_core/simple_live_core.dart'; 12 | 13 | class BiliBiliAccountService extends GetxService { 14 | static BiliBiliAccountService get instance => 15 | Get.find(); 16 | 17 | var logged = false.obs; 18 | 19 | var cookie = ""; 20 | var uid = 0; 21 | var name = "未登录".obs; 22 | 23 | @override 24 | void onInit() { 25 | cookie = LocalStorageService.instance.getValue( 26 | LocalStorageService.kBilibiliCookie, 27 | "", 28 | ); 29 | logged.value = cookie.isNotEmpty; 30 | loadUserInfo(); 31 | super.onInit(); 32 | } 33 | 34 | Future loadUserInfo() async { 35 | if (cookie.isEmpty) { 36 | return; 37 | } 38 | try { 39 | var result = await HttpClient.instance.getJson( 40 | "https://api.bilibili.com/x/member/web/account", 41 | header: { 42 | "Cookie": cookie, 43 | }, 44 | ); 45 | if (result["code"] == 0) { 46 | var info = BiliBiliUserInfoModel.fromJson(result["data"]); 47 | name.value = info.uname ?? "未登录"; 48 | uid = info.mid ?? 0; 49 | setSite(); 50 | } else { 51 | SmartDialog.showToast("哔哩哔哩登录已失效,请重新登录"); 52 | logout(); 53 | } 54 | } catch (e) { 55 | SmartDialog.showToast("获取哔哩哔哩用户信息失败,可前往账号管理重试"); 56 | } 57 | } 58 | 59 | void setSite() { 60 | (Sites.allSites[Constant.kBiliBili]!.liveSite as BiliBiliSite) 61 | ..userId = uid 62 | ..cookie = cookie; 63 | } 64 | 65 | void setCookie(String cookie) { 66 | this.cookie = cookie; 67 | LocalStorageService.instance.setValue( 68 | LocalStorageService.kBilibiliCookie, 69 | cookie, 70 | ); 71 | logged.value = cookie.isNotEmpty; 72 | } 73 | 74 | Future logout() async { 75 | cookie = ""; 76 | uid = 0; 77 | name.value = "未登录"; 78 | setSite(); 79 | LocalStorageService.instance.setValue( 80 | LocalStorageService.kBilibiliCookie, 81 | "", 82 | ); 83 | logged.value = false; 84 | 85 | if (Platform.isAndroid || Platform.isIOS) { 86 | CookieManager cookieManager = CookieManager.instance(); 87 | await cookieManager.deleteAllCookies(); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/widgets/none_border_circular_textfield.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NoneBorderCircularTextField extends StatelessWidget { 4 | final TextEditingController editingController; 5 | final String? hintText; 6 | final String? helperText; 7 | final String? labelText; 8 | final String? errorText; 9 | final Widget? prefixIcon; 10 | final bool obscureText; 11 | final VoidCallback? onEditingComplete; 12 | final ValueChanged? onChanged; 13 | final VoidCallback? onTap; 14 | final TextAlign textAlign; 15 | final Widget? trailing; 16 | final TextInputType? inputType; 17 | final int? maxLines; 18 | final bool autoFocus; 19 | final FocusNode? focusNode; 20 | final bool? enable; 21 | final bool readOnly; 22 | 23 | final bool needPadding; 24 | 25 | const NoneBorderCircularTextField({ 26 | super.key, 27 | required this.editingController, 28 | this.hintText, 29 | this.helperText, 30 | this.labelText, 31 | this.errorText, 32 | this.prefixIcon, 33 | this.textAlign = TextAlign.start, 34 | this.obscureText = false, 35 | this.maxLines = 1, 36 | this.onEditingComplete, 37 | this.trailing, 38 | this.autoFocus = false, 39 | this.focusNode, 40 | this.inputType, 41 | this.onChanged, 42 | this.onTap, 43 | this.enable, 44 | this.readOnly = false, 45 | this.needPadding = true, 46 | }); 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | TextField common = TextField( 51 | enabled: enable, 52 | readOnly: readOnly, 53 | decoration: InputDecoration( 54 | prefixIcon: prefixIcon, 55 | hintText: hintText, 56 | filled: true, 57 | contentPadding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), 58 | suffix: trailing, 59 | helperText: helperText, 60 | helperMaxLines: 3, 61 | border: const OutlineInputBorder( 62 | borderSide: BorderSide.none, 63 | borderRadius: BorderRadius.all(Radius.circular(8.0)), 64 | ), 65 | labelText: labelText, 66 | errorText: errorText, 67 | errorMaxLines: 3, 68 | ), 69 | textAlign: textAlign, 70 | autofocus: autoFocus, 71 | keyboardType: inputType, 72 | maxLines: maxLines, 73 | controller: editingController, 74 | obscureText: obscureText, 75 | onEditingComplete: onEditingComplete, 76 | onChanged: onChanged, 77 | onTap: onTap, 78 | focusNode: focusNode, 79 | ); 80 | if (needPadding) { 81 | return Padding( 82 | padding: const EdgeInsets.only( 83 | top: 10, 84 | bottom: 10, 85 | ), 86 | child: common, 87 | ); 88 | } else { 89 | return common; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.android.build.gradle.internal.api.ApkVariantOutputImpl 2 | import org.jetbrains.kotlin.konan.properties.Properties 3 | 4 | plugins { 5 | id("com.android.application") 6 | id("kotlin-android") 7 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 8 | id("dev.flutter.flutter-gradle-plugin") 9 | } 10 | 11 | android { 12 | namespace = "com.xycz.simple_live" 13 | compileSdk = flutter.compileSdkVersion 14 | ndkVersion = "28.0.13004108" 15 | 16 | compileOptions { 17 | sourceCompatibility = JavaVersion.VERSION_17 18 | targetCompatibility = JavaVersion.VERSION_17 19 | } 20 | 21 | kotlinOptions { 22 | jvmTarget = JavaVersion.VERSION_17.toString() 23 | } 24 | 25 | defaultConfig { 26 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 27 | applicationId = "com.xycz.simple_live" 28 | // You can update the following values to match your application needs. 29 | // For more information, see: https://flutter.dev/to/review-gradle-config. 30 | minSdk = flutter.minSdkVersion 31 | targetSdk = flutter.targetSdkVersion 32 | versionCode = flutter.versionCode 33 | versionName = flutter.versionName 34 | } 35 | 36 | packagingOptions.jniLibs.useLegacyPackaging = true 37 | 38 | val keyProperties = Properties().also { 39 | val properties = rootProject.file("key.properties") 40 | if (properties.exists()) 41 | it.load(properties.inputStream()) 42 | } 43 | 44 | val config = keyProperties.getProperty("storeFile")?.let { 45 | signingConfigs.create("release") { 46 | storeFile = file(it) 47 | storePassword = keyProperties.getProperty("storePassword") 48 | keyAlias = keyProperties.getProperty("keyAlias") 49 | keyPassword = keyProperties.getProperty("keyPassword") 50 | enableV1Signing = true 51 | enableV2Signing = true 52 | } 53 | } 54 | 55 | buildTypes { 56 | all { 57 | signingConfig = config ?: signingConfigs["debug"] 58 | } 59 | release { 60 | proguardFiles( 61 | getDefaultProguardFile("proguard-android-optimize.txt"), 62 | "proguard-rules.pro" 63 | ) 64 | } 65 | debug { 66 | applicationIdSuffix = ".debug" 67 | } 68 | } 69 | 70 | applicationVariants.all { 71 | val variant = this 72 | variant.outputs.forEach { output -> 73 | (output as ApkVariantOutputImpl).versionCodeOverride = flutter.versionCode 74 | } 75 | } 76 | } 77 | 78 | flutter { 79 | source = "../.." 80 | } 81 | -------------------------------------------------------------------------------- /lib/modules/search/search_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'package:get/get.dart'; 6 | import 'package:simple_live_app/app/sites.dart'; 7 | import 'package:simple_live_app/modules/search/search_list_controller.dart'; 8 | 9 | class AppSearchController extends GetxController 10 | with GetSingleTickerProviderStateMixin { 11 | late TabController tabController; 12 | int index = 0; 13 | 14 | var searchMode = 0.obs; 15 | 16 | AppSearchController() { 17 | tabController = TabController( 18 | length: Sites.supportSites.length, 19 | vsync: this, 20 | ); 21 | tabController.animation?.addListener(() { 22 | var currentIndex = (tabController.animation?.value ?? 0).round(); 23 | if (index == currentIndex) { 24 | return; 25 | } 26 | 27 | index = currentIndex; 28 | // if (Sites.supportSites[index].id == Constant.kDouyin) { 29 | // return; 30 | // } 31 | 32 | var controller = Get.find( 33 | tag: Sites.supportSites[index].id, 34 | ); 35 | 36 | if (controller.list.isEmpty && 37 | !controller.pageEmpty.value && 38 | controller.keyword.isNotEmpty) { 39 | controller.refreshData(); 40 | } 41 | }); 42 | } 43 | 44 | StreamSubscription? streamSubscription; 45 | 46 | TextEditingController searchController = TextEditingController(); 47 | 48 | @override 49 | void onInit() { 50 | for (var site in Sites.supportSites) { 51 | // if (site.id == Constant.kDouyin) { 52 | // Get.put(DouyinSearchController(site)); 53 | // } else { 54 | Get.put( 55 | SearchListController(site), 56 | tag: site.id, 57 | ); 58 | //} 59 | } 60 | 61 | super.onInit(); 62 | } 63 | 64 | void doSearch() { 65 | if (searchController.text.isEmpty) { 66 | return; 67 | } 68 | for (var site in Sites.supportSites) { 69 | // if (site.id == Constant.kDouyin) { 70 | // var controller = Get.find(); 71 | // controller.keyword = searchController.text; 72 | // controller.searchMode.value = searchMode.value; 73 | // controller.reloadWebView(); 74 | // } else { 75 | var controller = Get.find(tag: site.id) 76 | ..clear() 77 | ..keyword = searchController.text; 78 | controller.searchMode.value = searchMode.value; 79 | //} 80 | } 81 | // if (Sites.supportSites[index].id != Constant.kDouyin) { 82 | Get.find( 83 | tag: Sites.supportSites[index].id, 84 | ).refreshData(); 85 | //} 86 | } 87 | 88 | @override 89 | void onClose() { 90 | streamSubscription?.cancel(); 91 | super.onClose(); 92 | } 93 | } 94 | --------------------------------------------------------------------------------