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