├── .github └── workflows │ ├── dart.yml │ └── release.yml ├── .gitignore ├── .metadata ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── mystyle │ │ │ │ └── pure_live │ │ │ │ ├── BetterPlayerService.kt │ │ │ │ └── MainActivity.kt │ │ ├── libs │ │ │ └── Msc.jar │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-mdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxhdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable │ │ │ ├── banner.png │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-en │ │ │ └── strings.xml │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ │ └── xml │ │ │ ├── filepaths.xml │ │ │ ├── network_security_config.xml │ │ │ └── share_targets.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── PingFangSC.ttf ├── crypto-js.js ├── icons │ ├── CustomIcons.ttf │ ├── icon.png │ └── icon_foreground.png ├── images │ ├── bilibili.png │ ├── bilibili_2.png │ ├── douyin.png │ ├── douyu.png │ ├── egame.png │ ├── huya.png │ ├── kuaishou.png │ ├── logo.png │ └── wechat.png ├── index.html └── version.json ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h └── RunnerTests │ └── RunnerTests.swift ├── iptvgenerate.py ├── lib ├── common │ ├── base │ │ └── base_controller.dart │ ├── index.dart │ ├── l10n │ │ ├── generated │ │ │ ├── intl │ │ │ │ ├── messages_all.dart │ │ │ │ ├── messages_en.dart │ │ │ │ └── messages_zh_CN.dart │ │ │ └── l10n.dart │ │ ├── intl_en.arb │ │ └── intl_zh_CN.arb │ ├── models │ │ ├── bilibili_user_info_page.dart │ │ ├── index.dart │ │ ├── live_area.dart │ │ ├── live_message.dart │ │ └── live_room.dart │ ├── services │ │ ├── bilibili_account_service.dart │ │ ├── index.dart │ │ └── settings_service.dart │ ├── style │ │ ├── index.dart │ │ └── theme.dart │ ├── utils │ │ ├── cache_manager.dart │ │ ├── index.dart │ │ ├── js_engine.dart │ │ ├── pref_util.dart │ │ ├── snackbar_util.dart │ │ ├── string_to_boolean.dart │ │ ├── supabase_policy.dart │ │ ├── text_util.dart │ │ └── version_util.dart │ └── widgets │ │ ├── custom_icons.dart │ │ ├── empty_view.dart │ │ ├── index.dart │ │ ├── keep_alive_wrapper.dart │ │ ├── menu_button.dart │ │ ├── room_card.dart │ │ ├── search_button.dart │ │ └── section_listtile.dart ├── core │ ├── common │ │ ├── binary_writer.dart │ │ ├── convert_helper.dart │ │ ├── core_error.dart │ │ ├── core_log.dart │ │ ├── custom_interceptor.dart │ │ ├── fk_user_agent.dart │ │ ├── http_client.dart │ │ ├── iterum.dart │ │ └── web_socket_util.dart │ ├── danmaku │ │ ├── bilibili_danmaku.dart │ │ ├── douyin_danmaku.dart │ │ ├── douyu_danmaku.dart │ │ ├── empty_danmaku.dart │ │ ├── huya_danmaku.dart │ │ └── proto │ │ │ ├── douyin.pb.dart │ │ │ ├── douyin.pbenum.dart │ │ │ ├── douyin.pbjson.dart │ │ │ └── douyin.proto │ ├── index.dart │ ├── interface │ │ ├── live_danmaku.dart │ │ └── live_site.dart │ ├── iptv │ │ ├── iptv_utils.dart │ │ ├── m3u_parser_nullsafe.dart │ │ └── src │ │ │ ├── general_utils_object_extension.dart │ │ │ ├── m3u_header.dart │ │ │ ├── m3u_item.dart │ │ │ ├── m3u_list.dart │ │ │ ├── m3u_load_options.dart │ │ │ ├── text_utils.dart │ │ │ └── text_utils_string_extension.dart │ ├── site │ │ ├── bilibili_site.dart │ │ ├── cc_site.dart │ │ ├── douyin_site.dart │ │ ├── douyu_site.dart │ │ ├── huya_site.dart │ │ ├── iptv_site.dart │ │ └── kuaishou_site.dart │ └── sites.dart ├── main.dart ├── model │ ├── live_anchor_item.dart │ ├── live_category.dart │ ├── live_category_result.dart │ ├── live_message.dart │ ├── live_play_quality.dart │ └── live_search_result.dart ├── modules │ ├── about │ │ ├── about_page.dart │ │ ├── donate_page.dart │ │ ├── version_history.dart │ │ └── widgets │ │ │ └── version_dialog.dart │ ├── account │ │ ├── account_bing.dart │ │ ├── account_controller.dart │ │ ├── account_page.dart │ │ └── bilibili │ │ │ ├── bilibili_bings.dart │ │ │ ├── qr_login_controller.dart │ │ │ ├── qr_login_page.dart │ │ │ ├── web_login_controller.dart │ │ │ └── web_login_page.dart │ ├── area_rooms │ │ ├── area_rooms_binding.dart │ │ ├── area_rooms_controller.dart │ │ └── area_rooms_page.dart │ ├── areas │ │ ├── areas_controller.dart │ │ ├── areas_grid_view.dart │ │ ├── areas_list_controller.dart │ │ ├── areas_page.dart │ │ ├── favorite_areas_page.dart │ │ └── widgets │ │ │ └── area_card.dart │ ├── auth │ │ ├── auth_controller.dart │ │ ├── components │ │ │ ├── supa_email_auth.dart │ │ │ ├── supa_reset_password.dart │ │ │ └── update_password.dart │ │ ├── mine_page.dart │ │ ├── sign_in_page.dart │ │ ├── user_manage_page.dart │ │ └── utils │ │ │ ├── constants.dart │ │ │ └── supa_auth_action.dart │ ├── backup │ │ └── backup_page.dart │ ├── contact │ │ └── contact_page.dart │ ├── favorite │ │ ├── favorite_controller.dart │ │ └── favorite_page.dart │ ├── history │ │ └── history_page.dart │ ├── home │ │ ├── home_page.dart │ │ ├── mobile_view.dart │ │ └── tablet_view.dart │ ├── hot_areas │ │ ├── hot_areas_binding.dart │ │ ├── hot_areas_controller.dart │ │ └── hot_areas_page.dart │ ├── live_play │ │ ├── danmu_merge.dart │ │ ├── live_play_binding.dart │ │ ├── live_play_controller.dart │ │ ├── live_play_page.dart │ │ └── widgets │ │ │ ├── danmaku_list_view.dart │ │ │ ├── index.dart │ │ │ ├── live_dlna_dialog.dart │ │ │ └── video_player │ │ │ ├── danmaku_text.dart │ │ │ ├── player_full.dart │ │ │ ├── video_controller.dart │ │ │ ├── video_controller_panel.dart │ │ │ └── video_player.dart │ ├── popular │ │ ├── popular_controller.dart │ │ ├── popular_grid_controller.dart │ │ ├── popular_grid_view.dart │ │ └── popular_page.dart │ ├── search │ │ ├── search_binding.dart │ │ ├── search_controller.dart │ │ ├── search_list_controller.dart │ │ ├── search_list_view.dart │ │ └── search_page.dart │ ├── settings │ │ ├── danmuset.dart │ │ ├── settings_binding.dart │ │ └── settings_page.dart │ └── shield │ │ ├── danmu_shield_binding.dart │ │ ├── danmu_shield_controller.dart │ │ └── danmu_shield_page.dart ├── plugins │ ├── archethic.dart │ ├── barrage.dart │ ├── cache_network.dart │ ├── core_error.dart │ ├── file_recover_utils.dart │ ├── global.dart │ ├── local_http.dart │ ├── lzstring.dart │ ├── screen_device.dart │ ├── supabase.dart │ ├── utils.dart │ └── window_util.dart └── routes │ ├── app_navigation.dart │ ├── app_pages.dart │ └── route_path.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake ├── main.cc ├── my_application.cc └── my_application.h ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── app_icon_1024.png │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ └── app_icon_64.png │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements └── RunnerTests │ └── RunnerTests.swift ├── pubspec.lock ├── pubspec.yaml ├── pure_live_rename.py ├── run.MD ├── test └── widget_test.dart └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: app-build-action 2 | #推送Tag时触发 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | jobs: 8 | build-mac-ios-android: 9 | runs-on: macos-latest 10 | permissions: 11 | contents: write 12 | steps: 13 | #签出代码 14 | - uses: actions/checkout@v3 15 | with: 16 | ref: master 17 | #读取版本信息 18 | - name: Read version 19 | id: version 20 | uses: juliangruber/read-file-action@v1 21 | with: 22 | path: assets/version.json 23 | - name: Echo version 24 | run: echo "${{ fromJson(steps.version.outputs.content).version }}" 25 | - name: Echo version content 26 | run: echo "${{ fromJson(steps.version.outputs.content).version_desc }}" 27 | #上传至Release 28 | - name: Upload Release 29 | uses: juliangruber/read-file-action@v1 30 | with: 31 | path: assets/version.json 32 | - name: Echo version 33 | run: echo "${{ fromJson(steps.version.outputs.content).version }}" 34 | - name: Echo version content 35 | run: echo "${{ fromJson(steps.version.outputs.content).version_desc }}" 36 | - name: Upload Release 37 | uses: ncipollo/release-action@v1 38 | with: 39 | allowUpdates: true 40 | artifactErrorsFailBuild: false 41 | name: "${{ fromJson(steps.version.outputs.content).version }}" 42 | body: "${{ fromJson(steps.version.outputs.content).version_desc }}" 43 | prerelease: ${{ fromJson(steps.version.outputs.content).prerelease }} 44 | token: ${{ secrets.TOKEN }} 45 | #完成 46 | - run: echo "🍏 This job's status is ${{ job.status }}." 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | 46 | pubspec.lock 47 | key.properties 48 | **/*.keystore 49 | **/*.jks 50 | 51 | /assets/keystore/ 52 | /assets/pure_live_web/ 53 | pubspec.lock -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 17 | base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 18 | - platform: android 19 | create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 20 | base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmake.sourceDirectory": "E:/project/pure_live/windows", 3 | "cmake.configureOnOpen": true 4 | } -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/android/app/proguard-rules.pro -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/mystyle/pure_live/BetterPlayerService.kt: -------------------------------------------------------------------------------- 1 | package com.mystyle.purelive 2 | 3 | import android.app.* 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Build 7 | import android.os.IBinder 8 | import androidx.annotation.RequiresApi 9 | import androidx.core.app.NotificationCompat 10 | import androidx.core.app.NotificationCompat.PRIORITY_MIN 11 | 12 | class BetterPlayerService : Service() { 13 | 14 | companion object { 15 | const val notificationId = 20772077 16 | const val foregroundNotificationId = 20772078 17 | const val channelId = "VideoPlayer" 18 | } 19 | 20 | override fun onBind(intent: Intent?): IBinder? { 21 | return null 22 | } 23 | 24 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 25 | val channelId = 26 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 27 | createNotificationChannel(channelId, "Channel") 28 | } else { 29 | "" 30 | } 31 | val notificationIntent = Intent(this, MainActivity::class.java) 32 | val pendingIntent = 33 | PendingIntent.getActivity( 34 | this, 0, notificationIntent, 35 | PendingIntent.FLAG_IMMUTABLE 36 | ) 37 | 38 | 39 | val notificationBuilder = NotificationCompat.Builder(this, channelId) 40 | .setContentTitle("纯粹直播通知") 41 | .setContentText("纯粹直播正在运行") 42 | .setSmallIcon(R.mipmap.ic_launcher) 43 | .setPriority(PRIORITY_MIN) 44 | .setOngoing(true) 45 | .setContentIntent(pendingIntent) 46 | 47 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 48 | notificationBuilder.setCategory(Notification.CATEGORY_SERVICE); 49 | } 50 | startForeground(foregroundNotificationId, notificationBuilder.build()) 51 | return START_NOT_STICKY 52 | } 53 | 54 | @RequiresApi(Build.VERSION_CODES.O) 55 | private fun createNotificationChannel(channelId: String, channelName: String): String { 56 | val chan = NotificationChannel( 57 | channelId, 58 | channelName, NotificationManager.IMPORTANCE_NONE 59 | ) 60 | val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 61 | service.createNotificationChannel(chan) 62 | return channelId 63 | } 64 | 65 | override fun onTaskRemoved(rootIntent: Intent?) { 66 | try { 67 | val notificationManager = 68 | getSystemService( 69 | Context.NOTIFICATION_SERVICE 70 | ) as NotificationManager 71 | notificationManager.cancel(notificationId) 72 | } catch (exception: Exception) { 73 | 74 | } finally { 75 | stopSelf() 76 | } 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/mystyle/pure_live/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mystyle.purelive 2 | 3 | import android.content.Intent 4 | import android.os.Build 5 | import android.os.Bundle 6 | import io.flutter.embedding.android.FlutterActivity 7 | 8 | class MainActivity: FlutterActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | startNotificationService() 12 | } 13 | 14 | override fun onDestroy() { 15 | super.onDestroy() 16 | stopNotificationService() 17 | } 18 | ///TODO: Call this method via channel after remote notification start 19 | private fun startNotificationService() { 20 | try { 21 | val intent = Intent(this, BetterPlayerService::class.java) 22 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { 23 | startForegroundService(intent) 24 | } else { 25 | startService(intent) 26 | } 27 | } catch (exception: Exception) { 28 | } 29 | } 30 | 31 | ///TODO: Call this method via channel after remote notification stop 32 | private fun stopNotificationService() { 33 | try { 34 | val intent = Intent(this, BetterPlayerService::class.java) 35 | stopService(intent) 36 | } catch (exception: Exception) { 37 | 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /android/app/src/main/libs/Msc.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/android/app/src/main/libs/Msc.jar -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/android/app/src/main/res/drawable/banner.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-en/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | PureLive 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 纯粹直播 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/filepaths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/share_targets.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // This file is auto generated. 6 | // To update all the build.gradle files in the Flutter repo, 7 | // See dev/tools/bin/generate_gradle_lockfiles.dart. 8 | 9 | buildscript { 10 | ext.kotlin_version = '1.9.10' 11 | repositories { 12 | maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } //gradle-plugin 13 | maven { url 'https://maven.aliyun.com/repository/google' } //google 14 | maven { url 'https://maven.aliyun.com/repository/public' } //public 15 | maven { url 'https://maven.aliyun.com/repository/jcenter'} //jcenter 16 | } 17 | 18 | dependencies { 19 | classpath 'com.android.tools.build:gradle:7.3.0' 20 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 21 | } 22 | 23 | configurations.classpath { 24 | resolutionStrategy.activateDependencyLocking() 25 | } 26 | } 27 | 28 | allprojects { 29 | repositories { 30 | maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } //gradle-plugin 31 | maven { url 'https://maven.aliyun.com/repository/google' } //google 32 | maven { url 'https://maven.aliyun.com/repository/public' } //public 33 | maven { url 'https://maven.aliyun.com/repository/jcenter'} //jcenter 34 | } 35 | } 36 | 37 | rootProject.buildDir = '../build' 38 | 39 | subprojects { 40 | project.buildDir = "${rootProject.buildDir}/${project.name}" 41 | } 42 | 43 | subprojects { 44 | project.configurations.all { 45 | resolutionStrategy.eachDependency { details -> 46 | if (details.requested.group == 'com.android.support' 47 | && !details.requested.name.contains('multidex') ) { 48 | details.useVersion "27.1.1" 49 | } 50 | } 51 | } 52 | } 53 | 54 | subprojects { 55 | project.evaluationDependsOn(':app') 56 | 57 | dependencyLocking { 58 | ignoredDependencies.add('io.flutter:*') 59 | lockFile = file("${rootProject.projectDir}/project-${project.name}.lockfile") 60 | lockAllConfigurations() 61 | } 62 | } 63 | 64 | tasks.register("clean", Delete) { 65 | delete rootProject.buildDir 66 | } 67 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /assets/PingFangSC.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/assets/PingFangSC.ttf -------------------------------------------------------------------------------- /assets/icons/CustomIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/assets/icons/CustomIcons.ttf -------------------------------------------------------------------------------- /assets/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/assets/icons/icon.png -------------------------------------------------------------------------------- /assets/icons/icon_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/assets/icons/icon_foreground.png -------------------------------------------------------------------------------- /assets/images/bilibili.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/assets/images/bilibili.png -------------------------------------------------------------------------------- /assets/images/bilibili_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/assets/images/bilibili_2.png -------------------------------------------------------------------------------- /assets/images/douyin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/assets/images/douyin.png -------------------------------------------------------------------------------- /assets/images/douyu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/assets/images/douyu.png -------------------------------------------------------------------------------- /assets/images/egame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/assets/images/egame.png -------------------------------------------------------------------------------- /assets/images/huya.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/assets/images/huya.png -------------------------------------------------------------------------------- /assets/images/kuaishou.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/assets/images/kuaishou.png -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/assets/images/wechat.png -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | adssa 10 | 11 | -------------------------------------------------------------------------------- /assets/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.5.9", 3 | "version_num": 10505, 4 | "version_desc": "-Fix:修复虎牙一起看.", 5 | "prerelease":false, 6 | "download_url": "https://github.com/liuchuancong/pure_live/releases" 7 | } 8 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/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 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Pure Live 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | pure_live 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /iptvgenerate.py: -------------------------------------------------------------------------------- 1 | import os 2 | path="d:/flutter/pure_live/assets/iptv/category" 3 | jsonpath="d:/flutter/pure_live/assets/iptv/categories.json" 4 | import json 5 | data = [] 6 | #获取该目录下所有文件,存入列表中 7 | n=0 8 | for n,name in enumerate(os.listdir(path)): 9 | 10 | #设置旧文件名(就是路径+文件名) 11 | oldname= name # os.sep添加系统分隔符 12 | 13 | #设置新文件名 14 | newname='category'+str(n+1) 15 | 16 | data.append({ 17 | 'id': str(n+1), 18 | 'path': oldname, 19 | 'name': newname 20 | }) 21 | # os.rename(oldname,newname) #用os模块中的rename方法对文件改名 22 | print(oldname,'======>',newname) 23 | 24 | n+=1 25 | print(data,'======>',data) 26 | with open(jsonpath, "w") as json_file: 27 | json_data = json.dumps(data, indent=4) 28 | json_file.write(json_data) -------------------------------------------------------------------------------- /lib/common/index.dart: -------------------------------------------------------------------------------- 1 | library common; 2 | 3 | export '../core/index.dart'; 4 | export 'l10n/generated/l10n.dart'; 5 | export 'models/index.dart'; 6 | export 'services/index.dart'; 7 | export 'style/index.dart'; 8 | export 'utils/index.dart'; 9 | export 'utils/string_to_boolean.dart'; 10 | export 'widgets/index.dart'; 11 | export 'package:flutter_color/flutter_color.dart'; 12 | export 'package:flutter/material.dart'; 13 | export 'package:windows_single_instance/windows_single_instance.dart'; 14 | export 'package:pure_live/modules/popular/popular_controller.dart'; 15 | export 'package:pure_live/modules/favorite/favorite_controller.dart'; 16 | export 'package:permission_handler/permission_handler.dart'; 17 | 18 | export 'package:pure_live/modules/auth/auth_controller.dart'; 19 | export 'package:pure_live/modules/areas/areas_controller.dart'; 20 | export 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; 21 | export 'package:flutter_localizations/flutter_localizations.dart'; 22 | export 'package:media_kit/media_kit.dart'; 23 | export 'package:app_links/app_links.dart'; 24 | export 'package:pure_live/common/index.dart'; 25 | export 'package:uri_to_file/uri_to_file.dart'; 26 | export 'package:easy_refresh/easy_refresh.dart'; 27 | export 'package:pure_live/plugins/supabase.dart'; 28 | export 'package:pure_live/routes/app_pages.dart'; 29 | export 'package:path_provider/path_provider.dart'; 30 | export 'package:dynamic_color/dynamic_color.dart'; 31 | export 'package:pure_live/routes/route_path.dart'; 32 | export 'package:share_handler/share_handler.dart'; 33 | export 'package:pure_live/plugins/window_util.dart'; 34 | export 'package:win32_registry/win32_registry.dart'; 35 | export 'package:window_manager/window_manager.dart'; 36 | export 'package:shared_preferences/shared_preferences.dart'; 37 | -------------------------------------------------------------------------------- /lib/common/l10n/generated/intl/messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | // Ignore issues from commonly used lints in this file. 6 | // ignore_for_file:implementation_imports, file_names, unnecessary_new 7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering 8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment 9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases 10 | // ignore_for_file:comment_references 11 | 12 | import 'dart:async'; 13 | 14 | import 'package:flutter/foundation.dart'; 15 | import 'package:intl/intl.dart'; 16 | import 'package:intl/message_lookup_by_library.dart'; 17 | import 'package:intl/src/intl_helpers.dart'; 18 | 19 | import 'messages_en.dart' as messages_en; 20 | import 'messages_zh_CN.dart' as messages_zh_cn; 21 | 22 | typedef Future LibraryLoader(); 23 | Map _deferredLibraries = { 24 | 'en': () => new SynchronousFuture(null), 25 | 'zh_CN': () => new SynchronousFuture(null), 26 | }; 27 | 28 | MessageLookupByLibrary? _findExact(String localeName) { 29 | switch (localeName) { 30 | case 'en': 31 | return messages_en.messages; 32 | case 'zh_CN': 33 | return messages_zh_cn.messages; 34 | default: 35 | return null; 36 | } 37 | } 38 | 39 | /// User programs should call this before using [localeName] for messages. 40 | Future initializeMessages(String localeName) { 41 | var availableLocale = Intl.verifiedLocale( 42 | localeName, (locale) => _deferredLibraries[locale] != null, 43 | onFailure: (_) => null); 44 | if (availableLocale == null) { 45 | return new SynchronousFuture(false); 46 | } 47 | var lib = _deferredLibraries[availableLocale]; 48 | lib == null ? new SynchronousFuture(false) : lib(); 49 | initializeInternalMessageLookup(() => new CompositeMessageLookup()); 50 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 51 | return new SynchronousFuture(true); 52 | } 53 | 54 | bool _messagesExistFor(String locale) { 55 | try { 56 | return _findExact(locale) != null; 57 | } catch (e) { 58 | return false; 59 | } 60 | } 61 | 62 | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { 63 | var actualLocale = 64 | Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); 65 | if (actualLocale == null) return null; 66 | return _findExact(actualLocale); 67 | } 68 | -------------------------------------------------------------------------------- /lib/common/models/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/common/models/index.dart: -------------------------------------------------------------------------------- 1 | library models; 2 | 3 | export 'live_room.dart'; 4 | export 'live_area.dart'; 5 | export './live_message.dart'; 6 | -------------------------------------------------------------------------------- /lib/common/models/live_area.dart: -------------------------------------------------------------------------------- 1 | class LiveArea { 2 | String? platform = ''; 3 | String? areaType = ''; 4 | String? typeName = ''; 5 | String? areaId = ''; 6 | String? areaName = ''; 7 | String? areaPic = ''; 8 | String? shortName = ''; 9 | 10 | LiveArea({ 11 | this.platform, 12 | this.areaType, 13 | this.typeName, 14 | this.areaId, 15 | this.areaName, 16 | this.areaPic, 17 | this.shortName, 18 | }); 19 | 20 | LiveArea.fromJson(Map json) 21 | : platform = json['platform'] ?? '', 22 | areaType = json['areaType'] ?? '', 23 | typeName = json['typeName'] ?? '', 24 | areaId = json['areaId'] ?? '', 25 | areaName = json['areaName'] ?? '', 26 | areaPic = json['areaPic'] ?? '', 27 | shortName = json['shortName'] ?? ''; 28 | 29 | Map toJson() => { 30 | 'platform': platform, 31 | 'areaType': areaType, 32 | 'typeName': typeName, 33 | 'areaId': areaId, 34 | 'areaName': areaName, 35 | 'areaPic': areaPic, 36 | 'shortName': shortName, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /lib/common/models/live_message.dart: -------------------------------------------------------------------------------- 1 | enum LiveMessageType { 2 | /// 聊天 3 | chat, 4 | 5 | /// 礼物,暂时不支持 6 | gift, 7 | 8 | /// 在线人数 9 | online, 10 | 11 | /// 醒目留言 12 | superChat, 13 | } 14 | 15 | class LiveMessage { 16 | /// 消息类型 17 | final LiveMessageType type; 18 | 19 | /// 用户名 20 | final String userName; 21 | 22 | /// 信息 23 | final String message; 24 | 25 | /// 数据 26 | /// 单Type=Online时,Data为人气值(long) 27 | final dynamic data; 28 | 29 | /// 弹幕颜色 30 | final LiveMessageColor color; 31 | LiveMessage({ 32 | required this.type, 33 | required this.userName, 34 | required this.message, 35 | this.data, 36 | required this.color, 37 | }); 38 | } 39 | 40 | class LiveMessageColor { 41 | final int r, g, b; 42 | LiveMessageColor(this.r, this.g, this.b); 43 | static LiveMessageColor get white => LiveMessageColor(255, 255, 255); 44 | static LiveMessageColor numberToColor(int intColor) { 45 | var obj = intColor.toRadixString(16); 46 | 47 | LiveMessageColor color = LiveMessageColor.white; 48 | if (obj.length == 4) { 49 | obj = "00$obj"; 50 | } 51 | if (obj.length == 6) { 52 | var R = int.parse(obj.substring(0, 2), radix: 16); 53 | var G = int.parse(obj.substring(2, 4), radix: 16); 54 | var B = int.parse(obj.substring(4, 6), radix: 16); 55 | 56 | color = LiveMessageColor(R, G, B); 57 | } 58 | if (obj.length == 8) { 59 | var R = int.parse(obj.substring(2, 4), radix: 16); 60 | var G = int.parse(obj.substring(4, 6), radix: 16); 61 | var B = int.parse(obj.substring(6, 8), radix: 16); 62 | //var A = int.parse(obj.substring(0, 2), radix: 16); 63 | color = LiveMessageColor(R, G, B); 64 | } 65 | 66 | return color; 67 | } 68 | 69 | @override 70 | String toString() { 71 | return "#${r.toRadixString(16).padLeft(2, '0')}${g.toRadixString(16).padLeft(2, '0')}${b.toRadixString(16).padLeft(2, '0')}"; 72 | } 73 | } 74 | 75 | class LiveSuperChatMessage { 76 | final String userName; 77 | final String face; 78 | final String message; 79 | final int price; 80 | final DateTime startTime; 81 | final DateTime endTime; 82 | final String backgroundColor; 83 | final String backgroundBottomColor; 84 | LiveSuperChatMessage({ 85 | required this.backgroundBottomColor, 86 | required this.backgroundColor, 87 | required this.endTime, 88 | required this.face, 89 | required this.message, 90 | required this.price, 91 | required this.startTime, 92 | required this.userName, 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /lib/common/services/bilibili_account_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; 4 | import 'package:get/get.dart'; 5 | import 'package:pure_live/common/models/bilibili_user_info_page.dart'; 6 | import 'package:pure_live/common/services/settings_service.dart'; 7 | import 'package:pure_live/common/utils/pref_util.dart'; 8 | import 'package:pure_live/core/common/http_client.dart'; 9 | import 'package:pure_live/core/site/bilibili_site.dart'; 10 | import 'package:pure_live/core/sites.dart'; 11 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 12 | 13 | class BiliBiliAccountService extends GetxController { 14 | static BiliBiliAccountService get instance => 15 | Get.find(); 16 | final SettingsService settingsService = Get.find(); 17 | 18 | var logined = false.obs; 19 | 20 | var cookie = "".obs; 21 | var uid = 0; 22 | var name = "未登录".obs; 23 | static const String kBilibiliCookie = "bilibiliCookie"; 24 | @override 25 | void onInit() { 26 | cookie.value = PrefUtil.getString(kBilibiliCookie) ?? ''; 27 | logined.value = cookie.isNotEmpty; 28 | loadUserInfo(); 29 | super.onInit(); 30 | } 31 | 32 | Future loadUserInfo() async { 33 | if (cookie.isEmpty) { 34 | return; 35 | } 36 | Timer(const Duration(seconds: 1), () async { 37 | try { 38 | var result = await HttpClient.instance.getJson( 39 | "https://api.bilibili.com/x/member/web/account", 40 | header: { 41 | "Cookie": cookie, 42 | }, 43 | ); 44 | if (result["code"] == 0) { 45 | var info = BiliBiliUserInfoModel.fromJson(result["data"]); 46 | name.value = info.uname ?? "未登录"; 47 | uid = info.mid ?? 0; 48 | setSite(); 49 | } else { 50 | SmartDialog.showToast("哔哩哔哩登录已失效,请重新登录"); 51 | logout(); 52 | } 53 | } catch (e) { 54 | SmartDialog.showToast("获取哔哩哔哩用户信息失败,可前往账号管理重试"); 55 | } 56 | }); 57 | } 58 | 59 | void setSite() { 60 | var site = (Sites.of('bilibili').liveSite as BiliBiliSite); 61 | site.userId = uid; 62 | site.cookie = cookie.value; 63 | } 64 | 65 | void setCookie(String cookie) { 66 | this.cookie.value = cookie; 67 | settingsService.bilibiliCookie.value = cookie; 68 | logined.value = cookie.isNotEmpty; 69 | } 70 | 71 | void resetCookie(String cookie) { 72 | this.cookie.value = cookie; 73 | logined.value = cookie.isNotEmpty; 74 | } 75 | 76 | void logout() async { 77 | cookie.value = ""; 78 | uid = 0; 79 | name.value = "未登录"; 80 | setSite(); 81 | PrefUtil.setString(kBilibiliCookie, ''); 82 | logined.value = false; 83 | CookieManager cookieManager = CookieManager.instance(); 84 | await cookieManager.deleteAllCookies(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/common/services/index.dart: -------------------------------------------------------------------------------- 1 | library services; 2 | 3 | export './settings_service.dart'; 4 | -------------------------------------------------------------------------------- /lib/common/style/index.dart: -------------------------------------------------------------------------------- 1 | library style; 2 | 3 | export './theme.dart'; 4 | -------------------------------------------------------------------------------- /lib/common/style/theme.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class MyTheme { 5 | Color? primaryColor; 6 | ColorScheme? colorScheme; 7 | String? fontFamily; 8 | 9 | MyTheme({ 10 | this.primaryColor, 11 | this.colorScheme, 12 | }) : assert(colorScheme == null || primaryColor == null); 13 | 14 | get lightThemeData { 15 | if (Platform.isWindows) { 16 | fontFamily = 'PingFang'; 17 | } 18 | return ThemeData( 19 | useMaterial3: true, 20 | colorSchemeSeed: primaryColor, 21 | colorScheme: colorScheme, 22 | brightness: Brightness.light, 23 | fontFamily: fontFamily, 24 | ); 25 | } 26 | 27 | get darkThemeData { 28 | if (Platform.isWindows) { 29 | fontFamily = 'PingFang'; 30 | } 31 | return ThemeData( 32 | useMaterial3: true, 33 | colorSchemeSeed: primaryColor, 34 | colorScheme: colorScheme?.copyWith( 35 | error: const Color.fromARGB(255, 255, 99, 71), 36 | ), 37 | brightness: Brightness.dark, 38 | fontFamily: fontFamily, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/common/utils/cache_manager.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: implementation_imports 2 | import 'package:path/path.dart' as p; 3 | import 'package:file/local.dart'; 4 | import 'package:path_provider/path_provider.dart'; 5 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 6 | import 'package:flutter_cache_manager/src/storage/file_system/file_system_io.dart'; 7 | 8 | class CustomCacheManager { 9 | static const key = 'customCacheKey'; 10 | 11 | static CacheManager instance = CacheManager( 12 | Config( 13 | key, 14 | stalePeriod: const Duration(days: 1), 15 | maxNrOfCacheObjects: 20, 16 | repo: JsonCacheInfoRepository(databaseName: key), 17 | fileSystem: IOFileSystem(key), 18 | fileService: HttpFileService(), 19 | ), 20 | ); 21 | 22 | static Future cacheSize() async { 23 | var baseDir = await getTemporaryDirectory(); 24 | var path = p.join(baseDir.path, key); 25 | 26 | var fs = const LocalFileSystem(); 27 | var directory = fs.directory((path)); 28 | return (await directory.stat()).size / 8 / 1000; 29 | } 30 | 31 | static Future clearCache() async { 32 | var baseDir = await getTemporaryDirectory(); 33 | var path = p.join(baseDir.path, key); 34 | 35 | var fs = const LocalFileSystem(); 36 | var directory = fs.directory((path)); 37 | await directory.delete(recursive: true); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/common/utils/index.dart: -------------------------------------------------------------------------------- 1 | library utils; 2 | 3 | export './text_util.dart'; 4 | export './pref_util.dart'; 5 | export './version_util.dart'; 6 | export './cache_manager.dart'; 7 | export './snackbar_util.dart'; 8 | export './js_engine.dart'; 9 | -------------------------------------------------------------------------------- /lib/common/utils/js_engine.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_js/flutter_js.dart'; 3 | 4 | class JsEngine { 5 | static JavascriptRuntime? _jsRuntime; 6 | static JavascriptRuntime get jsRuntime => _jsRuntime!; 7 | 8 | static void init() { 9 | _jsRuntime ??= getJavascriptRuntime(); 10 | jsRuntime.enableHandlePromises(); 11 | loadPackages(); 12 | } 13 | 14 | static Future loadPackages() async { 15 | final cryptojs = await rootBundle.loadString('assets/crypto-js.js'); 16 | jsRuntime.evaluate(cryptojs); 17 | } 18 | 19 | static JsEvalResult evaluate(String code) { 20 | return jsRuntime.evaluate(code); 21 | } 22 | 23 | static Future evaluateAsync(String code) { 24 | return jsRuntime.evaluateAsync(code); 25 | } 26 | 27 | static dynamic onMessage(String channelName, dynamic Function(dynamic) fn) { 28 | return jsRuntime.onMessage(channelName, (args) => null); 29 | } 30 | 31 | static dynamic sendMessage({ 32 | required String channelName, 33 | required List args, 34 | String? uuid, 35 | }) { 36 | return jsRuntime.sendMessage(channelName: channelName, args: args); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/common/utils/pref_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | ///This is the new util class for the shared preferences. 4 | /// 5 | ///And the old `storage.dart` will be deprecated. 6 | class PrefUtil { 7 | static late SharedPreferences prefs; 8 | 9 | static dynamic getAnyPref(String key) { 10 | return prefs.get(key); 11 | } 12 | 13 | static void setAnyPref(String key, dynamic value) { 14 | if (value is String) { 15 | prefs.setString(key, value); 16 | } else if (value is int) { 17 | prefs.setInt(key, value); 18 | } else if (value is bool) { 19 | prefs.setBool(key, value); 20 | } else if (value is double) { 21 | prefs.setDouble(key, value); 22 | } else if (value is List) { 23 | prefs.setStringList(key, value); 24 | } 25 | } 26 | 27 | static bool? getBool(String key) { 28 | return prefs.getBool(key); 29 | } 30 | 31 | static Future setBool(String key, bool value) { 32 | return prefs.setBool(key, value); 33 | } 34 | 35 | static int? getInt(String key) { 36 | return prefs.getInt(key); 37 | } 38 | 39 | static Future setInt(String key, int value) { 40 | return prefs.setInt(key, value); 41 | } 42 | 43 | static String? getString(String key) { 44 | return prefs.getString(key); 45 | } 46 | 47 | static Future setString(String key, String value) { 48 | return prefs.setString(key, value); 49 | } 50 | 51 | static double? getDouble(String key) { 52 | return prefs.getDouble(key); 53 | } 54 | 55 | static Future setDouble(String key, double value) { 56 | return prefs.setDouble(key, value); 57 | } 58 | 59 | static List? getStringList(String key) { 60 | return prefs.getStringList(key); 61 | } 62 | 63 | static Future setStringList(String key, List value) { 64 | return prefs.setStringList(key, value); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/common/utils/snackbar_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | class SnackBarUtil { 4 | static void success(String text) { 5 | Get.snackbar( 6 | 'Success', 7 | text, 8 | duration: const Duration(seconds: 2), 9 | backgroundColor: Get.theme.colorScheme.surfaceVariant, 10 | colorText: Get.theme.colorScheme.onSurfaceVariant, 11 | snackPosition: SnackPosition.bottom, 12 | ); 13 | } 14 | 15 | static void error(String text) { 16 | Get.snackbar( 17 | 'Error', 18 | text, 19 | duration: const Duration(seconds: 2), 20 | backgroundColor: Get.theme.colorScheme.errorContainer, 21 | colorText: Get.theme.colorScheme.onErrorContainer, 22 | snackPosition: SnackPosition.bottom, 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/common/utils/string_to_boolean.dart: -------------------------------------------------------------------------------- 1 | extension TextUtilsStringExtension on String { 2 | bool toBoolean() { 3 | return toLowerCase() == "true" ? true : false; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/common/utils/supabase_policy.dart: -------------------------------------------------------------------------------- 1 | class SupabasePolicy { 2 | final String supabaseUrl; 3 | final String supabaseKey; 4 | final String id; 5 | final String userId; 6 | final String uid; 7 | final String config; 8 | final String email; 9 | final String updateAt; 10 | final String version; 11 | final String createdAt; 12 | final String tableName; 13 | final String checkTable; 14 | final String owner; 15 | SupabasePolicy( 16 | {required this.supabaseUrl, 17 | required this.supabaseKey, 18 | required this.id, 19 | required this.userId, 20 | required this.uid, 21 | required this.config, 22 | required this.email, 23 | required this.updateAt, 24 | required this.version, 25 | required this.createdAt, 26 | required this.tableName, 27 | required this.checkTable, 28 | required this.owner}); 29 | 30 | factory SupabasePolicy.fromJson(Map json) { 31 | return SupabasePolicy( 32 | supabaseUrl: json['supabaseUrl'], 33 | supabaseKey: json['supabaseKey'], 34 | id: json['id'], 35 | userId: json['user_id'], 36 | uid: json['uid'], 37 | config: json['config'], 38 | email: json['email'], 39 | updateAt: json['update_at'], 40 | version: json['version'], 41 | createdAt: json['created_at'], 42 | tableName: json['table_name'], 43 | checkTable: json['check_table'], 44 | owner: json['owner']); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/common/utils/text_util.dart: -------------------------------------------------------------------------------- 1 | String readableCount(String info) { 2 | try { 3 | int count = int.parse(info); 4 | if (count > 10000) { 5 | return '${(count / 10000).toStringAsFixed(1)}万'; 6 | } 7 | } catch (e) { 8 | return info; 9 | } 10 | return info; 11 | } 12 | -------------------------------------------------------------------------------- /lib/common/utils/version_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:http/http.dart' as http; 3 | 4 | class VersionUtil { 5 | static const String version = '1.5.9'; 6 | static const String projectUrl = 'https://github.com/liuchuancong/pure_live'; 7 | static const String releaseUrl = 'https://api.github.com/repos/liuchuancong/pure_live/releases'; 8 | static const String issuesUrl = 'https://github.com/liuchuancong/pure_live/issues'; 9 | static const String kanbanUrl = 10 | 'https://jackiu-notes.notion.site/50bc0d3d377445eea029c6e3d4195671?v=663125e639b047cea5e69d8264926b8b'; 11 | 12 | static const String githubUrl = 'https://github.com/liuchuancong'; 13 | static const String email = '17792321552@163.com'; 14 | static const String emailUrl = 'mailto:17792321552@163.com?subject=PureLive Feedback'; 15 | static const String telegramGroup = 't.me/pure_live_channel'; 16 | static const String telegramGroupUrl = 'https://t.me/pure_live_channel'; 17 | 18 | static String latestVersion = version; 19 | static String latestUpdateLog = ''; 20 | static List allReleased = []; 21 | static Future checkUpdate() async { 22 | try { 23 | var response = await http.get(Uri.parse(releaseUrl)); 24 | allReleased = await jsonDecode(response.body); 25 | var latest = allReleased[0]; 26 | latestVersion = latest['tag_name'].replaceAll('v', ''); 27 | latestUpdateLog = latest['body']; 28 | } catch (e) { 29 | latestUpdateLog = e.toString(); 30 | } 31 | } 32 | 33 | static bool hasNewVersion() { 34 | List latestVersions = latestVersion.split('-')[0].split('.'); 35 | List versions = version.split('-')[0].split('.'); 36 | for (int i = 0; i < latestVersions.length; i++) { 37 | if (int.parse(latestVersions[i]) > int.parse(versions[i])) { 38 | return true; 39 | } else if (int.parse(latestVersions[i]) < int.parse(versions[i])) { 40 | return false; 41 | } 42 | } 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/common/widgets/custom_icons.dart: -------------------------------------------------------------------------------- 1 | /// Flutter icons CustomIcons 2 | /// Copyright (C) 2023 by original authors @ fluttericon.com, fontello.com 3 | /// This font was generated by FlutterIcon.com, which is derived from Fontello. 4 | /// 5 | /// To use this font, place it in your fonts/ directory and include the 6 | /// following in your pubspec.yaml 7 | /// 8 | /// flutter: 9 | /// fonts: 10 | /// - family: CustomIcons 11 | /// fonts: 12 | /// - asset: fonts/CustomIcons.ttf 13 | /// 14 | /// 15 | /// * Font Awesome 5, Copyright (C) 2016 by Dave Gandy 16 | /// Author: Dave Gandy 17 | /// License: SIL (https://github.com/FortAwesome/Font-Awesome/blob/master/LICENSE.txt) 18 | /// Homepage: http://fortawesome.github.com/Font-Awesome/ 19 | /// * Font Awesome 4, Copyright (C) 2016 by Dave Gandy 20 | /// Author: Dave Gandy 21 | /// License: SIL () 22 | /// Homepage: http://fortawesome.github.com/Font-Awesome/ 23 | /// 24 | library; 25 | // ignore_for_file: constant_identifier_names 26 | 27 | import 'package:flutter/widgets.dart'; 28 | 29 | class CustomIcons { 30 | CustomIcons._(); 31 | 32 | static const _kFontFam = 'CustomIcons'; 33 | static const String? _kFontPkg = null; 34 | 35 | static const IconData danmaku_close = 36 | IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); 37 | static const IconData danmaku_open = 38 | IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); 39 | static const IconData danmaku_setting = 40 | IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); 41 | static const IconData popular = 42 | IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); 43 | static const IconData search = 44 | IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); 45 | static const IconData qq_1 = 46 | IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg); 47 | static const IconData float_window = 48 | IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg); 49 | static const IconData cast = 50 | IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg); 51 | static const IconData github_circled = 52 | IconData(0xf09b, fontFamily: _kFontFam, fontPackage: _kFontPkg); 53 | static const IconData mail_squared = 54 | IconData(0xf199, fontFamily: _kFontFam, fontPackage: _kFontPkg); 55 | static const IconData wechat = 56 | IconData(0xf1d7, fontFamily: _kFontFam, fontPackage: _kFontPkg); 57 | static const IconData telegram = 58 | IconData(0xf2c6, fontFamily: _kFontFam, fontPackage: _kFontPkg); 59 | static const IconData alipay = 60 | IconData(0xf642, fontFamily: _kFontFam, fontPackage: _kFontPkg); 61 | } 62 | -------------------------------------------------------------------------------- /lib/common/widgets/empty_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class EmptyView extends StatelessWidget { 4 | const EmptyView({ 5 | super.key, 6 | required this.icon, 7 | required this.title, 8 | required this.subtitle, 9 | }); 10 | 11 | final IconData icon; 12 | final String title; 13 | final String subtitle; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final color = Theme.of(context).disabledColor; 18 | return SingleChildScrollView( 19 | scrollDirection: Axis.vertical, 20 | child: ConstrainedBox( 21 | constraints: BoxConstraints(minHeight: MediaQuery.of(context).size.height), 22 | child: Center( 23 | child: Column( 24 | crossAxisAlignment: CrossAxisAlignment.center, 25 | mainAxisSize: MainAxisSize.min, 26 | mainAxisAlignment: MainAxisAlignment.center, 27 | children: [ 28 | Icon(icon, size: 144, color: color), 29 | const SizedBox(height: 24), 30 | Text.rich( 31 | TextSpan(children: [ 32 | TextSpan(text: "$title\n", style: Theme.of(context).textTheme.headlineMedium?.copyWith(color: color)), 33 | TextSpan(text: "\n$subtitle", style: Theme.of(context).textTheme.titleSmall?.copyWith(color: color)), 34 | ]), 35 | textAlign: TextAlign.center, 36 | ), 37 | const SizedBox(height: 32), 38 | ], 39 | ), 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/common/widgets/index.dart: -------------------------------------------------------------------------------- 1 | library widgets; 2 | 3 | export './room_card.dart'; 4 | export './empty_view.dart'; 5 | export './custom_icons.dart'; 6 | export './menu_button.dart'; 7 | export './search_button.dart'; 8 | export './section_listtile.dart'; 9 | -------------------------------------------------------------------------------- /lib/common/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 with AutomaticKeepAliveClientMixin { 13 | @override 14 | Widget build(BuildContext context) { 15 | super.build(context); 16 | return widget.child; 17 | } 18 | 19 | @override 20 | bool get wantKeepAlive => true; 21 | } 22 | -------------------------------------------------------------------------------- /lib/common/widgets/search_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/common/index.dart'; 3 | 4 | class SearchButton extends StatelessWidget { 5 | const SearchButton({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return IconButton( 10 | onPressed: () => Get.toNamed(RoutePath.kSearch), 11 | icon: const Icon(CustomIcons.search), 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/common/widgets/section_listtile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | 4 | class CupertinoSwitchListTile extends StatelessWidget { 5 | const CupertinoSwitchListTile({ 6 | super.key, 7 | required this.value, 8 | required this.onChanged, 9 | this.leading, 10 | this.title, 11 | this.subtitle, 12 | this.activeColor, 13 | }); 14 | 15 | final Widget? leading; 16 | final Widget? title; 17 | final Widget? subtitle; 18 | final Color? activeColor; 19 | final bool value; 20 | final void Function(bool)? onChanged; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return ListTile( 25 | leading: leading, 26 | title: title, 27 | subtitle: subtitle, 28 | onTap: () { 29 | if (onChanged != null) onChanged!(!value); 30 | }, 31 | trailing: CupertinoSwitch( 32 | value: value, 33 | activeColor: activeColor, 34 | onChanged: onChanged, 35 | ), 36 | ); 37 | } 38 | } 39 | 40 | class SectionTitle extends StatelessWidget { 41 | final String title; 42 | 43 | const SectionTitle({ 44 | required this.title, 45 | super.key, 46 | }); 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | return ListTile( 51 | contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), 52 | title: Text( 53 | title, 54 | style: Theme.of(context).textTheme.headlineSmall?.copyWith( 55 | color: Theme.of(context).colorScheme.primary, 56 | fontWeight: FontWeight.w500, 57 | ), 58 | ), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/core/common/convert_helper.dart: -------------------------------------------------------------------------------- 1 | T? asT(dynamic value) { 2 | if (value is T) { 3 | return value; 4 | } 5 | return null; 6 | } 7 | -------------------------------------------------------------------------------- /lib/core/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/core/common/core_log.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | 3 | class CoreLog { 4 | static bool enableLog = true; 5 | static Function(Level, String)? onPrintLog; 6 | static Logger logger = Logger( 7 | printer: PrettyPrinter( 8 | methodCount: 0, 9 | errorMethodCount: 8, 10 | lineLength: 120, 11 | colors: true, 12 | printEmojis: true, 13 | printTime: false, 14 | ), 15 | ); 16 | 17 | static void d(String message) { 18 | onPrintLog?.call(Level.debug, message); 19 | if (!enableLog) { 20 | return; 21 | } 22 | logger.d("${DateTime.now().toString()}\n$message"); 23 | } 24 | 25 | static void i(String message) { 26 | onPrintLog?.call(Level.info, message); 27 | if (!enableLog) { 28 | return; 29 | } 30 | logger.i("${DateTime.now().toString()}\n$message"); 31 | } 32 | 33 | static void e(String message, StackTrace stackTrace) { 34 | onPrintLog?.call(Level.error, message); 35 | if (!enableLog) { 36 | return; 37 | } 38 | logger.e("${DateTime.now().toString()}\n$message", stackTrace: stackTrace); 39 | } 40 | 41 | static void error(e) { 42 | onPrintLog?.call(Level.error, e.toString()); 43 | logger.e( 44 | "${DateTime.now().toString()}\n${e.toString()}", 45 | error: e, 46 | stackTrace: (e is Error) ? e.stackTrace : StackTrace.current, 47 | ); 48 | } 49 | 50 | static void w(String message) { 51 | onPrintLog?.call(Level.warning, message); 52 | if (!enableLog) { 53 | return; 54 | } 55 | logger.w("${DateTime.now().toString()}\n$message"); 56 | } 57 | 58 | static void logPrint(dynamic obj) { 59 | onPrintLog?.call(Level.error, obj.toString()); 60 | if (!enableLog) { 61 | return; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/core/common/custom_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | class CustomInterceptor extends Interceptor { 4 | @override 5 | void onRequest(RequestOptions options, RequestInterceptorHandler handler) { 6 | options.extra["ts"] = DateTime.now().millisecondsSinceEpoch; 7 | super.onRequest(options, handler); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/core/common/iterum.dart: -------------------------------------------------------------------------------- 1 | import 'package:pure_live/common/index.dart'; 2 | 3 | class Iterum extends StatefulWidget { 4 | final Widget child; 5 | 6 | const Iterum({super.key, required this.child}); 7 | 8 | @override 9 | // ignore: library_private_types_in_public_api 10 | _IterumState createState() => _IterumState(); 11 | 12 | static revive(BuildContext context) { 13 | context.findAncestorStateOfType<_IterumState>()!.revive(); 14 | } 15 | } 16 | 17 | class _IterumState extends State { 18 | Key _key = UniqueKey(); 19 | void revive() { 20 | setState(() { 21 | _key = UniqueKey(); 22 | }); 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return KeyedSubtree( 28 | key: _key, 29 | child: widget.child, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/core/danmaku/empty_danmaku.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:pure_live/common/models/live_message.dart'; 3 | import 'package:pure_live/core/common/web_socket_util.dart'; 4 | import 'package:pure_live/core/interface/live_danmaku.dart'; 5 | // ignore_for_file: no_leading_underscores_for_local_identifiers 6 | 7 | 8 | 9 | class EmptyDanmaku implements LiveDanmaku { 10 | @override 11 | int heartbeatTime = 60 * 1000; 12 | 13 | @override 14 | Function(LiveMessage msg)? onMessage; 15 | @override 16 | Function(String msg)? onClose; 17 | @override 18 | Function()? onReady; 19 | 20 | WebScoketUtils? webScoketUtils; 21 | 22 | @override 23 | Future start(dynamic args) async {} 24 | 25 | void joinRoom() {} 26 | 27 | @override 28 | void heartbeat() {} 29 | 30 | @override 31 | Future stop() async { 32 | onMessage = null; 33 | onClose = null; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/core/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 = 18 | CommentTypeTag._(0, _omitEnumNames ? '' : 'COMMENTTYPETAGUNKNOWN'); 19 | static const CommentTypeTag COMMENTTYPETAGSTAR = 20 | CommentTypeTag._(1, _omitEnumNames ? '' : 'COMMENTTYPETAGSTAR'); 21 | 22 | static const $core.List values = [ 23 | COMMENTTYPETAGUNKNOWN, 24 | COMMENTTYPETAGSTAR, 25 | ]; 26 | 27 | static final $core.Map<$core.int, CommentTypeTag> _byValue = 28 | $pb.ProtobufEnum.initByValue(values); 29 | static CommentTypeTag? valueOf($core.int value) => _byValue[value]; 30 | 31 | const CommentTypeTag._($core.int v, $core.String n) : super(v, n); 32 | } 33 | 34 | const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); 35 | -------------------------------------------------------------------------------- /lib/core/index.dart: -------------------------------------------------------------------------------- 1 | library api; 2 | 3 | export 'sites.dart'; 4 | -------------------------------------------------------------------------------- /lib/core/interface/live_danmaku.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:pure_live/common/models/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 | -------------------------------------------------------------------------------- /lib/core/interface/live_site.dart: -------------------------------------------------------------------------------- 1 | import 'package:pure_live/common/models/live_area.dart'; 2 | import 'package:pure_live/common/models/live_message.dart'; 3 | import 'package:pure_live/common/models/live_room.dart'; 4 | import 'package:pure_live/core/interface/live_danmaku.dart'; 5 | import 'package:pure_live/model/live_anchor_item.dart'; 6 | import 'package:pure_live/model/live_category.dart'; 7 | import 'package:pure_live/model/live_category_result.dart'; 8 | import 'package:pure_live/model/live_play_quality.dart'; 9 | 10 | import 'package:pure_live/model/live_search_result.dart'; 11 | 12 | class LiveSite { 13 | /// 站点唯一ID 14 | String id = ""; 15 | 16 | /// 站点名称 17 | String name = ""; 18 | 19 | /// 站点名称 20 | LiveDanmaku getDanmaku() => LiveDanmaku(); 21 | 22 | /// 读取网站的分类 23 | Future> getCategores(int page, int pageSize) { 24 | return Future.value([]); 25 | } 26 | 27 | /// 搜索直播间 28 | Future searchRooms(String keyword, {int page = 1}) { 29 | return Future.value( 30 | LiveSearchRoomResult(hasMore: false, items: [])); 31 | } 32 | 33 | /// 搜索直播间 34 | Future searchAnchors(String keyword, {int page = 1}) { 35 | return Future.value( 36 | LiveSearchAnchorResult(hasMore: false, items: [])); 37 | } 38 | 39 | /// 读取类目下房间 40 | Future getCategoryRooms(LiveArea category, 41 | {int page = 1}) { 42 | return Future.value( 43 | LiveCategoryResult(hasMore: false, items: [])); 44 | } 45 | 46 | /// 读取推荐的房间 47 | Future getRecommendRooms({int page = 1}) { 48 | return Future.value( 49 | LiveCategoryResult(hasMore: false, items: [])); 50 | } 51 | 52 | /// 读取房间详情 53 | Future getRoomDetail({required String roomId}) { 54 | return Future.value(LiveRoom( 55 | cover: '', 56 | watching: '0', 57 | roomId: '', 58 | status: false, 59 | liveStatus: LiveStatus.offline, 60 | title: '', 61 | link: '', 62 | avatar: '', 63 | nick: '', 64 | isRecord: false)); 65 | } 66 | 67 | /// 读取房间清晰度 68 | Future> getPlayQualites({required LiveRoom detail}) { 69 | return Future.value([]); 70 | } 71 | 72 | /// 读取播放链接 73 | Future> getPlayUrls( 74 | {required LiveRoom detail, required LivePlayQuality quality}) { 75 | return Future.value([]); 76 | } 77 | 78 | /// 查询直播状态 79 | Future getLiveStatus({required String roomId}) { 80 | return Future.value(false); 81 | } 82 | 83 | /// 读取指定房间的SC 84 | Future> getSuperChatMessage( 85 | {required String roomId}) { 86 | return Future.value([]); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/core/iptv/m3u_parser_nullsafe.dart: -------------------------------------------------------------------------------- 1 | library m3u_parser_nullsafe; 2 | 3 | export 'package:pure_live/core/iptv/src/m3u_list.dart'; 4 | export 'package:pure_live/core/iptv/src/m3u_item.dart'; 5 | export 'package:pure_live/core/iptv/src/m3u_header.dart'; 6 | -------------------------------------------------------------------------------- /lib/core/iptv/src/general_utils_object_extension.dart: -------------------------------------------------------------------------------- 1 | /// - [isNullOrEmpty], [isNullEmptyOrFalse], [isNullEmptyZeroOrFalse] are from [this StackOverflow answer](https://stackoverflow.com/a/59826129/10830091) 2 | extension GeneralUtilsObjectExtension on Object { 3 | /// Returns true if object is: 4 | /// - null `Object` 5 | // ignore: unnecessary_null_comparison 6 | bool get isNull => this == null; 7 | 8 | /// Returns true if object is NOT: 9 | /// - null `Object` 10 | // ignore: unnecessary_null_comparison 11 | bool get isNotNull => this != null; 12 | 13 | /// Returns true if object is: 14 | /// - null `Object` 15 | /// - empty `String` 16 | /// - empty `Iterable` (list, map, set, ...) 17 | bool get isNullOrEmpty => 18 | isNull || _isStringObjectEmpty || _isIterableObjectEmpty; 19 | 20 | /// Returns true if object is: 21 | /// - null `Object` 22 | /// - empty `String` 23 | /// - empty `Iterable` (list, map, set, ...) 24 | /// - false `bool` 25 | bool get isNullEmptyOrFalse => 26 | isNull || 27 | _isStringObjectEmpty || 28 | _isIterableObjectEmpty || 29 | _isBoolObjectFalse; 30 | 31 | /// Returns true if object is: 32 | /// - null `Object` 33 | /// - empty `String` 34 | /// - empty `Iterable` (list, map, set, ...) 35 | /// - false `bool` 36 | /// - zero `num` 37 | bool get isNullEmptyFalseOrZero => 38 | isNull || 39 | _isStringObjectEmpty || 40 | _isIterableObjectEmpty || 41 | _isBoolObjectFalse || 42 | _isNumObjectZero; 43 | 44 | // ------- PRIVATE EXTENSION HELPERS ------- 45 | /// **Private helper** 46 | /// 47 | /// If `String` object, return String's method `isEmpty` 48 | /// 49 | /// Otherwise return `false` to not affect logical-OR expression. As `false` denotes undefined or N/A since object is not `String` 50 | bool get _isStringObjectEmpty => 51 | (this is String) ? (this as String).isEmpty : false; 52 | 53 | /// **Private helper** 54 | /// 55 | /// If `Iterable` object, return Iterable's method `isEmpty` 56 | /// 57 | /// Otherwise return `false` to not affect logical-OR expression. As `false` denotes undefined or N/A since object is not `Iterable` 58 | bool get _isIterableObjectEmpty => 59 | (this is Iterable) ? (this as Iterable).isEmpty : false; 60 | 61 | /// **Private helper** 62 | /// 63 | /// If `bool` object, return `isFalse` expression 64 | /// 65 | /// Otherwise return `false` to not affect logical-OR expression. As `false` denotes undefined or N/A since object is not `bool` 66 | bool get _isBoolObjectFalse => 67 | (this is bool) ? (this as bool) == false : false; 68 | 69 | /// **Private helper** 70 | /// 71 | /// If `num` object, return `isZero` expression 72 | /// 73 | /// Otherwise return `false` to not affect logical-OR expression. As `false` denotes undefined or N/A since object is not `num` 74 | bool get _isNumObjectZero => (this is num) ? (this as num) == 0 : false; 75 | } 76 | -------------------------------------------------------------------------------- /lib/core/iptv/src/m3u_header.dart: -------------------------------------------------------------------------------- 1 | class M3uHeader { 2 | Map attributes; 3 | 4 | M3uHeader({Map? attributes}) : attributes = attributes ?? {}; 5 | } 6 | -------------------------------------------------------------------------------- /lib/core/iptv/src/m3u_item.dart: -------------------------------------------------------------------------------- 1 | class M3uItem { 2 | int duration; 3 | 4 | String title; 5 | 6 | String groupTitle; 7 | 8 | Map attributes; 9 | 10 | String link; 11 | 12 | M3uItem( 13 | {required this.duration, 14 | required this.title, 15 | this.groupTitle = '', 16 | Map? attributes, 17 | this.link = ''}) 18 | : attributes = attributes ?? {}; 19 | 20 | factory M3uItem.fromItem(M3uItem item, String link) => M3uItem( 21 | duration: item.duration, 22 | title: item.title, 23 | groupTitle: item.groupTitle, 24 | attributes: item.attributes, 25 | link: link); 26 | } 27 | -------------------------------------------------------------------------------- /lib/core/iptv/src/m3u_load_options.dart: -------------------------------------------------------------------------------- 1 | class M3uLoadOptions { 2 | String groupTitleField; 3 | String wrongItemTitle; 4 | 5 | M3uLoadOptions({String? groupTitleField, String? wrongItemTitle}) 6 | : groupTitleField = groupTitleField ?? 'group-title', 7 | wrongItemTitle = wrongItemTitle ?? 'Unknown'; 8 | } 9 | -------------------------------------------------------------------------------- /lib/core/iptv/src/text_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:pure_live/core/iptv/src/text_utils_string_extension.dart'; 2 | 3 | Map getKeyValueList(String input, List separator) { 4 | var result = {}; 5 | 6 | if (input.isNullEmptyOrWhitespace) { 7 | return result; 8 | } 9 | 10 | input = input.trim(); 11 | if (input.startsWith(',')) { 12 | input = input.substring(1); 13 | } 14 | 15 | var escape = false; 16 | var quoted = false; 17 | var startIndex = 0; 18 | var delimIndex = 0; 19 | 20 | final regExp = RegExp( 21 | '(^[\\s"]*)|([\\s"]+\$)', 22 | ); 23 | 24 | void addKeyValue(int endIndex) { 25 | if (delimIndex > startIndex) { 26 | var key = input 27 | .substring(startIndex, delimIndex) 28 | .replaceAll('\\"', '"') 29 | .replaceAll(regExp, ''); 30 | var value = input 31 | .substring(delimIndex + 1, endIndex) 32 | .replaceAll('\\"', '"') 33 | .replaceAll(regExp, ''); 34 | 35 | result[key] = value; 36 | } 37 | } 38 | 39 | for (var i = 0; i < input.length; i++) { 40 | var c = input[i]; 41 | 42 | if (c == '\\' && !escape) { 43 | escape = true; 44 | } else { 45 | if (c == '"' && !escape) { 46 | quoted = !quoted; 47 | } else if (!quoted) { 48 | if (separator.contains(c)) { 49 | addKeyValue(i); 50 | 51 | delimIndex = 0; 52 | startIndex = i + 1; 53 | } else if (c == '=') { 54 | delimIndex = i; 55 | } 56 | } 57 | 58 | escape = false; 59 | } 60 | } 61 | 62 | addKeyValue(input.length); 63 | 64 | return result; 65 | } 66 | -------------------------------------------------------------------------------- /lib/core/iptv/src/text_utils_string_extension.dart: -------------------------------------------------------------------------------- 1 | /// 2 | extension TextUtilsStringExtension on String { 3 | /// Returns true if string is: 4 | /// - null 5 | /// - empty 6 | /// - whitespace string. 7 | /// 8 | /// Characters considered "whitespace" are listed [here](https://stackoverflow.com/a/59826129/10830091). 9 | bool get isNullEmptyOrWhitespace => isEmpty || trim().isEmpty; 10 | } 11 | -------------------------------------------------------------------------------- /lib/core/sites.dart: -------------------------------------------------------------------------------- 1 | import 'site/huya_site.dart'; 2 | import 'package:get/get.dart'; 3 | import 'site/douyu_site.dart'; 4 | import 'site/douyin_site.dart'; 5 | import 'interface/live_site.dart'; 6 | import 'package:pure_live/core/site/cc_site.dart'; 7 | import 'package:pure_live/core/site/iptv_site.dart'; 8 | import 'package:pure_live/core/site/bilibili_site.dart'; 9 | import 'package:pure_live/core/site/kuaishou_site.dart'; 10 | import 'package:pure_live/common/services/settings_service.dart'; 11 | 12 | class Sites { 13 | static List supportSites = [ 14 | Site( 15 | id: "bilibili", 16 | name: "哔哩", 17 | logo: "assets/images/bilibili_2.png", 18 | liveSite: BiliBiliSite(), 19 | ), 20 | Site( 21 | id: "douyu", 22 | name: "斗鱼", 23 | logo: "assets/images/douyu.png", 24 | liveSite: DouyuSite(), 25 | ), 26 | Site( 27 | id: "huya", 28 | name: "虎牙", 29 | logo: "assets/images/huya.png", 30 | liveSite: HuyaSite(), 31 | ), 32 | Site( 33 | id: "douyin", 34 | name: "抖音", 35 | logo: "assets/images/douyin.png", 36 | liveSite: DouyinSite(), 37 | ), 38 | Site( 39 | id: "kuaishou", 40 | name: "快手", 41 | logo: "assets/images/kuaishou.png", 42 | liveSite: KuaishowSite(), 43 | ), 44 | Site( 45 | id: "cc", 46 | name: "网易CC", 47 | logo: "assets/images/kuaishou.png", 48 | liveSite: CCSite(), 49 | ), 50 | Site( 51 | id: "iptv", 52 | name: "网络", 53 | logo: "assets/images/kuaishou.png", 54 | liveSite: IptvSite(), 55 | ), 56 | ]; 57 | 58 | static Site of(String id) { 59 | return supportSites.firstWhere((e) => id == e.id); 60 | } 61 | 62 | List availableSites() { 63 | final SettingsService settingsService = Get.find(); 64 | return supportSites.where((element) => settingsService.hotAreasList.value.contains(element.id)).toList(); 65 | } 66 | } 67 | 68 | class Site { 69 | final String id; 70 | final String name; 71 | final String logo; 72 | final LiveSite liveSite; 73 | Site({ 74 | required this.id, 75 | required this.liveSite, 76 | required this.logo, 77 | required this.name, 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /lib/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/model/live_category.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:pure_live/common/models/index.dart'; 4 | 5 | class LiveCategory { 6 | final String name; 7 | final String id; 8 | final List children; 9 | LiveCategory({ 10 | required this.id, 11 | required this.name, 12 | required this.children, 13 | }); 14 | 15 | @override 16 | String toString() { 17 | return json.encode({ 18 | "name": name, 19 | "id": id, 20 | "children": children, 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/model/live_category_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:pure_live/common/models/live_room.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 | -------------------------------------------------------------------------------- /lib/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 | -------------------------------------------------------------------------------- /lib/model/live_search_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:pure_live/common/models/live_room.dart'; 2 | import 'package:pure_live/model/live_anchor_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 | -------------------------------------------------------------------------------- /lib/modules/about/donate_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; 2 | import 'package:pure_live/common/index.dart'; 3 | 4 | class DonatePage extends StatelessWidget { 5 | const DonatePage({super.key}); 6 | 7 | final widgets = const [WechatItem()]; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return LayoutBuilder(builder: (context, constraint) { 12 | final width = constraint.maxWidth; 13 | final crossAxisCount = width > 640 ? 2 : 1; 14 | return Scaffold( 15 | appBar: AppBar(title: Text(S.of(context).support_donate)), 16 | body: MasonryGridView.count( 17 | physics: const BouncingScrollPhysics(), 18 | crossAxisCount: crossAxisCount, 19 | itemCount: 1, 20 | itemBuilder: (BuildContext context, int index) => widgets[index], 21 | ), 22 | ); 23 | }); 24 | } 25 | } 26 | 27 | class WechatItem extends StatelessWidget { 28 | const WechatItem({super.key}); 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return Column( 33 | mainAxisSize: MainAxisSize.min, 34 | children: [ 35 | const SectionTitle(title: 'Wechat'), 36 | Container( 37 | alignment: Alignment.center, 38 | padding: const EdgeInsets.all(12), 39 | child: Image.asset( 40 | 'assets/images/wechat.png', 41 | fit: BoxFit.contain, 42 | ), 43 | ), 44 | ], 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/modules/about/version_history.dart: -------------------------------------------------------------------------------- 1 | import 'package:pure_live/common/index.dart'; 2 | 3 | class VersionHistoryPage extends StatefulWidget { 4 | const VersionHistoryPage({super.key}); 5 | 6 | @override 7 | State createState() => _VersionHistoryPageState(); 8 | } 9 | 10 | class _VersionHistoryPageState extends State { 11 | List loadHistoryList() { 12 | return VersionUtil.allReleased 13 | .map((e) => VersionHistoryModel(version: e['tag_name'].toString().replaceAll('v', ''), updateBody: e['body'])) 14 | .toList(); 15 | } 16 | 17 | List getRichTextList() { 18 | List versions = loadHistoryList(); 19 | return versions 20 | .map((e) => Padding( 21 | padding: const EdgeInsets.all(16.0), 22 | child: Column( 23 | mainAxisAlignment: MainAxisAlignment.start, 24 | crossAxisAlignment: CrossAxisAlignment.start, 25 | children: [ 26 | Text( 27 | e.version, 28 | style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), 29 | ), 30 | Text( 31 | e.updateBody, 32 | style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400), 33 | ), 34 | ], 35 | ), 36 | )) 37 | .toList(); 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return Scaffold( 43 | appBar: AppBar( 44 | title: const Text('版本历史更新'), 45 | ), 46 | body: ListView( 47 | scrollDirection: Axis.vertical, 48 | physics: const AlwaysScrollableScrollPhysics(), 49 | children: getRichTextList(), 50 | ), 51 | ); 52 | } 53 | } 54 | 55 | class VersionHistoryModel { 56 | final String version; 57 | final String updateBody; 58 | VersionHistoryModel({required this.version, required this.updateBody}); 59 | } 60 | -------------------------------------------------------------------------------- /lib/modules/about/widgets/version_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:pure_live/common/index.dart'; 2 | import 'package:url_launcher/url_launcher.dart'; 3 | 4 | class NoNewVersionDialog extends StatelessWidget { 5 | const NoNewVersionDialog({ 6 | super.key, 7 | }); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return AlertDialog( 12 | title: Text(S.of(context).check_update), 13 | content: Text(S.of(context).no_new_version_info), 14 | actions: [ 15 | TextButton( 16 | child: Text(S.of(context).confirm), 17 | onPressed: () { 18 | Navigator.pop(context); 19 | }, 20 | ), 21 | ], 22 | ); 23 | } 24 | } 25 | 26 | class NewVersionDialog extends StatelessWidget { 27 | const NewVersionDialog({super.key, this.entry}); 28 | 29 | final OverlayEntry? entry; 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return AlertDialog( 34 | title: Text(S.of(context).check_update), 35 | content: Column( 36 | crossAxisAlignment: CrossAxisAlignment.start, 37 | mainAxisSize: MainAxisSize.min, 38 | children: [ 39 | Text(S.of(context).new_version_info(VersionUtil.latestVersion)), 40 | const SizedBox(height: 20), 41 | Text( 42 | VersionUtil.latestUpdateLog, 43 | style: Theme.of(context).textTheme.bodySmall, 44 | ), 45 | const SizedBox(height: 10), 46 | TextButton( 47 | onPressed: () { 48 | if (entry != null) { 49 | entry!.remove(); 50 | } else { 51 | Navigator.pop(context); 52 | } 53 | launchUrl( 54 | Uri.parse('https://www.123pan.com/s/Jucxjv-NwYYd.html'), 55 | mode: LaunchMode.externalApplication, 56 | ); 57 | }, 58 | child: const Text('本软件开源免费,国内下载:123云盘'), 59 | ) 60 | ], 61 | ), 62 | actions: [ 63 | TextButton( 64 | child: Text(S.of(context).cancel), 65 | onPressed: () { 66 | if (entry != null) { 67 | entry!.remove(); 68 | } else { 69 | Navigator.pop(context); 70 | } 71 | }, 72 | ), 73 | ElevatedButton( 74 | child: Text(S.of(context).update), 75 | onPressed: () { 76 | if (entry != null) { 77 | entry!.remove(); 78 | } else { 79 | Navigator.pop(context); 80 | } 81 | launchUrl( 82 | Uri.parse('https://github.com/liuchuancong/pure_live/releases'), 83 | mode: LaunchMode.externalApplication, 84 | ); 85 | }, 86 | ), 87 | ], 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/modules/account/account_bing.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/modules/account/account_controller.dart'; 3 | 4 | class AccountBinding extends Binding { 5 | @override 6 | List dependencies() { 7 | return [Bind.lazyPut(() => AccountController())]; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/modules/account/account_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/plugins/utils.dart'; 3 | import 'package:pure_live/routes/app_navigation.dart'; 4 | import 'package:pure_live/common/services/bilibili_account_service.dart'; 5 | 6 | class AccountController extends GetxController { 7 | void bilibiliTap() async { 8 | if (BiliBiliAccountService.instance.logined.value) { 9 | var result = await Utils.showAlertDialog("确定要退出哔哩哔哩账号吗?", title: "退出登录"); 10 | if (result) { 11 | BiliBiliAccountService.instance.logout(); 12 | } 13 | } else { 14 | AppNavigator.toBiliBiliLogin(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/modules/account/account_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:pure_live/common/services/bilibili_account_service.dart'; 4 | import 'package:pure_live/modules/account/account_controller.dart'; 5 | 6 | class AccountPage extends GetView { 7 | const AccountPage({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: AppBar( 13 | title: const Text("三方认证"), 14 | ), 15 | body: ListView( 16 | children: [ 17 | const Padding( 18 | padding: EdgeInsets.symmetric(vertical: 12), 19 | child: Text( 20 | "哔哩哔哩账号需要登录才能看高清晰度的直播,其他平台暂无此限制。", 21 | textAlign: TextAlign.center, 22 | ), 23 | ), 24 | Obx( 25 | () => ListTile( 26 | leading: Image.asset( 27 | 'assets/images/bilibili_2.png', 28 | width: 36, 29 | height: 36, 30 | ), 31 | title: const Text("哔哩哔哩"), 32 | subtitle: Text(BiliBiliAccountService.instance.name.value), 33 | trailing: BiliBiliAccountService.instance.logined.value 34 | ? const Icon(Icons.logout) 35 | : const Icon(Icons.chevron_right), 36 | onTap: controller.bilibiliTap, 37 | ), 38 | ), 39 | ListTile( 40 | leading: Image.asset( 41 | 'assets/images/douyu.png', 42 | width: 36, 43 | height: 36, 44 | ), 45 | title: const Text("斗鱼直播"), 46 | subtitle: const Text("尚不支持"), 47 | enabled: false, 48 | trailing: const Icon(Icons.chevron_right), 49 | ), 50 | ListTile( 51 | leading: Image.asset( 52 | 'assets/images/huya.png', 53 | width: 36, 54 | height: 36, 55 | ), 56 | title: const Text("虎牙直播"), 57 | subtitle: const Text("尚不支持"), 58 | enabled: false, 59 | trailing: const Icon(Icons.chevron_right), 60 | ), 61 | ListTile( 62 | leading: Image.asset( 63 | 'assets/images/douyin.png', 64 | width: 36, 65 | height: 36, 66 | ), 67 | title: const Text("抖音直播"), 68 | subtitle: const Text("尚不支持"), 69 | enabled: false, 70 | trailing: const Icon(Icons.chevron_right), 71 | ), 72 | ], 73 | ), 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/modules/account/bilibili/bilibili_bings.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/modules/account/bilibili/qr_login_controller.dart'; 3 | import 'package:pure_live/modules/account/bilibili/web_login_controller.dart'; 4 | 5 | class BilibiliWebLoginBinding extends Binding { 6 | @override 7 | List dependencies() { 8 | return [ 9 | Bind.lazyPut(() => BiliBiliWebLoginController()), 10 | ]; 11 | } 12 | } 13 | 14 | class BilibiliQrLoginBinding extends Binding { 15 | @override 16 | List dependencies() { 17 | return [ 18 | Bind.lazyPut(() => BiliBiliQRLoginController()), 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/modules/account/bilibili/web_login_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/routes/route_path.dart'; 3 | import 'package:pure_live/common/base/base_controller.dart'; 4 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 5 | import 'package:pure_live/common/services/bilibili_account_service.dart'; 6 | 7 | class BiliBiliWebLoginController extends BaseController { 8 | InAppWebViewController? webViewController; 9 | final CookieManager cookieManager = CookieManager.instance(); 10 | void onWebViewCreated(InAppWebViewController controller) { 11 | webViewController = controller; 12 | webViewController!.loadUrl( 13 | urlRequest: URLRequest( 14 | url: WebUri("https://passport.bilibili.com/login"), 15 | ), 16 | ); 17 | } 18 | 19 | void toQRLogin() async { 20 | await Get.offAndToNamed(RoutePath.kBiliBiliQRLogin); 21 | } 22 | 23 | void onLoadStop(InAppWebViewController controller, WebUri? uri) async { 24 | if (uri == null) { 25 | return; 26 | } 27 | if (uri.host == "m.bilibili.com") { 28 | var cookies = await cookieManager.getCookies(url: uri); 29 | var cookieStr = cookies.map((e) => "${e.name}=${e.value}").join(";"); 30 | BiliBiliAccountService.instance.setCookie(cookieStr); 31 | await BiliBiliAccountService.instance.loadUserInfo(); 32 | Get.back(result: true); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/modules/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:pure_live/modules/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: false, 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" || 36 | uri.host == "www.bilibili.com") { 37 | return NavigationActionPolicy.CANCEL; 38 | } 39 | return NavigationActionPolicy.ALLOW; 40 | }), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/modules/area_rooms/area_rooms_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/modules/area_rooms/area_rooms_controller.dart'; 3 | 4 | class AreaRoomsBinding extends Binding { 5 | @override 6 | List dependencies() { 7 | return [ 8 | Bind.lazyPut(() => AreaRoomsController( 9 | site: Get.arguments[0], 10 | subCategory: Get.arguments[1], 11 | )) 12 | ]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/modules/area_rooms/area_rooms_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:pure_live/core/sites.dart'; 2 | import 'package:pure_live/common/models/live_area.dart'; 3 | import 'package:pure_live/common/models/live_room.dart'; 4 | import 'package:pure_live/common/base/base_controller.dart'; 5 | 6 | class AreaRoomsController extends BasePageController { 7 | final Site site; 8 | final LiveArea subCategory; 9 | 10 | AreaRoomsController({ 11 | required this.site, 12 | required this.subCategory, 13 | }); 14 | 15 | @override 16 | Future> getData(int page, int pageSize) async { 17 | var result = await site.liveSite.getCategoryRooms(subCategory, page: page); 18 | for (var element in result.items) { 19 | element.area = subCategory.areaName; 20 | } 21 | return result.items; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/modules/areas/areas_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/common/index.dart'; 3 | import 'package:pure_live/modules/areas/areas_list_controller.dart'; 4 | 5 | class AreasController extends GetxController with GetSingleTickerProviderStateMixin { 6 | late TabController tabController; 7 | int index = 0; 8 | final isCustomSite = false.obs; 9 | AreasController() { 10 | final preferPlatform = Get.find().preferPlatform.value; 11 | final pIndex = Sites().availableSites().indexWhere((e) => e.id == preferPlatform); 12 | tabController = TabController( 13 | initialIndex: pIndex == -1 ? 0 : pIndex, 14 | length: Sites().availableSites().length, 15 | vsync: this, 16 | ); 17 | index = pIndex == -1 ? 0 : pIndex; 18 | tabController.animation?.addListener(() { 19 | var currentIndex = (tabController.animation?.value ?? 0).round(); 20 | if (index == currentIndex) { 21 | return; 22 | } 23 | index = currentIndex; 24 | var controller = Get.find(tag: Sites().availableSites()[index].id); 25 | isCustomSite.value = controller.site.id == 'custom'; 26 | if (controller.list.isEmpty) { 27 | controller.loadData(); 28 | } 29 | }); 30 | } 31 | 32 | @override 33 | void onInit() async { 34 | for (var site in Sites().availableSites()) { 35 | Get.put(AreasListController(site), tag: site.id); 36 | var controller = Get.find(tag: site.id); 37 | if (controller.list.isEmpty) { 38 | controller.loadData(); 39 | } 40 | } 41 | 42 | super.onInit(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/modules/areas/areas_grid_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/common/index.dart'; 3 | import 'package:pure_live/modules/areas/widgets/area_card.dart'; 4 | import 'package:pure_live/modules/areas/areas_list_controller.dart'; 5 | import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; 6 | 7 | class AreaGridView extends StatefulWidget { 8 | final String tag; 9 | const AreaGridView(this.tag, {super.key}); 10 | AreasListController get controller => Get.find(tag: tag); 11 | @override 12 | State createState() => _AreaGridViewState(); 13 | } 14 | 15 | class _AreaGridViewState extends State with SingleTickerProviderStateMixin { 16 | late TabController tabController = TabController(length: widget.controller.list.length, vsync: this); 17 | 18 | @override 19 | void initState() { 20 | widget.controller.tabIndex.addListener(() { 21 | tabController.animateTo(widget.controller.tabIndex.value); 22 | }); 23 | super.initState(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Column( 29 | children: [ 30 | TabBar( 31 | controller: tabController, 32 | isScrollable: true, 33 | tabAlignment: TabAlignment.center, 34 | indicatorSize: TabBarIndicatorSize.label, 35 | tabs: widget.controller.list.map((e) => Tab(text: e.name)).toList(), 36 | ), 37 | Expanded( 38 | child: Obx(() => TabBarView( 39 | controller: tabController, 40 | children: widget.controller.list.map((e) => buildAreasView(e)).toList(), 41 | )), 42 | ), 43 | ], 44 | ); 45 | } 46 | 47 | Widget buildAreasView(AppLiveCategory category) { 48 | return LayoutBuilder(builder: (context, constraint) { 49 | final width = constraint.maxWidth; 50 | final crossAxisCount = width > 1280 ? 9 : (width > 960 ? 7 : (width > 640 ? 5 : 3)); 51 | return widget.controller.list.isNotEmpty 52 | ? MasonryGridView.count( 53 | padding: const EdgeInsets.all(5), 54 | controller: ScrollController(), 55 | crossAxisCount: crossAxisCount, 56 | itemCount: category.children.length, 57 | itemBuilder: (context, index) => AreaCard(category: category.children[index]), 58 | ) 59 | : EmptyView( 60 | icon: Icons.area_chart_outlined, 61 | title: S.of(context).empty_areas_title, 62 | subtitle: S.of(context).empty_areas_subtitle, 63 | ); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/modules/areas/areas_list_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:get/get.dart'; 3 | import 'package:pure_live/common/index.dart'; 4 | import 'package:pure_live/model/live_category.dart'; 5 | import 'package:pure_live/common/base/base_controller.dart'; 6 | 7 | class AreasListController extends BasePageController { 8 | final Site site; 9 | final tabIndex = 0.obs; 10 | AreasListController(this.site); 11 | 12 | @override 13 | Future> getData(int page, int pageSize) async { 14 | var result = await site.liveSite.getCategores(page, pageSize); 15 | return result.map((e) => AppLiveCategory.fromLiveCategory(e)).toList(); 16 | } 17 | } 18 | 19 | class AppLiveCategory extends LiveCategory { 20 | var showAll = false.obs; 21 | AppLiveCategory({ 22 | required super.id, 23 | required super.name, 24 | required super.children, 25 | }) { 26 | showAll.value = children.length < 19; 27 | } 28 | 29 | List get take15 => children.take(15).toList(); 30 | 31 | factory AppLiveCategory.fromLiveCategory(LiveCategory item) { 32 | return AppLiveCategory( 33 | children: item.children, 34 | id: item.id, 35 | name: item.name, 36 | ); 37 | } 38 | Map toJson() { 39 | Map json = {}; 40 | json['id'] = id; 41 | json['name'] = name; 42 | json['children'] = children.map((LiveArea e) => jsonEncode(e.toJson())).toList(); 43 | return json; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/modules/areas/areas_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'areas_grid_view.dart'; 3 | import 'favorite_areas_page.dart'; 4 | import 'package:pure_live/common/index.dart'; 5 | 6 | class AreasPage extends GetView { 7 | const AreasPage({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return LayoutBuilder(builder: (context, constraint) { 12 | bool showAction = constraint.maxWidth <= 680; 13 | return Scaffold( 14 | appBar: AppBar( 15 | centerTitle: true, 16 | scrolledUnderElevation: 0, 17 | leading: showAction ? const MenuButton() : null, 18 | actions: showAction ? [const SearchButton()] : null, 19 | title: TabBar( 20 | controller: controller.tabController, 21 | isScrollable: true, 22 | tabAlignment: TabAlignment.center, 23 | labelStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), 24 | labelPadding: const EdgeInsets.symmetric(horizontal: 12), 25 | indicatorSize: TabBarIndicatorSize.label, 26 | tabs: Sites().availableSites().map((e) => Tab(text: e.name)).toList(), 27 | ), 28 | ), 29 | body: TabBarView( 30 | controller: controller.tabController, 31 | children: Sites().availableSites().map((e) => AreaGridView(e.id)).toList(), 32 | ), 33 | floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, 34 | floatingActionButton: FloatingActionButton( 35 | onPressed: () { 36 | Get.to(() => const FavoriteAreasPage()); 37 | }, 38 | child: const Icon(Icons.favorite_rounded), 39 | ), 40 | ); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/modules/areas/favorite_areas_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:pure_live/common/index.dart'; 4 | import 'package:pure_live/modules/areas/widgets/area_card.dart'; 5 | 6 | class FavoriteAreasPage extends GetView { 7 | const FavoriteAreasPage({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return LayoutBuilder(builder: (context, constraint) { 12 | final width = constraint.maxWidth; 13 | final crossAxisCount = 14 | width > 1280 ? 9 : (width > 960 ? 7 : (width > 640 ? 5 : 3)); 15 | return Scaffold( 16 | appBar: AppBar(title: Text(S.of(context).favorite_areas)), 17 | body: Obx( 18 | () => controller.favoriteAreas.isNotEmpty 19 | ? MasonryGridView.count( 20 | padding: const EdgeInsets.all(5), 21 | crossAxisCount: crossAxisCount, 22 | itemCount: controller.favoriteAreas.length, 23 | itemBuilder: (context, index) => 24 | AreaCard(category: controller.favoriteAreas[index])) 25 | : EmptyView( 26 | icon: Icons.area_chart_outlined, 27 | title: S.of(context).empty_areas_title, 28 | subtitle: '', 29 | ), 30 | ), 31 | ); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/modules/auth/auth_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:get/get.dart'; 3 | import 'package:pure_live/common/index.dart'; 4 | import 'package:supabase_flutter/supabase_flutter.dart'; 5 | 6 | class AuthController extends GetxController { 7 | final supabaseClient = SupaBaseManager().client; 8 | bool shouldGoReset = false; 9 | late bool isLogin = false; 10 | late User user; 11 | String userId = ''; 12 | @override 13 | void onInit() { 14 | super.onInit(); 15 | supabaseClient.auth.onAuthStateChange.listen((data) async { 16 | final AuthChangeEvent event = data.event; 17 | final Session? session = data.session; 18 | if (session?.user != null) { 19 | isLogin = true; 20 | userId = data.session!.user.id; 21 | user = session!.user; 22 | final SettingsService service = Get.find(); 23 | await SupaBaseManager().loadUploadConfig(); 24 | bool wantLoad = service.favoriteRooms.isEmpty; 25 | if (wantLoad) { 26 | SupaBaseManager().readConfig(); 27 | } 28 | } else { 29 | isLogin = false; 30 | userId = ''; 31 | } 32 | if (event == AuthChangeEvent.passwordRecovery && shouldGoReset) { 33 | Timer(const Duration(seconds: 2), () { 34 | shouldGoReset = false; 35 | Get.offAndToNamed(RoutePath.kUpdatePassword); 36 | }); 37 | } 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/modules/auth/components/update_password.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/common/index.dart'; 3 | import 'package:supabase_flutter/supabase_flutter.dart'; 4 | import 'package:pure_live/modules/auth/components/supa_reset_password.dart'; 5 | 6 | class UpdatePassword extends StatelessWidget { 7 | const UpdatePassword({super.key}); 8 | 9 | AppBar appBar(String title) => AppBar( 10 | title: Text(title), 11 | automaticallyImplyLeading: false, 12 | ); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | appBar: appBar('更新密码'), 18 | body: Padding( 19 | padding: const EdgeInsets.all(24.0), 20 | child: Column( 21 | children: [ 22 | SupaResetPassword( 23 | accessToken: Supabase.instance.client.auth.currentSession!.accessToken, 24 | onSuccess: (response) { 25 | Supabase.instance.client.auth.refreshSession(); 26 | SmartDialog.showToast(S.of(context).supabase_sign_success); 27 | Get.offAllNamed(RoutePath.kInitial); 28 | }, 29 | ), 30 | TextButton( 31 | child: Text( 32 | S.of(context).supabase_back_sign_in, 33 | style: const TextStyle(fontWeight: FontWeight.bold), 34 | ), 35 | onPressed: () { 36 | Get.offAllNamed(RoutePath.kSignIn); 37 | }, 38 | ), 39 | ], 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/modules/auth/mine_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/common/index.dart'; 3 | 4 | class MinePage extends StatefulWidget { 5 | const MinePage({super.key}); 6 | 7 | @override 8 | State createState() => _MinePageState(); 9 | } 10 | 11 | class _MinePageState extends State { 12 | void uploadUserConifg() { 13 | SupaBaseManager().uploadConfig(); 14 | } 15 | 16 | void downloadUserConifg() { 17 | SupaBaseManager().readConfig(); 18 | } 19 | 20 | void singOut() { 21 | SupaBaseManager().signOut(); 22 | } 23 | 24 | bool isManager() { 25 | final AuthController authController = Get.find(); 26 | if (!authController.isLogin) return false; 27 | return SupaBaseManager.supabasePolicy.owner == authController.user.id; 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return Scaffold( 33 | appBar: AppBar( 34 | title: Text(S.of(context).supabase_mine), 35 | ), 36 | body: ListView(scrollDirection: Axis.vertical, physics: const BouncingScrollPhysics(), children: [ 37 | Padding( 38 | padding: const EdgeInsets.all(24.0), 39 | child: Column( 40 | children: [ 41 | if (isManager()) 42 | ListTile( 43 | leading: const Icon(Icons.verified_user_outlined, size: 32), 44 | subtitle: const Text("允许用户是否可以上传"), 45 | title: const Text("用户管理"), 46 | onTap: () => Get.toNamed(RoutePath.kUserManage), 47 | ), 48 | ListTile( 49 | leading: const Icon(Icons.sim_card_download_outlined, size: 32), 50 | subtitle: Text(S.of(context).supabase_mine_streams), 51 | title: const Text('下载用户配置'), 52 | onTap: downloadUserConifg, 53 | ), 54 | ListTile( 55 | leading: const Icon(Icons.upload_file_outlined, size: 32), 56 | subtitle: Text(S.of(context).supabase_mine_streams), 57 | title: Text(S.of(context).supabase_mine_profiles), 58 | onTap: uploadUserConifg, 59 | ), 60 | ListTile( 61 | leading: const Icon(Icons.login_outlined, size: 32), 62 | title: Text(S.of(context).supabase_log_out), 63 | onTap: singOut, 64 | ), 65 | ], 66 | ), 67 | ), 68 | ]), 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/modules/auth/sign_in_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:pure_live/common/index.dart'; 4 | import 'package:supabase_flutter/supabase_flutter.dart'; 5 | import 'package:pure_live/modules/auth/components/supa_email_auth.dart'; 6 | 7 | class SignInPage extends StatefulWidget { 8 | const SignInPage({super.key}); 9 | 10 | @override 11 | State createState() => _SignInPageState(); 12 | } 13 | 14 | class _SignInPageState extends State { 15 | @override 16 | Widget build(BuildContext context) { 17 | return Scaffold( 18 | appBar: AppBar( 19 | title: Text(S.of(context).supabase_sign_in), 20 | ), 21 | body: SingleChildScrollView( 22 | scrollDirection: Axis.vertical, 23 | child: Padding( 24 | padding: const EdgeInsets.all(24.0), 25 | child: Column( 26 | children: [ 27 | SupaEmailAuth( 28 | redirectTo: kIsWeb ? null : 'purelive://signin', 29 | onPasswordResetEmailSent: () { 30 | final AuthController authController = Get.find(); 31 | authController.shouldGoReset = true; 32 | SmartDialog.showToast('请打开邮箱重置密码', animationTime: const Duration(seconds: 2)); 33 | }, 34 | onSignInComplete: (AuthResponse response) { 35 | SmartDialog.showToast(S.of(context).supabase_sign_success); 36 | Get.offAllNamed(RoutePath.kInitial); 37 | }, 38 | onSignUpComplete: (AuthResponse response) { 39 | SmartDialog.showToast(S.of(context).supabase_sign_confirm); 40 | }, 41 | ), 42 | ], 43 | ), 44 | ), 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/modules/auth/utils/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:supabase_flutter/supabase_flutter.dart'; 3 | 4 | final supabase = Supabase.instance.client; 5 | 6 | SizedBox spacer(double height) { 7 | return SizedBox( 8 | height: height, 9 | ); 10 | } 11 | 12 | /// Set of extension methods to easily display a snackbar 13 | extension ShowSnackBar on BuildContext { 14 | /// Displays a basic snackbar 15 | void showSnackBar( 16 | String message, { 17 | Color? textColor, 18 | Color? backgroundColor, 19 | String? actionLabel, 20 | }) { 21 | ScaffoldMessenger.of(this).showSnackBar(SnackBar( 22 | content: Text( 23 | message, 24 | style: textColor == null ? null : TextStyle(color: textColor), 25 | ), 26 | backgroundColor: backgroundColor, 27 | action: SnackBarAction( 28 | label: actionLabel ?? 'ok', 29 | onPressed: () {}, 30 | ), 31 | )); 32 | } 33 | 34 | /// Displays a red snackbar indicating error 35 | void showErrorSnackBar( 36 | String message, { 37 | String? actionLabel, 38 | }) { 39 | showSnackBar( 40 | message, 41 | textColor: Theme.of(this).colorScheme.onErrorContainer, 42 | backgroundColor: Theme.of(this).colorScheme.errorContainer, 43 | actionLabel: actionLabel, 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/modules/auth/utils/supa_auth_action.dart: -------------------------------------------------------------------------------- 1 | enum SupaAuthAction { signIn, signUp } 2 | -------------------------------------------------------------------------------- /lib/modules/contact/contact_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:pure_live/common/index.dart'; 3 | import 'package:url_launcher/url_launcher.dart'; 4 | 5 | class ContactPage extends StatefulWidget { 6 | const ContactPage({super.key}); 7 | 8 | @override 9 | State createState() => _ContactPageState(); 10 | } 11 | 12 | class _ContactPageState extends State { 13 | void clipboard(String text) { 14 | Clipboard.setData(ClipboardData(text: text)).then((value) => SnackBarUtil.success('已复制到剪贴板')); 15 | } 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Scaffold( 20 | appBar: AppBar(), 21 | body: ListView( 22 | physics: const BouncingScrollPhysics(), 23 | children: [ 24 | SectionTitle(title: S.of(context).contact), 25 | ListTile( 26 | leading: const Icon(CustomIcons.mail_squared, size: 34), 27 | title: Text(S.of(context).email), 28 | subtitle: const Text(VersionUtil.email), 29 | onLongPress: () => clipboard(VersionUtil.email), 30 | onTap: () { 31 | launchUrl( 32 | Uri.parse(VersionUtil.emailUrl), 33 | mode: LaunchMode.externalApplication, 34 | ); 35 | }, 36 | ), 37 | ListTile( 38 | leading: const Icon(CustomIcons.github_circled, size: 32), 39 | title: Text(S.of(context).github), 40 | subtitle: const Text(VersionUtil.githubUrl), 41 | onTap: () { 42 | launchUrl( 43 | Uri.parse(VersionUtil.githubUrl), 44 | mode: LaunchMode.externalApplication, 45 | ); 46 | }, 47 | ), 48 | ], 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/modules/favorite/favorite_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer'; 3 | import 'package:get/get.dart'; 4 | import 'package:pure_live/common/index.dart'; 5 | 6 | class FavoriteController extends GetxController with GetSingleTickerProviderStateMixin { 7 | final SettingsService settings = Get.find(); 8 | late TabController tabController; 9 | final tabBottomIndex = 0.obs; 10 | bool isFirstLoad = true; 11 | FavoriteController() { 12 | tabController = TabController(length: 2, vsync: this); 13 | } 14 | 15 | @override 16 | void onInit() { 17 | super.onInit(); 18 | // 初始化关注页 19 | syncRooms(); 20 | // 监听settings rooms变化 21 | settings.favoriteRooms.listen((rooms) => syncRooms()); 22 | onRefresh(); 23 | // 定时自动刷新 24 | if (settings.autoRefreshTime.value != 0) { 25 | Timer.periodic( 26 | Duration(minutes: settings.autoRefreshTime.value), 27 | (timer) => onRefresh(), 28 | ); 29 | } 30 | } 31 | 32 | final onlineRooms = [].obs; 33 | final offlineRooms = [].obs; 34 | 35 | void syncRooms() { 36 | onlineRooms.clear(); 37 | offlineRooms.clear(); 38 | onlineRooms.addAll(settings.favoriteRooms.where((room) => room.liveStatus == LiveStatus.live)); 39 | 40 | offlineRooms.addAll(settings.favoriteRooms.where((room) => room.liveStatus != LiveStatus.live)); 41 | } 42 | 43 | Future onRefresh() async { 44 | // 自动刷新时间为0关闭。不是手动刷新并且不是第一次刷新 45 | if (isFirstLoad) { 46 | await const Duration(seconds: 1).delay(); 47 | } 48 | bool hasError = false; 49 | List> futures = []; 50 | if (settings.favoriteRooms.value.isEmpty) return false; 51 | for (final room in settings.favoriteRooms.value) { 52 | futures.add(Sites.of(room.platform!).liveSite.getRoomDetail(roomId: room.roomId!)); 53 | } 54 | try { 55 | final rooms = await Future.wait(futures); 56 | for (var room in rooms) { 57 | settings.updateRoom(room); 58 | } 59 | syncRooms(); 60 | } catch (e) { 61 | hasError = true; 62 | log(e.toString(), name: 'syncRooms'); 63 | } 64 | isFirstLoad = false; 65 | return hasError; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/modules/history/history_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/common/index.dart'; 3 | import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; 4 | 5 | class HistoryPage extends GetView { 6 | HistoryPage({super.key}); 7 | 8 | final refreshController = EasyRefreshController( 9 | controlFinishRefresh: true, 10 | controlFinishLoad: true, 11 | ); 12 | 13 | Future onRefresh() async { 14 | bool result = true; 15 | final SettingsService settings = Get.find(); 16 | 17 | for (final room in settings.historyRooms) { 18 | try { 19 | var newRoom = await Sites.of(room.platform!).liveSite.getRoomDetail(roomId: room.roomId!); 20 | settings.updateRoomInHistory(newRoom); 21 | } catch (e) { 22 | result = false; 23 | } 24 | } 25 | if (result) { 26 | refreshController.finishRefresh(IndicatorResult.success); 27 | refreshController.resetFooter(); 28 | } else { 29 | refreshController.finishRefresh(IndicatorResult.fail); 30 | } 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return Scaffold( 36 | appBar: AppBar( 37 | centerTitle: true, 38 | scrolledUnderElevation: 0, 39 | title: Text('${S.of(context).history}(20)'), 40 | ), 41 | body: Obx(() { 42 | final SettingsService settings = Get.find(); 43 | const dense = true; 44 | final rooms = settings.historyRooms.reversed.toList(); 45 | return LayoutBuilder(builder: (context, constraint) { 46 | final width = constraint.maxWidth; 47 | int crossAxisCount = width > 1280 ? 4 : (width > 960 ? 3 : (width > 640 ? 2 : 1)); 48 | if (dense) { 49 | crossAxisCount = width > 1280 ? 5 : (width > 960 ? 4 : (width > 640 ? 3 : 2)); 50 | } 51 | return EasyRefresh( 52 | controller: refreshController, 53 | onRefresh: onRefresh, 54 | onLoad: () { 55 | refreshController.finishLoad(IndicatorResult.noMore); 56 | }, 57 | child: rooms.isEmpty 58 | ? EmptyView( 59 | icon: Icons.history_rounded, 60 | title: S.of(context).empty_history, 61 | subtitle: '', 62 | ) 63 | : MasonryGridView.count( 64 | padding: const EdgeInsets.all(5), 65 | controller: ScrollController(), 66 | crossAxisCount: crossAxisCount, 67 | itemCount: rooms.length, 68 | itemBuilder: (context, index) => RoomCard( 69 | room: rooms[index], 70 | dense: dense, 71 | ), 72 | ), 73 | ); 74 | }); 75 | }), 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/modules/home/mobile_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:pure_live/common/index.dart'; 2 | 3 | class HomeMobileView extends StatelessWidget { 4 | final Widget body; 5 | final int index; 6 | final void Function(int) onDestinationSelected; 7 | final void Function()? onFavoriteDoubleTap; 8 | const HomeMobileView({ 9 | super.key, 10 | required this.body, 11 | required this.index, 12 | required this.onDestinationSelected, 13 | required this.onFavoriteDoubleTap, 14 | }); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Scaffold( 19 | bottomNavigationBar: NavigationBar( 20 | destinations: [ 21 | NavigationDestination( 22 | icon: GestureDetector( 23 | onDoubleTap: onFavoriteDoubleTap, 24 | child: const Icon(Icons.favorite_rounded), 25 | ), 26 | label: S.of(context).favorites_title, 27 | ), 28 | NavigationDestination( 29 | icon: const Icon(CustomIcons.popular), 30 | label: S.of(context).popular_title, 31 | ), 32 | NavigationDestination( 33 | icon: const Icon(Icons.area_chart_rounded), 34 | label: S.of(context).areas_title, 35 | ), 36 | ], 37 | selectedIndex: index, 38 | onDestinationSelected: onDestinationSelected, 39 | ), 40 | body: body, 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/modules/home/tablet_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/common/index.dart'; 3 | import 'package:pure_live/modules/search/search_controller.dart' as pure_live; 4 | 5 | class HomeTabletView extends StatelessWidget { 6 | final Widget body; 7 | final int index; 8 | final void Function(int) onDestinationSelected; 9 | 10 | const HomeTabletView({ 11 | super.key, 12 | required this.body, 13 | required this.index, 14 | required this.onDestinationSelected, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Scaffold( 20 | body: SafeArea( 21 | child: Row( 22 | children: [ 23 | NavigationRail( 24 | groupAlignment: 0.9, 25 | labelType: NavigationRailLabelType.all, 26 | leading: Column( 27 | mainAxisSize: MainAxisSize.min, 28 | children: [ 29 | const Padding( 30 | padding: EdgeInsets.all(12), 31 | child: MenuButton(), 32 | ), 33 | FloatingActionButton( 34 | heroTag: 'search', 35 | elevation: 0, 36 | onPressed: () { 37 | Get.put(pure_live.SearchController()); 38 | onDestinationSelected(3); 39 | }, 40 | child: const Icon(CustomIcons.search), 41 | ), 42 | ], 43 | ), 44 | destinations: [ 45 | NavigationRailDestination( 46 | icon: const Icon(Icons.favorite_rounded), 47 | label: Text(S.of(context).favorites_title), 48 | ), 49 | NavigationRailDestination( 50 | icon: const Icon(CustomIcons.popular), 51 | label: Text(S.of(context).popular_title), 52 | ), 53 | NavigationRailDestination( 54 | icon: const Icon(Icons.area_chart_rounded), 55 | label: Text(S.of(context).areas_title), 56 | ), 57 | ], 58 | selectedIndex: index > 2 ? 0 : index, 59 | onDestinationSelected: onDestinationSelected, 60 | ), 61 | const VerticalDivider(width: 1), 62 | Expanded(child: body), 63 | ], 64 | ), 65 | ), 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/modules/hot_areas/hot_areas_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/modules/hot_areas/hot_areas_controller.dart'; 3 | 4 | class HotAreasBinding extends Binding { 5 | @override 6 | List dependencies() { 7 | return [Bind.lazyPut(() => HotAreasController())]; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/modules/hot_areas/hot_areas_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/common/index.dart'; 3 | import 'package:pure_live/common/base/base_controller.dart'; 4 | 5 | class HotAreasController extends BaseController { 6 | final SettingsService settingsController = Get.find(); 7 | final sites = [].obs; 8 | @override 9 | void onInit() { 10 | for (var element in Sites.supportSites) { 11 | var show = settingsController.hotAreasList.value.contains(element.id); 12 | var area = HotAreasModel(id: element.id, name: element.name, show: show); 13 | sites.add(area); 14 | } 15 | super.onInit(); 16 | } 17 | 18 | Color get themeColor => HexColor(settingsController.themeColorSwitch.value); 19 | 20 | void onChanged(id, value) { 21 | var index = sites.map((element) => element.id).toList().indexWhere((note) => note == id); 22 | HotAreasModel origin = sites[index]; 23 | sites.removeAt(index); 24 | sites.insert(index, HotAreasModel(id: origin.id, name: origin.name, show: !origin.show)); 25 | SmartDialog.showToast('重启后生效'); 26 | settingsController.hotAreasList.value = sites.where((p0) => p0.show).map((e) => e.id.toString()).toList(); 27 | } 28 | } 29 | 30 | class HotAreasModel { 31 | String id; 32 | String name; 33 | bool show; 34 | 35 | HotAreasModel({required this.id, required this.name, required this.show}); 36 | } 37 | -------------------------------------------------------------------------------- /lib/modules/hot_areas/hot_areas_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/common/index.dart'; 3 | import 'package:pure_live/modules/hot_areas/hot_areas_controller.dart'; 4 | 5 | class HotAreasPage extends GetView { 6 | const HotAreasPage({super.key}); 7 | 8 | _initListData() { 9 | return controller.sites.map((e) { 10 | return SwitchListTile( 11 | title: Text(e.name), 12 | value: e.show, 13 | activeColor: Theme.of(Get.context!).colorScheme.primary, 14 | onChanged: (bool value) => controller.onChanged(e.id, value)); 15 | }).toList(); 16 | } 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Scaffold( 21 | appBar: AppBar( 22 | title: const Text("平台显示"), 23 | ), 24 | body: Obx(() => ListView( 25 | padding: const EdgeInsets.all(12.0), 26 | children: _initListData(), 27 | )), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/modules/live_play/live_play_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/modules/live_play/live_play_controller.dart'; 3 | 4 | class LivePlayBinding extends Binding { 5 | @override 6 | List dependencies() { 7 | return [ 8 | Bind.lazyPut(() => LivePlayController( 9 | room: Get.arguments, 10 | site: Get.parameters["site"] ?? "", 11 | )) 12 | ]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/modules/live_play/widgets/index.dart: -------------------------------------------------------------------------------- 1 | library widgets; 2 | 3 | export './video_player/video_controller.dart'; 4 | export './video_player/video_player.dart'; 5 | export './danmaku_list_view.dart'; 6 | export './live_dlna_dialog.dart'; 7 | -------------------------------------------------------------------------------- /lib/modules/live_play/widgets/video_player/danmaku_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:pure_live/common/index.dart'; 2 | import 'package:bordered_text/bordered_text.dart'; 3 | 4 | class DanmakuText extends StatelessWidget { 5 | final String text; 6 | final TextAlign textAlign; 7 | final Color color; 8 | final double fontSize; 9 | final double strokeWidth; 10 | 11 | const DanmakuText( 12 | this.text, { 13 | this.textAlign = TextAlign.left, 14 | this.color = Colors.white, 15 | this.fontSize = 16, 16 | this.strokeWidth = 2.0, 17 | super.key, 18 | }); 19 | 20 | Color get borderColor { 21 | var brightness = ((color.red * 299) + (color.green * 587) + (color.blue * 114)) / 1000; 22 | return brightness > 70 ? Colors.black54 : Colors.white54; 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return BorderedText( 28 | strokeWidth: strokeWidth, 29 | strokeCap: StrokeCap.round, 30 | strokeJoin: StrokeJoin.round, 31 | strokeColor: borderColor, 32 | child: Text( 33 | text, 34 | softWrap: false, 35 | textAlign: textAlign, 36 | style: TextStyle( 37 | decoration: TextDecoration.none, 38 | fontSize: fontSize, 39 | color: color, 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/modules/popular/popular_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import '../../core/sites.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:pure_live/common/services/settings_service.dart'; 5 | import 'package:pure_live/modules/popular/popular_grid_controller.dart'; 6 | 7 | class PopularController extends GetxController with GetSingleTickerProviderStateMixin { 8 | late TabController tabController; 9 | int index = 0; 10 | 11 | PopularController() { 12 | final preferPlatform = Get.find().preferPlatform.value; 13 | final pIndex = Sites().availableSites().indexWhere((e) => e.id == preferPlatform); 14 | tabController = TabController( 15 | initialIndex: pIndex == -1 ? 0 : pIndex, 16 | length: Sites().availableSites().length, 17 | vsync: this, 18 | ); 19 | index = pIndex == -1 ? 0 : pIndex; 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 | var controller = Get.find(tag: Sites().availableSites()[index].id); 29 | 30 | if (controller.list.isEmpty) { 31 | controller.loadData(); 32 | } 33 | }); 34 | } 35 | 36 | @override 37 | void onInit() { 38 | for (var site in Sites().availableSites()) { 39 | var controller = Get.put(PopularGridController(site), tag: site.id); 40 | if (controller.list.isEmpty) { 41 | controller.loadData(); 42 | } 43 | } 44 | super.onInit(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/modules/popular/popular_grid_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:pure_live/common/base/base_controller.dart'; 4 | import 'package:pure_live/common/index.dart'; 5 | 6 | class PopularGridController extends BasePageController { 7 | final Site site; 8 | 9 | PopularGridController(this.site); 10 | 11 | @override 12 | Future> getData(int page, int pageSize) async { 13 | var result = await site.liveSite.getRecommendRooms(page: page); 14 | if (site.id == 'iptv' && list.isNotEmpty) { 15 | return []; 16 | } 17 | return result.items; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/modules/popular/popular_grid_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/common/index.dart'; 3 | import 'package:pure_live/modules/popular/popular_grid_controller.dart'; 4 | import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; 5 | 6 | class PopularGridView extends StatefulWidget { 7 | final String tag; 8 | 9 | const PopularGridView(this.tag, {super.key}); 10 | 11 | @override 12 | State createState() => _PopularGridViewState(); 13 | } 14 | 15 | class _PopularGridViewState extends State { 16 | PopularGridController get controller => Get.find(tag: widget.tag); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return LayoutBuilder( 21 | builder: (context, constraint) { 22 | final width = constraint.maxWidth; 23 | final crossAxisCount = width > 1280 ? 5 : (width > 960 ? 4 : (width > 640 ? 3 : 2)); 24 | return Obx(() => EasyRefresh( 25 | controller: controller.easyRefreshController, 26 | onRefresh: controller.refreshData, 27 | onLoad: controller.loadData, 28 | child: controller.list.isNotEmpty 29 | ? MasonryGridView.count( 30 | padding: const EdgeInsets.all(5), 31 | controller: controller.scrollController, 32 | crossAxisCount: crossAxisCount, 33 | itemCount: controller.list.length, 34 | itemBuilder: (context, index) => RoomCard(room: controller.list[index], dense: true), 35 | ) 36 | : EmptyView( 37 | icon: Icons.live_tv_rounded, 38 | title: S.of(context).empty_live_title, 39 | subtitle: S.of(context).empty_live_subtitle, 40 | ), 41 | )); 42 | }, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/modules/popular/popular_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'popular_grid_view.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:pure_live/core/sites.dart'; 5 | import 'package:pure_live/common/widgets/index.dart'; 6 | import 'package:pure_live/modules/popular/popular_controller.dart'; 7 | 8 | class PopularPage extends GetView { 9 | const PopularPage({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return LayoutBuilder(builder: (context, constraint) { 14 | bool showAction = constraint.maxWidth <= 680; 15 | return Scaffold( 16 | appBar: AppBar( 17 | centerTitle: true, 18 | scrolledUnderElevation: 0, 19 | leading: showAction ? const MenuButton() : null, 20 | actions: showAction ? [const SearchButton()] : null, 21 | title: TabBar( 22 | controller: controller.tabController, 23 | isScrollable: true, 24 | tabAlignment: TabAlignment.center, 25 | labelStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), 26 | labelPadding: const EdgeInsets.symmetric(horizontal: 12), 27 | indicatorSize: TabBarIndicatorSize.label, 28 | tabs: Sites().availableSites().map((e) => Tab(text: e.name)).toList(), 29 | ), 30 | ), 31 | body: TabBarView( 32 | controller: controller.tabController, 33 | children: Sites().availableSites().map((e) => PopularGridView(e.id)).toList(), 34 | ), 35 | ); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/modules/search/search_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import 'search_controller.dart'; 4 | 5 | class SearchBinding extends Binding { 6 | @override 7 | List dependencies() { 8 | return [Bind.lazyPut(() => SearchController())]; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/modules/search/search_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import '../../core/sites.dart'; 3 | import 'search_list_controller.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class SearchController extends GetxController with GetSingleTickerProviderStateMixin { 7 | late TabController tabController; 8 | int index = 0; 9 | 10 | SearchController() { 11 | tabController = TabController( 12 | length: Sites().availableSites().length, 13 | vsync: this, 14 | ); 15 | tabController.animation?.addListener(() { 16 | var currentIndex = (tabController.animation?.value ?? 0).round(); 17 | if (index == currentIndex) { 18 | return; 19 | } 20 | index = currentIndex; 21 | var controller = Get.find(tag: Sites().availableSites()[index].id); 22 | if (controller.list.isEmpty && !controller.pageEmpty.value) { 23 | controller.refreshData(); 24 | } 25 | controller.keyword.addListener(() { 26 | searchController.text = controller.keyword.value; 27 | }); 28 | }); 29 | } 30 | 31 | TextEditingController searchController = TextEditingController(); 32 | 33 | @override 34 | void onInit() { 35 | for (var site in Sites().availableSites()) { 36 | Get.put(SearchListController(site), tag: site.id); 37 | } 38 | 39 | super.onInit(); 40 | } 41 | 42 | void doSearch() { 43 | if (searchController.text.isEmpty) { 44 | return; 45 | } 46 | for (var site in Sites().availableSites()) { 47 | var controller = Get.find(tag: site.id); 48 | controller.clear(); 49 | controller.keyword.value = searchController.text; 50 | } 51 | var controller = Get.find(tag: Sites().availableSites()[index].id); 52 | controller.loadData(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/modules/search/search_list_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/core/sites.dart'; 3 | import 'package:pure_live/common/base/base_controller.dart'; 4 | 5 | class SearchListController extends BasePageController { 6 | final keyword = "".obs; 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.value.isEmpty) { 18 | return; 19 | } 20 | return await super.refreshData(); 21 | } 22 | 23 | @override 24 | Future getData(int page, int pageSize) async { 25 | if (keyword.value.isEmpty) { 26 | return []; 27 | } 28 | if (searchMode.value == 1) { 29 | // 搜索主播 30 | var result = await site.liveSite.searchAnchors(keyword.value, page: page); 31 | return result.items; 32 | } 33 | var result = await site.liveSite.searchRooms(keyword.value, page: page); 34 | return result.items; 35 | } 36 | 37 | void clear() { 38 | currentPage = 1; 39 | list.value = []; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/modules/search/search_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'search_list_view.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:pure_live/core/sites.dart'; 5 | import 'package:pure_live/common/l10n/generated/l10n.dart'; 6 | import 'package:pure_live/modules/search/search_controller.dart' as pure_live; 7 | 8 | class SearchPage extends GetView { 9 | const SearchPage({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | automaticallyImplyLeading: false, 16 | title: TextField( 17 | controller: controller.searchController, 18 | autofocus: true, 19 | decoration: InputDecoration( 20 | hintText: S.of(context).search_input_hint, 21 | border: OutlineInputBorder(borderRadius: BorderRadius.circular(24)), 22 | contentPadding: const EdgeInsets.symmetric(horizontal: 12.0), 23 | prefixIcon: IconButton( 24 | onPressed: Get.back, 25 | icon: const Icon(Icons.arrow_back), 26 | ), 27 | suffixIcon: IconButton( 28 | onPressed: controller.doSearch, 29 | icon: const Icon(Icons.search), 30 | ), 31 | ), 32 | onSubmitted: (e) { 33 | controller.doSearch(); 34 | }, 35 | ), 36 | bottom: TabBar( 37 | controller: controller.tabController, 38 | padding: EdgeInsets.zero, 39 | tabAlignment: TabAlignment.center, 40 | tabs: Sites().availableSites().map((e) => Tab(text: e.name)).toList(), 41 | isScrollable: false, 42 | indicatorSize: TabBarIndicatorSize.label, 43 | ), 44 | ), 45 | body: TabBarView( 46 | controller: controller.tabController, 47 | children: Sites().availableSites().map((e) => SearchListView(e.id)).toList(), 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/modules/settings/settings_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/common/services/settings_service.dart'; 3 | 4 | class SettingsBinding extends Binding { 5 | @override 6 | List dependencies() { 7 | return [Bind.lazyPut(() => SettingsService())]; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/modules/shield/danmu_shield_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/modules/shield/danmu_shield_controller.dart'; 3 | 4 | class DanmuShieldBinding extends Binding { 5 | @override 6 | List dependencies() { 7 | return [Bind.lazyPut(() => DanmuShieldController())]; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/modules/shield/danmu_shield_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:pure_live/common/index.dart'; 3 | import 'package:pure_live/common/base/base_controller.dart'; 4 | 5 | class DanmuShieldController extends BaseController { 6 | final TextEditingController textEditingController = TextEditingController(); 7 | final SettingsService settingsController = Get.find(); 8 | void add() { 9 | if (textEditingController.text.isEmpty) { 10 | SmartDialog.showToast("请输入关键词"); 11 | return; 12 | } 13 | 14 | settingsController.addShieldList(textEditingController.text.trim()); 15 | textEditingController.text = ""; 16 | } 17 | 18 | Color get themeColor => HexColor(settingsController.themeColorSwitch.value); 19 | void remove(int itemIndex) { 20 | settingsController.removeShieldList(itemIndex); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/plugins/archethic.dart: -------------------------------------------------------------------------------- 1 | class ArchethicUtils { 2 | // 加密 3 | String encrypt(String data) { 4 | return data; 5 | } 6 | 7 | // 解密 8 | String decrypti(String data) { 9 | return data; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/plugins/cache_network.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:pure_live/common/utils/cache_manager.dart'; 4 | 5 | class CacheNetWorkUtils { 6 | static Widget getCacheImage(String imageUrl, 7 | {double radius = 0.0, required Widget errorWidget, bool full = false}) { 8 | return imageUrl.isNotEmpty 9 | ? CachedNetworkImage( 10 | imageUrl: imageUrl, 11 | cacheManager: CustomCacheManager.instance, 12 | placeholder: (context, url) => const CircularProgressIndicator( 13 | color: Colors.white, 14 | ), 15 | errorWidget: (context, error, stackTrace) => errorWidget, 16 | imageBuilder: (context, image) => full == false 17 | ? CircleAvatar( 18 | foregroundImage: image, 19 | radius: radius, 20 | backgroundColor: Theme.of(context).disabledColor, 21 | ) 22 | : Image(image: image)) 23 | : errorWidget; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/plugins/core_error.dart: -------------------------------------------------------------------------------- 1 | class CoreError extends Error { 2 | /// 错误码 3 | final int statusCode; 4 | 5 | /// 错误信息 6 | final String message; 7 | 8 | /// 是否是Http请求错误 9 | final bool isHttpError; 10 | 11 | CoreError( 12 | this.message, { 13 | this.statusCode = 0, 14 | this.isHttpError = false, 15 | }); 16 | @override 17 | String toString() { 18 | if (isHttpError && message.isEmpty) { 19 | return statusCodeToString(statusCode); 20 | } 21 | 22 | return message; 23 | } 24 | 25 | String statusCodeToString(int statusCode) { 26 | switch (statusCode) { 27 | case 400: 28 | return "错误的请求(400)"; 29 | case 401: 30 | return "无权限访问资源(401)"; 31 | case 403: 32 | return "无权限访问资源(403)"; 33 | case 404: 34 | return "服务器找不到请求的资源(404)"; 35 | case 500: 36 | return "服务器出现错误(500)"; 37 | case 502: 38 | return "服务器出现错误(502)"; 39 | case 503: 40 | return "服务器出现错误(503)"; 41 | default: 42 | return "连接服务器失败,请稍后再试($statusCode)"; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/plugins/global.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:pure_live/common/index.dart'; 3 | 4 | Future register(String scheme) async { 5 | String appPath = Platform.resolvedExecutable; 6 | 7 | String protocolRegKey = 'Software\\Classes\\$scheme'; 8 | RegistryValue protocolRegValue = const RegistryValue( 9 | 'URL Protocol', 10 | RegistryValueType.string, 11 | '', 12 | ); 13 | String protocolCmdRegKey = 'shell\\open\\command'; 14 | RegistryValue protocolCmdRegValue = RegistryValue( 15 | '', 16 | RegistryValueType.string, 17 | '"$appPath" "%1"', 18 | ); 19 | 20 | final regKey = Registry.currentUser.createKey(protocolRegKey); 21 | regKey.createValue(protocolRegValue); 22 | regKey.createKey(protocolCmdRegKey).createValue(protocolCmdRegValue); 23 | } 24 | 25 | initRefresh() { 26 | EasyRefresh.defaultHeaderBuilder = () => const ClassicHeader( 27 | armedText: '松开加载', 28 | dragText: '上拉刷新', 29 | readyText: '加载中...', 30 | processingText: '正在刷新...', 31 | noMoreText: '没有更多数据了', 32 | failedText: '加载失败', 33 | messageText: '上次加载时间 %T', 34 | processedText: '加载成功', 35 | ); 36 | EasyRefresh.defaultFooterBuilder = () => const ClassicFooter( 37 | armedText: '松开加载', 38 | dragText: '下拉刷新', 39 | readyText: '加载中...', 40 | processingText: '正在刷新...', 41 | noMoreText: '没有更多数据了', 42 | failedText: '加载失败', 43 | messageText: '上次加载时间 %T', 44 | processedText: '加载成功', 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /lib/plugins/screen_device.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:math' as math; 3 | import 'package:get/get.dart'; 4 | import 'package:pure_live/common/index.dart'; 5 | import 'package:device_info_plus/device_info_plus.dart'; 6 | 7 | enum Device { phone, pad, tv } 8 | 9 | class ScreenDevice { 10 | static final deviceInfoPlugin = DeviceInfoPlugin(); 11 | 12 | static Future getDeviceType() async { 13 | if (Platform.isWindows) return Device.tv; 14 | final deviceInfo = await deviceInfoPlugin.deviceInfo; 15 | final dm = deviceInfo.data['displayMetrics']; 16 | double x = math.pow(dm['widthPx'] / dm['xDpi'], 2).toDouble(); 17 | double y = math.pow(dm['heightPx'] / dm['yDpi'], 2).toDouble(); 18 | double screenInches = math.sqrt(x + y); 19 | if (screenInches > 18.0) { 20 | return Device.tv; 21 | } else if (screenInches > 7.0 && screenInches <= 18.0) { 22 | return Device.pad; 23 | } 24 | return Device.phone; 25 | } 26 | 27 | static autoStartWebServer() async { 28 | await const Duration(seconds: 2).delay(); 29 | var device = await getDeviceType(); 30 | if (Platform.isAndroid && device == Device.tv) { 31 | final SettingsService service = Get.find(); 32 | if (!service.webPortEnable.value) { 33 | service.webPortEnable.value = true; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/plugins/window_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:pure_live/common/utils/index.dart'; 2 | import 'package:window_manager/window_manager.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class WindowUtil { 6 | static String title = '纯粹直播'; 7 | static Future init( 8 | {required double width, required double height}) async { 9 | double? windowsWidth = PrefUtil.getDouble('windowsWidth') ?? width; 10 | double? windowsHeight = PrefUtil.getDouble('windowsHeight') ?? height; 11 | WindowOptions windowOptions = 12 | WindowOptions(size: Size(windowsWidth, windowsHeight), center: false); 13 | windowManager.waitUntilReadyToShow(windowOptions, () async { 14 | await windowManager.show(); 15 | await windowManager.focus(); 16 | }); 17 | } 18 | 19 | static Future setTitle() async { 20 | await windowManager.setTitle(title); 21 | } 22 | 23 | static Future setWindowsPort() async { 24 | double? windowsXPosition = PrefUtil.getDouble('windowsXPosition') ?? 0.0; 25 | double? windowsYPosition = PrefUtil.getDouble('windowsYPosition') ?? 0.0; 26 | double? windowsWidth = PrefUtil.getDouble('windowsWidth') ?? 900; 27 | double? windowsHeight = PrefUtil.getDouble('windowsHeight') ?? 535; 28 | await windowManager.setBounds(Rect.fromLTWH( 29 | windowsXPosition, windowsYPosition, windowsWidth, windowsHeight)); 30 | } 31 | 32 | static void setPosition() async { 33 | Offset offset = await windowManager.getPosition(); 34 | Size size = await windowManager.getSize(); 35 | bool isFocused = await windowManager.isFocused(); 36 | if (isFocused) { 37 | await PrefUtil.setDouble('windowsXPosition', offset.dx); 38 | await PrefUtil.setDouble('windowsYPosition', offset.dy); 39 | await PrefUtil.setDouble('windowsWidth', size.width); 40 | await PrefUtil.setDouble('windowsHeight', size.height); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/routes/app_navigation.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:get/get.dart'; 3 | import 'package:pure_live/common/index.dart'; 4 | import 'package:pure_live/plugins/utils.dart'; 5 | 6 | /// APP页面跳转封装 7 | /// * 需要参数的页面都应使用此类 8 | /// * 如不需要参数,可以使用Get.toNamed 9 | class AppNavigator { 10 | /// 跳转至分类详情 11 | static void toCategoryDetail({required Site site, required LiveArea category}) { 12 | Get.toNamed(RoutePath.kAreaRooms, arguments: [site, category]); 13 | } 14 | 15 | /// 跳转至直播间 16 | static Future toLiveRoomDetail({required LiveRoom liveRoom}) async { 17 | Get.toNamed(RoutePath.kLivePlay, arguments: liveRoom, parameters: { 18 | "site": liveRoom.platform!, 19 | }); 20 | } 21 | 22 | /// 跳转至哔哩哔哩登录 23 | static Future toBiliBiliLogin() async { 24 | var contents = ['短信登陆', '二维码登陆']; 25 | if (Platform.isAndroid || Platform.isIOS) { 26 | var result = await Utils.showOptionDialog(contents, '', title: '请选择登陆方式'); 27 | if (result == '短信登陆') { 28 | await Get.toNamed(RoutePath.kBiliBiliWebLogin); 29 | } else if (result == '二维码登陆') { 30 | await Get.toNamed(RoutePath.kBiliBiliQRLogin); 31 | } 32 | } else { 33 | await Get.toNamed(RoutePath.kBiliBiliQRLogin); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/routes/route_path.dart: -------------------------------------------------------------------------------- 1 | /// 路由路径 2 | class RoutePath { 3 | /// 首页 4 | static const kInitial = "/home"; 5 | 6 | /// 关注 7 | static const kFavorite = "/favorite"; 8 | 9 | /// 热门 10 | static const kPopular = "/popular"; 11 | 12 | /// 分类 13 | static const kAreas = "/areas"; 14 | 15 | /// 分类房间 16 | static const kAreaRooms = "/area_rooms"; 17 | 18 | /// 播放页面 19 | static const kLivePlay = "/live_play"; 20 | 21 | /// 搜索 22 | static const kSearch = "/search"; 23 | 24 | /// 设置 25 | static const kSettings = "/settings"; 26 | 27 | /// 联系 28 | static const kContact = "/contact"; 29 | 30 | /// 本地恢复 31 | static const kBackup = "/backup"; 32 | 33 | /// 关于 34 | static const kAbout = "/about"; 35 | 36 | /// 版本历史记录 37 | static const kVersionHistory = "/version_history"; 38 | 39 | /// 历史记录 40 | static const kHistory = "/history"; 41 | 42 | /// 捐赠 43 | static const kDonate = "/donate"; 44 | 45 | /// 我的 46 | static const kMine = "/mine"; 47 | 48 | /// 登陆 49 | static const kSignIn = "/sign_in"; 50 | 51 | /// 登陆 52 | static const kUserManage = "/user_manage"; 53 | 54 | /// 登陆 55 | static const kUpdatePassword = "/update_password"; 56 | 57 | /// 弹幕过滤 58 | 59 | static const kSettingsDanmuShield = "/shield"; 60 | 61 | static const kSettingsHotAreas = "/hot_areas"; 62 | 63 | /// kBiliBiliQRLogin 64 | static const kSettingsAccount = "/settings_account"; 65 | 66 | static const kBiliBiliQRLogin = "/bilibili_qr_login"; 67 | 68 | static const kBiliBiliWebLogin = "/bilibili_web_login"; 69 | 70 | /// webview 71 | static const kWebview = "/webview_all"; 72 | } 73 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | void fl_register_plugins(FlPluginRegistry* registry) { 20 | g_autoptr(FlPluginRegistrar) dynamic_color_registrar = 21 | fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); 22 | dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); 23 | g_autoptr(FlPluginRegistrar) flutter_js_registrar = 24 | fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterJsPlugin"); 25 | flutter_js_plugin_register_with_registrar(flutter_js_registrar); 26 | g_autoptr(FlPluginRegistrar) flutter_volume_controller_registrar = 27 | fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterVolumeControllerPlugin"); 28 | flutter_volume_controller_plugin_register_with_registrar(flutter_volume_controller_registrar); 29 | g_autoptr(FlPluginRegistrar) gtk_registrar = 30 | fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); 31 | gtk_plugin_register_with_registrar(gtk_registrar); 32 | g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = 33 | fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); 34 | media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); 35 | g_autoptr(FlPluginRegistrar) media_kit_video_registrar = 36 | fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); 37 | media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); 38 | g_autoptr(FlPluginRegistrar) screen_retriever_registrar = 39 | fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); 40 | screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); 41 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 42 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 43 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 44 | g_autoptr(FlPluginRegistrar) window_manager_registrar = 45 | fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); 46 | window_manager_plugin_register_with_registrar(window_manager_registrar); 47 | } 48 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | dynamic_color 7 | flutter_js 8 | flutter_volume_controller 9 | gtk 10 | media_kit_libs_linux 11 | media_kit_video 12 | screen_retriever 13 | url_launcher_linux 14 | window_manager 15 | ) 16 | 17 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 18 | media_kit_native_event_loop 19 | ) 20 | 21 | set(PLUGIN_BUNDLED_LIBRARIES) 22 | 23 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 24 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 25 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 26 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 27 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 28 | endforeach(plugin) 29 | 30 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 31 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 32 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 33 | endforeach(ffi_plugin) 34 | -------------------------------------------------------------------------------- /linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import app_links 9 | import battery_plus 10 | import connectivity_plus 11 | import device_info_plus 12 | import dynamic_color 13 | import flutter_inappwebview_macos 14 | import flutter_js 15 | import flutter_volume_controller 16 | import media_kit_libs_macos_video 17 | import media_kit_video 18 | import package_info_plus 19 | import path_provider_foundation 20 | import screen_brightness_macos 21 | import screen_retriever 22 | import shared_preferences_foundation 23 | import sqflite 24 | import url_launcher_macos 25 | import wakelock_plus 26 | import window_manager 27 | 28 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 29 | AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) 30 | BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin")) 31 | ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) 32 | DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) 33 | DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) 34 | InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) 35 | FlutterJsPlugin.register(with: registry.registrar(forPlugin: "FlutterJsPlugin")) 36 | FlutterVolumeControllerPlugin.register(with: registry.registrar(forPlugin: "FlutterVolumeControllerPlugin")) 37 | MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) 38 | MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) 39 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 40 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 41 | ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin")) 42 | ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) 43 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 44 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 45 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 46 | WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) 47 | WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) 48 | } 49 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = pure_live 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.mystyle.purelive 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import 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 | -------------------------------------------------------------------------------- /pure_live_rename.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import time 4 | source_dir_name = 'E:/project/pure_live_release/' 5 | # target_win_app_name = 'E:/project/pure_live/build/windows/x64/runner/Release/' 6 | # target_apk_dir_name = 'E:/project/pure_live/build/app/outputs/flutter-apk/' 7 | target_files = ['app-arm64-v8a-release.apk','app-armeabi-v7a-release.apk','app-release.apk','app-x86_64-release.apk','pure_live.msix'] 8 | build_path = [] 9 | buildcellctions = [] 10 | target_win_app_name = 'D:/flutter/pure_live/build/windows/x64/runner/Release/' 11 | target_apk_dir_name = 'D:/flutter/pure_live/build/app/outputs/flutter-apk/' 12 | files = [] 13 | dirArr = ['安卓_手机高版本.apk','安卓_手机低版本或电视.apk','不知道就下载这个.apk','模拟器.apk','win10以上系统下载.msix'] 14 | def traversal_dirs(path): 15 | for item in os.scandir(path): 16 | if item.is_dir(): 17 | shutil.rmtree(item.path) 18 | else: 19 | os.remove(item.path) 20 | 21 | def traversal_files(path): 22 | for item in os.scandir(path): 23 | if item.is_file(): 24 | fileName = item.path.split('/')[-1].split('\\')[-1] 25 | if fileName in target_files: 26 | buildcellctions.append(item.path) 27 | 28 | def traversal_target_files(path,version): 29 | if (len(buildcellctions)!=len(dirArr)): 30 | print('请全部打包') 31 | return 32 | for i in range(0,len(dirArr)): 33 | src = os.path.join(path,version + '-' +dirArr[i]) 34 | source = buildcellctions[i] 35 | shutil.copy(source, src) 36 | def zip_dirs(path): 37 | for file in os.listdir(path): 38 | file_path = os.path.join(path, file) 39 | if os.path.isdir(file_path): 40 | shutil.make_archive(file_path, 'zip', file_path) 41 | 42 | def main(): 43 | version = str(input("请输入你想发布的版本:")) 44 | # 先改名字12 45 | traversal_dirs(source_dir_name) 46 | 47 | 48 | # 复制文件 49 | 50 | traversal_files(target_apk_dir_name) 51 | traversal_files(target_win_app_name) 52 | traversal_target_files(source_dir_name,version) 53 | # print(buildcellctions) 54 | # 压缩为zip 55 | # zip_dirs(source_dir_name) 56 | # 12 57 | if __name__ == '__main__': 58 | main() -------------------------------------------------------------------------------- /run.MD: -------------------------------------------------------------------------------- 1 | ### windows打包 2 | `dart run msix:create` 3 | ### android通用 4 | `flutter build apk` 5 | ### android分包 6 | `flutter build apk --split-per-abi` -------------------------------------------------------------------------------- /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:pure_live/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 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | void RegisterPlugins(flutter::PluginRegistry* registry) { 25 | AppLinksPluginCApiRegisterWithRegistrar( 26 | registry->GetRegistrarForPlugin("AppLinksPluginCApi")); 27 | BatteryPlusWindowsPluginRegisterWithRegistrar( 28 | registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin")); 29 | ConnectivityPlusWindowsPluginRegisterWithRegistrar( 30 | registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); 31 | DynamicColorPluginCApiRegisterWithRegistrar( 32 | registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); 33 | FlutterJsPluginRegisterWithRegistrar( 34 | registry->GetRegistrarForPlugin("FlutterJsPlugin")); 35 | FlutterVolumeControllerPluginCApiRegisterWithRegistrar( 36 | registry->GetRegistrarForPlugin("FlutterVolumeControllerPluginCApi")); 37 | MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( 38 | registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); 39 | MediaKitVideoPluginCApiRegisterWithRegistrar( 40 | registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); 41 | PermissionHandlerWindowsPluginRegisterWithRegistrar( 42 | registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); 43 | ScreenBrightnessWindowsPluginRegisterWithRegistrar( 44 | registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); 45 | ScreenRetrieverPluginRegisterWithRegistrar( 46 | registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); 47 | UrlLauncherWindowsRegisterWithRegistrar( 48 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 49 | WindowManagerPluginRegisterWithRegistrar( 50 | registry->GetRegistrarForPlugin("WindowManagerPlugin")); 51 | WindowsSingleInstancePluginRegisterWithRegistrar( 52 | registry->GetRegistrarForPlugin("WindowsSingleInstancePlugin")); 53 | } 54 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | app_links 7 | battery_plus 8 | connectivity_plus 9 | dynamic_color 10 | flutter_js 11 | flutter_volume_controller 12 | media_kit_libs_windows_video 13 | media_kit_video 14 | permission_handler_windows 15 | screen_brightness_windows 16 | screen_retriever 17 | url_launcher_windows 18 | window_manager 19 | windows_single_instance 20 | ) 21 | 22 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 23 | media_kit_native_event_loop 24 | ) 25 | 26 | set(PLUGIN_BUNDLED_LIBRARIES) 27 | 28 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 29 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 30 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 31 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 32 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 33 | endforeach(plugin) 34 | 35 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 36 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 37 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 38 | endforeach(ffi_plugin) 39 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.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 | 32 | return true; 33 | } 34 | 35 | void FlutterWindow::OnDestroy() { 36 | if (flutter_controller_) { 37 | flutter_controller_ = nullptr; 38 | } 39 | 40 | Win32Window::OnDestroy(); 41 | } 42 | 43 | LRESULT 44 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 45 | WPARAM const wparam, 46 | LPARAM const lparam) noexcept { 47 | // Give Flutter, including plugins, an opportunity to handle window messages. 48 | if (flutter_controller_) { 49 | std::optional result = 50 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 51 | lparam); 52 | if (result) { 53 | return *result; 54 | } 55 | } 56 | 57 | switch (message) { 58 | case WM_FONTCHANGE: 59 | flutter_controller_->engine()->ReloadSystemFonts(); 60 | break; 61 | } 62 | 63 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 64 | } 65 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"pure_live", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercao/pure_live/3c1890c86d1db07101d73378a4e1cc2ad152c461/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length <= 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | --------------------------------------------------------------------------------