├── lib ├── env.dart ├── distribute_options.yaml ├── common │ ├── values │ │ ├── values.dart │ │ ├── enum.dart │ │ ├── cache.dart │ │ ├── storage.dart │ │ └── consts.dart │ └── colors │ │ └── colors.dart ├── utils │ ├── dart_tars_protocol │ │ ├── tars_decode_exception.dart │ │ ├── tars_encode_exception.dart │ │ ├── tars_struct.dart │ │ └── tarscodec.dart │ ├── utils.dart │ ├── authentication.dart │ ├── screen_device.dart │ ├── logger.dart │ ├── gzip.dart │ ├── color_util.dart │ ├── local_storage.dart │ ├── ffi_util.dart │ ├── unilink_sub.dart │ ├── common.dart │ ├── LogFile.dart │ └── ip2region.dart ├── services │ ├── services.dart │ ├── user.dart │ ├── ipInfo │ │ ├── ip2region.dart │ │ └── find_my_ip.dart │ ├── danmaku │ │ ├── danmaku_type.dart │ │ ├── danmaku_service.dart │ │ ├── huya_danmaku_service.dart │ │ ├── bilibili_danmaku_service.dart │ │ └── douyu_danmaku_service.dart │ ├── event_bus.dart │ ├── notifications │ │ └── v_size_changed_layout_notification.dart │ └── hackchat │ │ └── hackchat.dart ├── router │ └── app_routes.dart ├── config.dart ├── components │ ├── components.dart │ ├── player │ │ ├── fvp_player.dart │ │ ├── settings │ │ │ ├── setting_alert_dialog.dart │ │ │ ├── widgets │ │ │ │ └── settings_widgets.dart │ │ │ ├── setting_tabs.dart │ │ │ ├── open_url_dialog.dart │ │ │ ├── player_settings.dart │ │ │ └── play_file_setting_dialog.dart │ │ ├── epg │ │ │ ├── epg_alert_dialog.dart │ │ │ ├── epg_date_tabs.dart │ │ │ └── epg_channel_date.dart │ │ └── player_context_menu.dart │ ├── widgets.dart │ ├── custom_scaffold.dart │ ├── custom_appbar.dart │ ├── spinning.dart │ ├── sniff │ │ └── sniff_res_table.dart │ └── updater │ │ ├── updater_widgets.dart │ │ └── updater.dart ├── models │ ├── login_mode.dart │ ├── subscription_url.dart │ ├── live_danmaku_item.dart │ ├── playlist_text_group.dart │ ├── ip_geo.dart │ ├── url_sniff_res.dart │ ├── playlist_info.dart │ ├── channel_epg.dart │ └── playlist_item.dart ├── window │ ├── live_sniff_win.dart │ ├── window.dart │ └── sub_window.dart ├── theme.dart ├── main.dart └── global.dart ├── linux ├── .gitignore ├── runner │ ├── main.cc │ ├── my_application.h │ ├── CMakeLists.txt │ └── my_application.cc ├── flutter │ ├── generated_plugin_registrant.h │ ├── generated_plugins.cmake │ ├── generated_plugin_registrant.cc │ └── CMakeLists.txt └── CMakeLists.txt ├── assets ├── logo.png ├── big-logo.png ├── ip2region.xdb ├── fonts │ └── WenQuanWeiMiHei.ttf └── scripts │ ├── install-update.bat │ └── install-update.sh ├── docs ├── player.png ├── settings.png ├── urls-sniffing.png └── remark.md ├── windows ├── runner │ ├── resources │ │ └── app_icon.ico │ ├── resource.h │ ├── utils.h │ ├── runner.exe.manifest │ ├── flutter_window.h │ ├── CMakeLists.txt │ ├── utils.cpp │ ├── flutter_window.cpp │ ├── main.cpp │ ├── Runner.rc │ └── win32_window.h ├── .gitignore ├── flutter │ ├── generated_plugin_registrant.h │ ├── generated_plugins.cmake │ ├── generated_plugin_registrant.cc │ └── CMakeLists.txt ├── rust.cmake └── CMakeLists.txt ├── devtools_options.yaml ├── update_version.dart ├── .vscode ├── launch.json └── settings.json ├── test └── widget_test.dart ├── .gitignore ├── analysis_options.yaml ├── .metadata ├── README.md ├── pubspec.yaml └── .github └── workflows └── build.yml /lib/env.dart: -------------------------------------------------------------------------------- 1 | const ENV = 'DEV'; 2 | -------------------------------------------------------------------------------- /lib/distribute_options.yaml: -------------------------------------------------------------------------------- 1 | output: dist/ -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moxun33/vvibe/HEAD/assets/logo.png -------------------------------------------------------------------------------- /docs/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moxun33/vvibe/HEAD/docs/player.png -------------------------------------------------------------------------------- /docs/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moxun33/vvibe/HEAD/docs/settings.png -------------------------------------------------------------------------------- /assets/big-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moxun33/vvibe/HEAD/assets/big-logo.png -------------------------------------------------------------------------------- /assets/ip2region.xdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moxun33/vvibe/HEAD/assets/ip2region.xdb -------------------------------------------------------------------------------- /docs/urls-sniffing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moxun33/vvibe/HEAD/docs/urls-sniffing.png -------------------------------------------------------------------------------- /assets/fonts/WenQuanWeiMiHei.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moxun33/vvibe/HEAD/assets/fonts/WenQuanWeiMiHei.ttf -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moxun33/vvibe/HEAD/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /lib/common/values/values.dart: -------------------------------------------------------------------------------- 1 | library values; 2 | 3 | export 'cache.dart'; 4 | export 'storage.dart'; 5 | export 'consts.dart'; 6 | -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /linux/runner/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /lib/utils/dart_tars_protocol/tars_decode_exception.dart: -------------------------------------------------------------------------------- 1 | class TarsDecodeException extends Error { 2 | String message; 3 | TarsDecodeException(this.message); 4 | @override 5 | String toString() { 6 | return message; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/services/services.dart: -------------------------------------------------------------------------------- 1 | library services; 2 | 3 | export 'user.dart'; 4 | export 'danmaku/bilibili_danmaku_service.dart'; 5 | export 'danmaku/douyu_danmaku_service.dart'; 6 | export 'danmaku/huya_danmaku_service.dart'; 7 | export 'hackchat/hackchat.dart'; 8 | -------------------------------------------------------------------------------- /lib/utils/dart_tars_protocol/tars_encode_exception.dart: -------------------------------------------------------------------------------- 1 | class TarsEncodeException extends Error { 2 | String message; 3 | TarsEncodeException(this.message); 4 | 5 | @override 6 | String toString() { 7 | return message; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils/utils.dart: -------------------------------------------------------------------------------- 1 | library utils; 2 | 3 | export 'local_storage.dart'; 4 | export 'request.dart'; 5 | export 'screen_device.dart'; 6 | export 'authentication.dart'; 7 | export 'ffi_util.dart'; 8 | export 'common.dart'; 9 | export 'playlist/playlist_util.dart'; 10 | -------------------------------------------------------------------------------- /lib/common/colors/colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:vvibe/utils/color_util.dart'; 3 | 4 | class AppColors { 5 | /// 主背景 6 | static const Color primaryBackground = Colors.white; 7 | 8 | /// 主色 9 | static Color primaryColor = ColorUtil.fromHex('#A92EFD'); 10 | } 11 | -------------------------------------------------------------------------------- /lib/common/values/enum.dart: -------------------------------------------------------------------------------- 1 | //url检测的结果状态 2 | enum UrlSniffResStatus { failed, success, timeout } 3 | 4 | enum UpdatStatus { 5 | available, 6 | availableWithChangelog, 7 | checking, 8 | upToDate, 9 | error, 10 | idle, 11 | downloading, 12 | readyToInstall, 13 | dismissed, 14 | } 15 | -------------------------------------------------------------------------------- /lib/router/app_routes.dart: -------------------------------------------------------------------------------- 1 | part of 'app_pages.dart'; 2 | 3 | abstract class AppRoutes { 4 | static const Index = '/index'; 5 | static const Home = '/home'; 6 | static const Login = '/login'; 7 | // notfound 8 | static const NotFound = '/notfound'; 9 | 10 | // setproxy 11 | static const Proxy = '/proxy'; 12 | } 13 | -------------------------------------------------------------------------------- /lib/config.dart: -------------------------------------------------------------------------------- 1 | import 'package:vvibe/env.dart'; 2 | 3 | // 开发环境 4 | const SERVER_HOST_DEV = 'http://192.168.3.21:30366'; 5 | 6 | // 生产环境 7 | // 生产环境地址禁止随意修改!!! 8 | const SERVER_HOST_PROD = ''; 9 | 10 | const SERVER_API_URL = ENV_IS_DEV ? SERVER_HOST_DEV : SERVER_HOST_PROD; 11 | 12 | const ENV_IS_DEV = ENV == "DEV"; 13 | 14 | const PUSH_PREFIX = ENV_IS_DEV ? "test_" : "prod_"; 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /lib/common/values/cache.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Moxx 3 | * @Date: 2022-08-30 15:53:32 4 | * @LastEditors: Moxx 5 | * @LastEditTime: 2022-09-14 11:08:09 6 | * @FilePath: \vvibe\lib\common\values\cache.dart 7 | * @Description: 8 | * @qmj 9 | */ 10 | // 是否启用缓存 11 | const CACHE_ENABLE = false; 12 | 13 | // 缓存的最长时间,单位(秒) 14 | const CACHE_MAXAGE = 1000; 15 | 16 | // 最大缓存数 17 | const CACHE_MAXCOUNT = 100; 18 | -------------------------------------------------------------------------------- /lib/components/components.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: moxun33 3 | * @Date: 2022-09-01 21:22:25 4 | * @LastEditors: moxun33 5 | * @LastEditTime: 2022-09-13 21:42:00 6 | * @FilePath: \vvibe\lib\components\components.dart 7 | * @Description: 8 | * @qmj 9 | */ 10 | library components; 11 | 12 | export 'custom_appbar.dart'; 13 | export 'custom_scaffold.dart'; 14 | export 'spinning.dart'; 15 | export 'widgets.dart'; 16 | -------------------------------------------------------------------------------- /lib/components/player/fvp_player.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FvpPlayer extends StatefulWidget { 4 | const FvpPlayer({Key? key}) : super(key: key); 5 | 6 | @override 7 | _FvpPlayerState createState() => _FvpPlayerState(); 8 | } 9 | 10 | class _FvpPlayerState extends State { 11 | @override 12 | Widget build(BuildContext context) { 13 | return Container(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/services/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:vvibe/models/login_mode.dart'; 2 | import 'package:vvibe/utils/utils.dart'; 3 | 4 | /// 用户 5 | class UserAPI { 6 | /// 登录 7 | static Future login({ 8 | required Map params, 9 | }) async { 10 | var response = await Request().post( 11 | '/login/', 12 | params: params, 13 | ); 14 | return UserLoginResponseModel.fromJson(response['data']); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /linux/runner/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, 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 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /lib/services/ipInfo/ip2region.dart: -------------------------------------------------------------------------------- 1 | import 'package:ip2region_plus/ip2region_plus.dart'; 2 | 3 | class Ip2region { 4 | static String getGeo() { 5 | String dbFile = "ip2region.xdb"; 6 | IP2RegionPlus searcher; 7 | try { 8 | searcher = IP2RegionPlus.newWithFileOnly(dbFile); 9 | Map region = searcher.search('8.8.8.8'); 10 | print(region); 11 | return region['region'] ?? ''; 12 | } catch (e) { 13 | print("failed to create searcher with '$dbFile': $e"); 14 | return ''; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/utils/authentication.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:vvibe/common/values/values.dart'; 3 | import 'package:vvibe/global.dart'; 4 | import 'package:vvibe/utils/utils.dart'; 5 | 6 | /// 检查是否有 token 7 | Future isAuthenticated() async { 8 | var profileJSON = LoacalStorage().getJSON(STORAGE_USER_PROFILE_KEY); 9 | return profileJSON != null ? true : false; 10 | } 11 | 12 | /// 删除缓存token 13 | Future deleteAuthentication() async { 14 | await LoacalStorage().remove(STORAGE_USER_PROFILE_KEY); 15 | Global.profile = null; 16 | } 17 | -------------------------------------------------------------------------------- /lib/utils/screen_device.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 设备屏幕高度 4 | double getDeviceHeight(BuildContext context) { 5 | return MediaQuery.of(context).size.height; 6 | } 7 | 8 | /// 设备屏幕宽度 9 | double getDeviceWidth(BuildContext context) { 10 | return MediaQuery.of(context).size.width; 11 | } 12 | 13 | /// 设备顶部状态栏宽度 14 | double getDeviceTopHeight(BuildContext context) { 15 | return MediaQuery.of(context).padding.top; 16 | } 17 | 18 | /// 设备底部Bar宽度 19 | double getDeviceBottomHeight(BuildContext context) { 20 | return MediaQuery.of(context).padding.bottom; 21 | } 22 | -------------------------------------------------------------------------------- /docs/remark.md: -------------------------------------------------------------------------------- 1 | # 备注 2 | 3 | ## 获取``pubspec.yaml``的version 4 | ```bash 5 | cat pubspec.yaml | grep 'version:' | head -1 | cut -f2- -d: | sed -e 's/^[ \t]*//' 6 | ``` 7 | 8 | ## 提升版本号 9 | ```bash 10 | awk '{ match($0,/([0-9]+)\+([0-9]+)/,a); a[1]=a[1]+1; a[2]=a[2]+1; sub(/[0-9]+\+[0-9]+/,a[1]"+"a[2])}1' pubspec.yaml | grep 'version:' | head -1 | cut -f2- -d: | sed -e 's/^[ \t]*//' 11 | ``` 12 | 13 | ## 更新``pubspec.yaml``的version 14 | ```bash 15 | awk '{ match($0,/([0-9]+)\+([0-9]+)/,a); a[1]=a[1]+1; a[2]=a[2]+1; sub(/[0-9]+\+[0-9]+/,a[1]"+"a[2])}1' pubspec.yaml | grep 'version:' | head -1 | cut -f2- -d: | sed -e 's/^[ \t]*//' 16 | ``` -------------------------------------------------------------------------------- /lib/common/values/storage.dart: -------------------------------------------------------------------------------- 1 | /// 用户信息 2 | const String STORAGE_USER_PROFILE_KEY = 'user_profile'; 3 | 4 | /// 设备是否第一次打开 5 | const String STORAGE_DEVICE_ALREADY_OPEN_KEY = 'device_already_open'; 6 | 7 | /// 极光设备RegistrationID 8 | const String REGISTRATION_ID = 'reegistration_id'; 9 | 10 | //最后选择的播放文件或订阅 11 | const LAST_PLAYLIST_FILE_OR_SUB = 'LAST_PLAYLIST_FILE_OR_SUB'; 12 | //最后的播放列表 13 | const LAST_PLAYLIST_DATA = 'LAST_LOCAL_PLAYLIST_DATA'; 14 | //最后播放的视频url 15 | const LAST_PLAY_VIDEO_URL = 'LAST_PLAY_VIDEO_URL'; 16 | //远程订阅的订阅地址列表 17 | const PLAYLIST_SUB_URLS = 'PLAYLIST_SUB_URLS'; 18 | //请求头、epg,弹幕等播放器设置 19 | const PLAYER_SETTINGS = 'PLAYER_SETTINGS'; 20 | -------------------------------------------------------------------------------- /lib/utils/dart_tars_protocol/tars_struct.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names, non_constant_identifier_names 2 | 3 | import 'tars_input_stream.dart'; 4 | import 'tars_output_stream.dart'; 5 | 6 | enum TarsStructType { 7 | BYTE, 8 | SHORT, 9 | INT, 10 | LONG, 11 | FLOAT, 12 | DOUBLE, 13 | STRING1, 14 | STRING4, 15 | MAP, 16 | LIST, 17 | STRUCT_BEGIN, 18 | STRUCT_END, 19 | ZERO_TAG, 20 | SIMPLE_LIST, 21 | } 22 | 23 | abstract class TarsStruct { 24 | static int TARS_MAX_STRING_LENGTH = 100 * 1024 * 1024; 25 | void writeTo(TarsOutputStream _os); 26 | void readFrom(TarsInputStream _is); 27 | void display(StringBuffer sb, int level); 28 | } 29 | -------------------------------------------------------------------------------- /lib/services/danmaku/danmaku_type.dart: -------------------------------------------------------------------------------- 1 | class DanmakuType { 2 | static final douyu = 'douyu'; 3 | static final douyuCN = '斗鱼'; 4 | static final douyuGroupReg = RegExp(r'斗鱼|douyu'); 5 | static final douyuProxyUrlReg = RegExp(r'\/dyu\/|\/douyu(\d+)?\/?(\.php)?'); 6 | 7 | static final huya = 'huya'; 8 | static final huyaCN = '虎牙'; 9 | static final huyaGroupReg = RegExp(r'虎牙|huya'); 10 | static final huyaProxyUrlReg = RegExp(r'\/hy\/|\/huya(\d+)?\/?(\.php)?'); 11 | static final bilibili = 'bilibili'; 12 | static final bilibiliCN = 'B站'; 13 | static final biliGroupReg = RegExp(r'B站|bilibili'); 14 | static final biliProxyUrlReg = RegExp(r'\/bilibili(\d+)?\/?(\.php)?'); 15 | } 16 | -------------------------------------------------------------------------------- /lib/utils/logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:logging/logging.dart'; 2 | import 'package:vvibe/common/values/consts.dart'; 3 | 4 | class MyLogger { 5 | static MyLogger _instance = new MyLogger._(); 6 | factory MyLogger() => _instance; 7 | 8 | MyLogger._(); 9 | static final log = Logger(APP_NAME); 10 | 11 | static info(String message) { 12 | log.info(message); 13 | } 14 | 15 | static error(String message) { 16 | log.fine(message); 17 | } 18 | 19 | static debug(String message) { 20 | log.shout(message); 21 | } 22 | 23 | static verbose(String message) { 24 | log.severe(message); 25 | } 26 | 27 | static warn(String message) { 28 | log.warning(message); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/common/values/consts.dart: -------------------------------------------------------------------------------- 1 | //播放列表面板宽度 2 | 3 | import 'dart:io'; 4 | 5 | const PLAYLIST_BAR_WIDTH = 220.0; 6 | 7 | //默认请求头 8 | final DEF_REQ_UA = 'VVibe/0.x ${Platform.operatingSystem}'; 9 | 10 | //默认epg地址 11 | const DEF_EPG_URL = 'https://epg.v1.mk/fy.xml.gz'; 12 | 13 | //默认弹幕字体大小 14 | const DEF_DM_FONT_SIZE = 20; 15 | 16 | const bool IS_RELEASE = bool.fromEnvironment("dart.vm.product"); 17 | 18 | //assets目录 19 | const ASSETS_DIR = IS_RELEASE ? 'data/flutter_assets/assets' : 'assets'; 20 | final APP_DIR = IS_RELEASE 21 | ? Directory.current.path 22 | : Directory(Platform.resolvedExecutable).parent.path; 23 | //自定义窗口标题栏高度 24 | const CUS_WIN_TITLEBAR_HEIGHT = 30.0; 25 | 26 | // app name 27 | const APP_NAME = 'VVibe'; 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/scripts/install-update.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 3 | :: 在exe所在目录运行 4 | :: 获取当前脚本所在目录 5 | set current_dir=%~dp0 6 | 7 | :: 设置源目录和目标目录路径(相对当前脚本目录) 8 | set source_dir=%cd%\data\updater\Release 9 | set target_dir=%cd% 10 | 11 | :: 设置需要启动的 exe 文件路径(相对当前脚本目录) 12 | set exe_path=%cd%\vvibe.exe 13 | 14 | 15 | :: 输出正在执行操作的信息 16 | echo Copying files from %source_dir% to %target_dir% 17 | 18 | :: 复制文件并覆盖已存在的文件 19 | xcopy "%source_dir%\*" "%target_dir%\" /E /H /Y 20 | 21 | :: 检查是否复制成功 22 | if %errorlevel% neq 0 ( 23 | echo Error occurred during file copy! 24 | exit /b 1 25 | ) 26 | 27 | :: 输出文件复制成功信息 28 | echo Files copied successfully. 29 | 30 | :: 启动指定的 exe 文件 31 | echo Starting %exe_path%... 32 | start "" "%exe_path%" 33 | 34 | :: 退出脚本 35 | ::exit /b 0 36 | -------------------------------------------------------------------------------- /update_version.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:yaml/yaml.dart'; 4 | 5 | void main() async { 6 | final file = File('pubspec.yaml'); 7 | final content = await file.readAsString(); 8 | final doc = loadYaml(content); 9 | 10 | final version = doc['version'] as String; 11 | final versionParts = version.split('.'); 12 | 13 | // 你可以根据需要更新版本的哪个部分 14 | // 以下是更新修订版本(即版本号的第三部分) 15 | versionParts[2] = (int.parse(versionParts[2]) + 1).toString(); // 增加修订版本 16 | 17 | // 重新组装版本号 18 | final newVersion = '${versionParts[0]}.${versionParts[1]}.${versionParts[2]}'; 19 | 20 | // 更新 pubspec.yaml 文件中的版本号 21 | final newContent = content.replaceFirst(version, newVersion); 22 | await file.writeAsString(newContent); 23 | 24 | print('Updated version: $newVersion'); 25 | } 26 | // dart run update_version.dart 27 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "vvibe", 9 | "request": "launch", 10 | "program": "lib/main.dart", 11 | "type": "dart", 12 | "args": [] 13 | }, 14 | { 15 | "name": "vvibe (profile mode)", 16 | "request": "launch", 17 | "program": "lib/main.dart", 18 | "type": "dart", 19 | "flutterMode": "profile", 20 | "args": [] 21 | }, 22 | { 23 | "name": "vvibe (release mode)", 24 | "request": "launch", 25 | "program": "lib/main.dart", 26 | "type": "dart", 27 | "flutterMode": "release", 28 | "args": [] 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /lib/models/login_mode.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | UserLoginResponseModel userLoginResponseModelFromJson(String str) => 4 | UserLoginResponseModel.fromJson(json.decode(str)); 5 | 6 | String userLoginResponseModelToJson(UserLoginResponseModel data) => 7 | json.encode(data.toJson()); 8 | 9 | class UserLoginResponseModel { 10 | UserLoginResponseModel({ 11 | this.token, 12 | this.name, 13 | this.id, 14 | }); 15 | 16 | String? token; 17 | String? name; 18 | int? id; 19 | 20 | factory UserLoginResponseModel.fromJson(Map json) => 21 | UserLoginResponseModel( 22 | token: json["token"], 23 | name: json["name"], 24 | id: json["id"], 25 | ); 26 | 27 | Map toJson() => { 28 | "token": token, 29 | "name": name, 30 | "id": id, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /windows/rust.cmake: -------------------------------------------------------------------------------- 1 | # We include Corrosion inline here, but ideally in a project with 2 | # many dependencies we would need to install Corrosion on the system. 3 | # See instructions on https://github.com/AndrewGaspar/corrosion#cmake-install 4 | # Once done, uncomment this line: 5 | # find_package(Corrosion REQUIRED) 6 | 7 | include(FetchContent) 8 | 9 | FetchContent_Declare( 10 | Corrosion 11 | GIT_REPOSITORY https://github.com/AndrewGaspar/corrosion.git 12 | GIT_TAG origin/master # Optionally specify a version tag or branch here 13 | ) 14 | 15 | FetchContent_MakeAvailable(Corrosion) 16 | 17 | corrosion_import_crate(MANIFEST_PATH ../rust/Cargo.toml) 18 | 19 | # Flutter-specific 20 | 21 | set(CRATE_NAME "native") 22 | 23 | target_link_libraries(${BINARY_NAME} PRIVATE ${CRATE_NAME}) 24 | 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) -------------------------------------------------------------------------------- /assets/scripts/install-update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 设置源目录和目标目录路径 4 | source_dir="$(pwd)/data/updater/test" 5 | target_dir=$(pwd) 6 | exe_file="vvibe" # 要执行的可执行文件 7 | echo $script_dir 8 | # 输出正在复制文件的信息 9 | echo "Copying files from $source_dir to $target_dir..." 10 | 11 | 12 | # 复制源目录的文件到目标目录,-r 表示递归复制目录 13 | cp -r "$source_dir"/* "$target_dir" 14 | 15 | # 检查复制操作是否成功 16 | if [ $? -ne 0 ]; then 17 | echo "Error occurred during file copy!" 18 | exit 1 19 | else 20 | echo "Files copied successfully." 21 | fi 22 | 23 | # 运行目标目录中的可执行文件 24 | exe_path="$target_dir/$exe_file" 25 | 26 | # 检查文件是否存在 27 | if [ ! -f "$exe_path" ]; then 28 | echo "Executable file not found: $exe_path" 29 | exit 1 30 | fi 31 | 32 | # 执行可执行文件 33 | echo "Starting executable: $exe_path" 34 | "$exe_path" 35 | 36 | # 检查执行是否成功 37 | if [ $? -ne 0 ]; then 38 | echo "Error occurred during execution!" 39 | exit 1 40 | else 41 | echo "Executable ran successfully." 42 | fi 43 | 44 | exit 0 -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | fvp 7 | native_context_menu 8 | screen_retriever_linux 9 | url_launcher_linux 10 | window_manager 11 | ) 12 | 13 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 14 | ) 15 | 16 | set(PLUGIN_BUNDLED_LIBRARIES) 17 | 18 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 19 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 20 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 21 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 23 | endforeach(plugin) 24 | 25 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 26 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 27 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 28 | endforeach(ffi_plugin) 29 | -------------------------------------------------------------------------------- /lib/models/subscription_url.dart: -------------------------------------------------------------------------------- 1 | // To parse this JSON data, do 2 | // 3 | // final subscriptionUrl = subscriptionUrlFromJson(jsonString); 4 | 5 | import 'dart:convert'; 6 | 7 | SubscriptionUrl subscriptionUrlFromJson(String str) => 8 | SubscriptionUrl.fromJson(json.decode(str)); 9 | 10 | String subscriptionUrlToJson(SubscriptionUrl data) => 11 | json.encode(data.toJson()); 12 | 13 | class SubscriptionUrl { 14 | SubscriptionUrl({ 15 | required this.id, 16 | required this.name, 17 | required this.url, 18 | }); 19 | 20 | String id; 21 | String name; 22 | String url; 23 | 24 | factory SubscriptionUrl.fromJson(Map json) => 25 | SubscriptionUrl( 26 | id: json["id"], 27 | name: json["name"], 28 | url: json["url"], 29 | ); 30 | 31 | Map toJson() => { 32 | "id": id, 33 | "name": name, 34 | "url": url, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /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/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | fvp 7 | native_context_menu 8 | screen_retriever_windows 9 | uni_links_desktop 10 | url_launcher_windows 11 | window_manager 12 | ) 13 | 14 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 15 | ) 16 | 17 | set(PLUGIN_BUNDLED_LIBRARIES) 18 | 19 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 20 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 21 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 24 | endforeach(plugin) 25 | 26 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 27 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 28 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 29 | endforeach(ffi_plugin) 30 | -------------------------------------------------------------------------------- /lib/window/live_sniff_win.dart: -------------------------------------------------------------------------------- 1 | //直播源嗅探窗口 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class LiveSniffWin extends StatefulWidget { 6 | const LiveSniffWin( 7 | {Key? key, 8 | //required this.windowController, 9 | required this.args, 10 | required this.theme}) 11 | : super(key: key); 12 | final ThemeData theme; 13 | // final WindowController windowController; 14 | final Map? args; 15 | 16 | @override 17 | _LiveSniffWinState createState() => _LiveSniffWinState(); 18 | } 19 | 20 | class _LiveSniffWinState extends State { 21 | @override 22 | void initState() { 23 | super.initState(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return SizedBox(); /* return SubWindow( 29 | windowController: widget.windowController, 30 | args: widget.args, 31 | child: LiveSniff(), 32 | title: 'VVibe 直播源扫描', 33 | theme: widget.theme, 34 | ); */ 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/theme.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Moxx 3 | * @Date: 2022-09-09 17:00:19 * @Last Modified by: Moxx 4 | * @Last Modified time: 2022-09-09 17:00:19 @Description: 5 | */ 6 | 7 | //import 'dart:convert'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:vvibe/common/colors/colors.dart'; 10 | 11 | /* import 'package:json_theme/json_theme.dart'; 12 | import 'package:flutter/services.dart'; 13 | 14 | //根据JSON生成主题 15 | 16 | Future genTheme( 17 | {String jsonPath = 'assets/light_theme.json'}) async { 18 | SchemaValidator.enabled = false; 19 | final themeStr = await rootBundle.loadString(jsonPath); 20 | final themeJson = jsonDecode(themeStr); 21 | return ThemeDecoder.decodeThemeData(themeJson)!; 22 | } 23 | */ 24 | ThemeData genTheme() { 25 | return ThemeData( 26 | scaffoldBackgroundColor: Colors.black, 27 | canvasColor: Colors.black12, 28 | primaryColor: AppColors.primaryColor, 29 | splashColor: Colors.transparent, 30 | highlightColor: Colors.transparent, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /lib/components/player/settings/setting_alert_dialog.dart: -------------------------------------------------------------------------------- 1 | //播放器的设置弹窗 2 | import 'package:flutter/material.dart'; 3 | import 'package:vvibe/components/player/settings/setting_tabs.dart'; 4 | import 'package:vvibe/utils/utils.dart'; 5 | 6 | class SettingAlertDialog extends StatefulWidget { 7 | const SettingAlertDialog({Key? key}) : super(key: key); 8 | 9 | @override 10 | _SettingAlertDialogState createState() => _SettingAlertDialogState(); 11 | } 12 | 13 | class _SettingAlertDialogState extends State { 14 | @override 15 | Widget build(BuildContext context) { 16 | return AlertDialog( 17 | title: const Text( 18 | '应用设置', 19 | ), 20 | titleTextStyle: TextStyle( 21 | fontWeight: FontWeight.bold, color: Colors.black87, fontSize: 22), 22 | actions: [ 23 | TextButton(child: const Text(''), onPressed: () {}), 24 | ], 25 | content: SizedBox( 26 | width: 1200, 27 | height: getDeviceHeight(context), 28 | child: SettingsTabsView(), 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /linux/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} 10 | "main.cc" 11 | "my_application.cc" 12 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 13 | ) 14 | 15 | # Apply the standard set of build settings. This can be removed for applications 16 | # that need different build settings. 17 | apply_standard_settings(${BINARY_NAME}) 18 | 19 | # Add preprocessor definitions for the application ID. 20 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 21 | 22 | # Add dependency libraries. Add any application-specific dependencies here. 23 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 24 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 25 | 26 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/models/live_danmaku_item.dart: -------------------------------------------------------------------------------- 1 | // To parse this JSON data, do 2 | // 3 | // final liveDanmakuItem = liveDanmakuItemFromJson(jsonString); 4 | 5 | import 'dart:convert'; 6 | import 'dart:ui'; 7 | 8 | LiveDanmakuItem liveDanmakuItemFromJson(String str) => 9 | LiveDanmakuItem.fromJson(json.decode(str)); 10 | 11 | String liveDanmakuItemToJson(LiveDanmakuItem data) => 12 | json.encode(data.toJson()); 13 | 14 | class LiveDanmakuItem { 15 | LiveDanmakuItem( 16 | {required this.name, 17 | required this.msg, 18 | required this.uid, 19 | this.ext, 20 | this.isUpper, 21 | this.color}); 22 | 23 | String name; 24 | String msg; 25 | String uid; 26 | Map? ext; 27 | Color? color; 28 | bool? isUpper; 29 | 30 | factory LiveDanmakuItem.fromJson(Map json) => 31 | LiveDanmakuItem( 32 | name: json["name"], 33 | msg: json["msg"], 34 | uid: json["uid"], 35 | isUpper: json["isUpper"], 36 | ); 37 | 38 | Map toJson() => 39 | {"name": name, "msg": msg, "uid": uid, 'isUpper': isUpper}; 40 | } 41 | -------------------------------------------------------------------------------- /lib/models/playlist_text_group.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Moxx 3 | * @Date: 2022-09-19 17:25:43 4 | * @LastEditors: Moxx 5 | * @LastEditTime: 2022-09-19 17:25:48 6 | * @FilePath: \vvibe\lib\models\playlist_text_group.dart 7 | * @Description: 8 | * @qmj 9 | */ 10 | // To parse this JSON data, do 11 | // 12 | // final playListTextGroup = playListTextGroupFromJson(jsonString); 13 | 14 | import 'dart:convert'; 15 | 16 | PlayListTextGroup playListTextGroupFromJson(String str) => 17 | PlayListTextGroup.fromJson(json.decode(str)); 18 | 19 | String playListTextGroupToJson(PlayListTextGroup data) => 20 | json.encode(data.toJson()); 21 | 22 | class PlayListTextGroup { 23 | PlayListTextGroup({ 24 | required this.group, 25 | required this.index, 26 | }); 27 | 28 | String group; 29 | int index; 30 | 31 | factory PlayListTextGroup.fromJson(Map json) => 32 | PlayListTextGroup( 33 | group: json["group"], 34 | index: json["index"], 35 | ); 36 | 37 | Map toJson() => { 38 | "group": group, 39 | "index": index, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /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:vvibe/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(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/models/ip_geo.dart: -------------------------------------------------------------------------------- 1 | // To parse this JSON data, do 2 | // 3 | // final ipGeo = ipGeoFromJson(jsonString); 4 | 5 | import 'dart:convert'; 6 | 7 | IpGeo ipGeoFromJson(String str) => IpGeo.fromJson(json.decode(str)); 8 | 9 | String ipGeoToJson(IpGeo data) => json.encode(data.toJson()); 10 | 11 | class IpGeo { 12 | String? ip; 13 | String? country; 14 | String? province; 15 | String? city; 16 | String? county; 17 | String? region; 18 | String? isp; 19 | 20 | IpGeo({ 21 | this.ip, 22 | this.country, 23 | this.province, 24 | this.city, 25 | this.county, 26 | this.region, 27 | this.isp, 28 | }); 29 | 30 | factory IpGeo.fromJson(Map json) => IpGeo( 31 | ip: json["ip"], 32 | country: json["country"], 33 | province: json["province"], 34 | city: json["city"], 35 | county: json["county"], 36 | region: json["region"], 37 | isp: json["isp"], 38 | ); 39 | 40 | Map toJson() => { 41 | "ip": ip, 42 | "country": country, 43 | "province": province, 44 | "city": city, 45 | "county": county, 46 | "region": region, 47 | "isp": isp, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /lib/services/event_bus.dart: -------------------------------------------------------------------------------- 1 | //订阅者回调签名 2 | typedef void EventCallback(arg); 3 | 4 | class EventBus { 5 | //私有构造函数 6 | EventBus._internal(); 7 | 8 | //保存单例 9 | static EventBus _singleton = EventBus._internal(); 10 | 11 | //工厂构造函数 12 | factory EventBus() => _singleton; 13 | 14 | //保存事件订阅者队列,key:事件名(id),value: 对应事件的订阅者队列 15 | final _emap = Map?>(); 16 | 17 | //添加订阅者 18 | void on(eventName, EventCallback f) { 19 | _emap[eventName] ??= []; 20 | _emap[eventName]!.add(f); 21 | } 22 | 23 | //移除订阅者 24 | void off(eventName, [EventCallback? f]) { 25 | var list = _emap[eventName]; 26 | if (eventName == null || list == null) return; 27 | if (f == null) { 28 | _emap[eventName] = null; 29 | } else { 30 | list.remove(f); 31 | } 32 | } 33 | 34 | //触发事件,事件触发后该事件所有订阅者会被调用 35 | void emit(eventName, [arg]) { 36 | var list = _emap[eventName]; 37 | if (list == null) return; 38 | int len = list.length - 1; 39 | //反向遍历,防止订阅者在回调中移除自身带来的下标错位 40 | for (var i = len; i > -1; --i) { 41 | list[i](arg); 42 | } 43 | } 44 | } 45 | 46 | //定义一个top-level(全局)变量,页面引入该文件后可以直接使用bus 47 | var eventBus = EventBus(); 48 | -------------------------------------------------------------------------------- /lib/utils/gzip.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:archive/archive_io.dart'; 4 | import 'package:vvibe/utils/logger.dart'; 5 | 6 | // 解压gzip 7 | Future unzipGzip(String gzPath, String extractPath) async { 8 | try { 9 | File compressedFile = File(gzPath); 10 | 11 | List compressedBytes = await compressedFile.readAsBytes(); 12 | 13 | GZipDecoder decoder = GZipDecoder(); 14 | List decompressedBytes = decoder.decodeBytes(compressedBytes); 15 | 16 | File decompressedFile = File(extractPath); 17 | await decompressedFile.writeAsBytes(decompressedBytes); 18 | 19 | MyLogger.info(gzPath + '解压完成!' + extractPath); 20 | return true; 21 | } catch (e) { 22 | MyLogger.error('解压 $gzPath 出错' + e.toString()); 23 | return false; 24 | } 25 | } 26 | 27 | // 解压zip 28 | Future unzipZip(String gzPath, String extractPath) async { 29 | try { 30 | final inputStream = InputFileStream(gzPath); 31 | 32 | final archive = ZipDecoder().decodeStream(inputStream); 33 | extractArchiveToDisk(archive, extractPath); 34 | 35 | MyLogger.info(gzPath + '解压完成!' + extractPath); 36 | return true; 37 | } catch (e) { 38 | MyLogger.error('解压 $gzPath 出错' + e.toString()); 39 | return false; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/components/player/settings/widgets/settings_widgets.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SettingsWidgets { 4 | static Widget buildInputRow(TextEditingController controller, 5 | {String? label, 6 | InputDecoration? decoration, 7 | double? inputWidth = 650.0}) { 8 | return Row( 9 | children: [ 10 | SizedBox( 11 | width: 100, 12 | child: Text(label ?? '', style: TextStyle(color: Colors.purple))), 13 | SizedBox( 14 | width: inputWidth ?? 650.0, 15 | child: TextField(controller: controller, decoration: decoration), 16 | ) 17 | ], 18 | ); 19 | } 20 | 21 | static Widget buildSwitch( 22 | String label, bool value, Function(bool v) onChanged) { 23 | return Wrap( 24 | children: [ 25 | Padding( 26 | padding: EdgeInsets.only(top: 10), 27 | child: SizedBox( 28 | width: 60, 29 | child: Text(label, style: TextStyle(color: Colors.purple))), 30 | ), 31 | SizedBox( 32 | width: 80, 33 | child: Switch( 34 | value: value, //当前状态 35 | onChanged: onChanged, 36 | ), 37 | ), 38 | ], 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.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 | /build1/ 35 | /build2/ 36 | 37 | # Web related 38 | lib/generated_plugin_registrant.dart 39 | 40 | # Symbolication related 41 | app.*.symbols 42 | 43 | # Obfuscation related 44 | app.*.map.json 45 | 46 | # Android Studio will place build artifacts here 47 | /android/app/debug 48 | /android/app/profile 49 | /android/app/release 50 | 51 | target/ 52 | 53 | flutter_rust_bridge_codegen.exe 54 | /playlist/* 55 | 56 | /snapshots/ 57 | /data/ 58 | /logs/ 59 | *.local.* 60 | *udp*.* 61 | *iptv*.txt 62 | 63 | .vs/ 64 | assets/epg/* 65 | assets/ffmpeg/* 66 | assets/logs/* 67 | assets/updater/* 68 | 69 | # FVM Version Cache 70 | .fvm/ 71 | -------------------------------------------------------------------------------- /lib/components/player/epg/epg_alert_dialog.dart: -------------------------------------------------------------------------------- 1 | //播放器的设置弹窗 2 | import 'package:flutter/material.dart'; 3 | import 'package:vvibe/components/player/epg/epg_date_tabs.dart'; 4 | import 'package:vvibe/models/playlist_item.dart'; 5 | import 'package:vvibe/utils/utils.dart'; 6 | 7 | class EpgAlertDialog extends StatefulWidget { 8 | const EpgAlertDialog( 9 | {Key? key, required this.urlItem, this.epgUrl, required this.doPlayback}) 10 | : super(key: key); 11 | 12 | final PlayListItem urlItem; 13 | final String? epgUrl; 14 | final Function doPlayback; 15 | @override 16 | _EpgAlertDialogState createState() => _EpgAlertDialogState(); 17 | } 18 | 19 | class _EpgAlertDialogState extends State { 20 | @override 21 | Widget build(BuildContext context) { 22 | return AlertDialog( 23 | title: const Text('节目单'), 24 | actions: [ 25 | TextButton(child: const Text(''), onPressed: () {}), 26 | ], 27 | content: SizedBox( 28 | width: 1000, 29 | height: getDeviceHeight(context), 30 | child: EpgDateTabsView( 31 | epgUrl: widget.epgUrl, 32 | doPlayback: widget.doPlayback, 33 | urlItem: widget.urlItem, 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /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 | 16 | void RegisterPlugins(flutter::PluginRegistry* registry) { 17 | FvpPluginCApiRegisterWithRegistrar( 18 | registry->GetRegistrarForPlugin("FvpPluginCApi")); 19 | NativeContextMenuPluginRegisterWithRegistrar( 20 | registry->GetRegistrarForPlugin("NativeContextMenuPlugin")); 21 | ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( 22 | registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); 23 | UniLinksDesktopPluginRegisterWithRegistrar( 24 | registry->GetRegistrarForPlugin("UniLinksDesktopPlugin")); 25 | UrlLauncherWindowsRegisterWithRegistrar( 26 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 27 | WindowManagerPluginRegisterWithRegistrar( 28 | registry->GetRegistrarForPlugin("WindowManagerPlugin")); 29 | } 30 | -------------------------------------------------------------------------------- /lib/components/widgets.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: moxun33 3 | * @Date: 2022-09-13 21:40:49 4 | * @LastEditors: moxun33 5 | * @LastEditTime: 2023-02-12 22:43:39 6 | * @FilePath: \vvibe\lib\components\widgets.dart 7 | * @Description: 一些小组件 8 | * @qmj 9 | */ 10 | 11 | //有边框的文字 12 | import 'package:flutter/material.dart'; 13 | 14 | class BorderText extends StatelessWidget { 15 | BorderText({Key? key, required this.text, this.fontSize}) : super(key: key); 16 | String text; 17 | double? fontSize; 18 | @override 19 | Widget build(BuildContext context) { 20 | return Stack( 21 | children: [ 22 | // Implement the stroke 23 | Text( 24 | text, 25 | style: TextStyle( 26 | fontSize: fontSize ?? 20, 27 | fontWeight: FontWeight.bold, 28 | foreground: Paint() 29 | ..style = PaintingStyle.stroke 30 | ..strokeWidth = 5 31 | ..color = Colors.purple, 32 | ), 33 | ), 34 | // The text inside 35 | Text( 36 | text, 37 | style: TextStyle( 38 | fontSize: fontSize ?? 20, 39 | fontWeight: FontWeight.bold, 40 | color: Colors.white, 41 | ), 42 | ), 43 | ], 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/services/ipInfo/find_my_ip.dart: -------------------------------------------------------------------------------- 1 | import 'package:vvibe/models/ip_geo.dart'; 2 | import 'package:vvibe/utils/utils.dart'; 3 | 4 | class FindMyIp { 5 | /** 6 | * @desc: 获取ip的地理位置 7 | * @author Moxx 8 | * @date 2023/11/12 9 | * @return {?Promise} 10 | * { 11 | * "code": 200, 12 | * "data": { 13 | * "API_1": { 14 | * "ip": "175.9.143.86", 15 | * "country": "中国", 16 | * "province": "湖南", 17 | * "city": "长沙", 18 | * "county": "", 19 | * "region": "亚洲", 20 | * "isp": "电信" 21 | * }, 22 | * "API_2": { 23 | * "ip": "175.9.143.86", 24 | * "region": "湖南省长沙市 电信" 25 | * } 26 | * }, 27 | * "processTime": "31.69ms", 28 | * "url": "https://findmyip.net/", 29 | * "time": "2023-12-15 19:41:51" 30 | * } 31 | */ 32 | static Future getGeo(String ip) async { 33 | final res = 34 | await Request().get("https://findmyip.net/api/ipinfo.php", {'ip': ip}); 35 | if (res['code'] != 200 || res['data'] == null) { 36 | return null; 37 | } 38 | final info = res['data']['API_1'] ?? res['data']['API_2']; 39 | if (info == null) return null; 40 | return IpGeo.fromJson(info); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/components/custom_scaffold.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'custom_appbar.dart'; 3 | 4 | class BaseScaffold extends Scaffold { 5 | BaseScaffold( 6 | {String? title, 7 | PreferredSizeWidget? appBar, 8 | required Widget body, 9 | List? actions, 10 | AppBarBackType? leadType, 11 | WillPopCallback? onWillPop, 12 | Brightness brightness = Brightness.light, 13 | Widget? floatingActionButton, 14 | Color appBarBackgroundColor = Colors.white, 15 | Color? titleColor, 16 | bool centerTitle = true, 17 | FloatingActionButtonLocation? floatingActionButtonLocation}) 18 | : super( 19 | appBar: appBar ?? 20 | MyAppBar( 21 | brightness: Brightness.light, 22 | leadingType: leadType ?? AppBarBackType.Back, 23 | onWillPop: onWillPop, 24 | actions: actions ?? [], 25 | centerTitle: centerTitle, 26 | title: MyTitle(title ?? '', color: titleColor ?? Colors.grey[800]), 27 | backgroundColor: appBarBackgroundColor, 28 | ), 29 | backgroundColor: Colors.white, 30 | body: body, 31 | floatingActionButton: floatingActionButton, 32 | floatingActionButtonLocation: floatingActionButtonLocation, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /lib/services/notifications/v_size_changed_layout_notification.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | 4 | class VSizeChangedLayoutNotification extends Notification { 5 | final Size size; 6 | 7 | VSizeChangedLayoutNotification(this.size); 8 | } 9 | 10 | class VSizeChangedLayoutNotifier extends SingleChildRenderObjectWidget { 11 | const VSizeChangedLayoutNotifier({ 12 | Key? key, 13 | Widget? child, 14 | }) : super(key: key, child: child); 15 | 16 | @override 17 | _RenderSizeChangedWithCallback createRenderObject(BuildContext context) { 18 | return _RenderSizeChangedWithCallback(onLayoutChangedCallback: (Size size) { 19 | VSizeChangedLayoutNotification(size).dispatch(context); 20 | }); 21 | } 22 | } 23 | 24 | class _RenderSizeChangedWithCallback extends RenderProxyBox { 25 | _RenderSizeChangedWithCallback({ 26 | RenderBox? child, 27 | required this.onLayoutChangedCallback, 28 | }) : assert(onLayoutChangedCallback != null), 29 | super(child); 30 | 31 | final ValueChanged onLayoutChangedCallback; 32 | 33 | Size? _oldSize; 34 | 35 | @override 36 | void performLayout() { 37 | super.performLayout(); 38 | if (_oldSize != null && size != _oldSize) onLayoutChangedCallback(size); 39 | _oldSize = size; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/window/window.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:vvibe/common/values/values.dart'; 3 | import 'package:vvibe/services/event_bus.dart'; 4 | import 'package:window_manager/window_manager.dart'; 5 | 6 | /* 自定义窗口外观 */ 7 | class VWindow { 8 | static VWindow _instance = new VWindow._(); 9 | factory VWindow() => _instance; 10 | 11 | VWindow._(); 12 | initWindow() async { 13 | WindowOptions windowOptions = const WindowOptions( 14 | size: Size(1280, 1280 * 9 / 16 + 30), 15 | minimumSize: Size(1280, 1280 * 9 / 16 + 30), 16 | center: true, 17 | backgroundColor: Colors.transparent, 18 | skipTaskbar: false, 19 | titleBarStyle: TitleBarStyle.hidden, 20 | title: 'vvibe', 21 | ); 22 | windowManager.waitUntilReadyToShow(windowOptions).then((_) async { 23 | await windowManager.show(); 24 | await windowManager.focus(); 25 | }); 26 | } 27 | 28 | //设置window title 29 | void setWindowTitle([String? title, String? icon]) { 30 | eventBus.emit('set-window-title', 31 | title != null && title.isNotEmpty ? title : APP_NAME); 32 | setWindowIcon(icon); 33 | } 34 | 35 | //设置window icon 36 | void setWindowIcon( 37 | String? icon, 38 | ) { 39 | eventBus.emit('set-window-icon', icon ?? ''); 40 | } 41 | 42 | // 显示隐藏标题栏 43 | void showTitleBar([bool show = false]) { 44 | eventBus.emit('show-title-bar', show); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/utils/color_util.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: moxun33 3 | * @Date: 2022-09-09 21:59:47 4 | * @LastEditors: moxun33 5 | * @LastEditTime: 2023-02-12 15:53:26 6 | * @FilePath: \vvibe\lib\utils\color_util.dart 7 | * @Description: 8 | * @qmj 9 | */ 10 | import 'dart:ui'; 11 | 12 | class ColorUtil { 13 | /// 十六进制颜色, 14 | /// hex, 十六进制值,例如:0xffffff, 15 | /// alpha, 透明度 [0.0,1.0] 16 | static Color hexColor(int hex, {double alpha = 1}) { 17 | if (alpha < 0) { 18 | alpha = 0; 19 | } else if (alpha > 1) { 20 | alpha = 1; 21 | } 22 | return Color.fromRGBO((hex & 0xFF0000) >> 16, (hex & 0x00FF00) >> 8, 23 | (hex & 0x0000FF) >> 0, alpha); 24 | } 25 | 26 | /// String is in the format "aabbcc" or "ffaabbcc" with an optional leading "#". 27 | static Color fromHex(String hexString) { 28 | final buffer = StringBuffer(); 29 | if (hexString.length == 6 || hexString.length == 7) buffer.write('ff'); 30 | buffer.write(hexString.replaceFirst('#', '')); 31 | return Color( 32 | int.parse(buffer.toString().replaceAll('0x00', '0xff'), radix: 16)); 33 | } 34 | 35 | //十进制转颜色 36 | static Color fromDecimal(String? num) { 37 | try { 38 | if (!(num != null && num.isNotEmpty)) return ColorUtil.fromHex('FFFFFF'); 39 | return ColorUtil.fromHex(int.parse(num.toString(), radix: 16).toString()); 40 | } catch (e) { 41 | return ColorUtil.fromHex('FFFFFF'); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/models/url_sniff_res.dart: -------------------------------------------------------------------------------- 1 | // To parse this JSON data, do 2 | // 3 | // final mediaInfo = mediaInfoFromJson(jsonString); 4 | 5 | import 'dart:convert'; 6 | 7 | import 'package:vvibe/common/values/enum.dart'; 8 | import 'package:vvibe/models/ffprobe_info.dart'; 9 | 10 | UrlSniffRes mediaInfoFromJson(String str) => 11 | UrlSniffRes.fromJson(json.decode(str)); 12 | 13 | String mediaInfoToJson(UrlSniffRes data) => json.encode(data.toJson()); 14 | 15 | class UrlSniffRes { 16 | UrlSniffRes({ 17 | this.url, 18 | this.status, 19 | this.statusCode, 20 | this.mediaInfo, 21 | this.ipInfo, 22 | this.index, 23 | this.duration, 24 | }); 25 | 26 | String? url; 27 | UrlSniffResStatus? status; 28 | int? statusCode; 29 | int? index; 30 | FFprobeInfo? mediaInfo; 31 | String? ipInfo; 32 | int? duration; 33 | 34 | factory UrlSniffRes.fromJson(Map json) => UrlSniffRes( 35 | url: json["url"], 36 | status: json["status"], 37 | statusCode: json["statusCode"], 38 | mediaInfo: json["mediaInfo"], 39 | ipInfo: json["ipInfo"], 40 | index: json["index"], 41 | duration: json["duration"], 42 | ); 43 | 44 | Map toJson() => { 45 | "url": url, 46 | "status": status, 47 | "statusCode": statusCode, 48 | "mediaInfo": mediaInfo, 49 | "ipInfo": ipInfo, 50 | "index": index, 51 | "duration": duration, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /lib/utils/local_storage.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | /// 本地存储-单例模式 5 | class LoacalStorage { 6 | static LoacalStorage _instance = new LoacalStorage._(); 7 | factory LoacalStorage() => _instance; 8 | static late SharedPreferences _prefs; 9 | 10 | LoacalStorage._(); 11 | 12 | static Future init() async { 13 | _prefs = await SharedPreferences.getInstance(); 14 | } 15 | 16 | Future setJSON(String key, dynamic jsonVal) { 17 | String jsonString = jsonEncode(jsonVal); 18 | return _prefs.setString(key, jsonString); 19 | } 20 | 21 | dynamic getJSON(String key) { 22 | String? jsonString = _prefs.getString(key); 23 | return jsonString == null ? null : jsonDecode(jsonString); 24 | } 25 | 26 | Future setBool(String key, bool val) { 27 | return _prefs.setBool(key, val); 28 | } 29 | 30 | bool getBool(String key) { 31 | bool? val = _prefs.getBool(key); 32 | return val == null ? false : val; 33 | } 34 | 35 | Future setString(String key, String val) { 36 | return _prefs.setString(key, val); 37 | } 38 | 39 | String getString(String key) { 40 | return _prefs.getString(key) ?? ''; 41 | } 42 | 43 | Future setStringList(String key, List val) { 44 | return _prefs.setStringList(key, val); 45 | } 46 | 47 | List getStringList(String key) { 48 | return _prefs.getStringList(key) ?? []; 49 | } 50 | 51 | Future remove(String key) { 52 | return _prefs.remove(key); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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 | 15 | void fl_register_plugins(FlPluginRegistry* registry) { 16 | g_autoptr(FlPluginRegistrar) fvp_registrar = 17 | fl_plugin_registry_get_registrar_for_plugin(registry, "FvpPlugin"); 18 | fvp_plugin_register_with_registrar(fvp_registrar); 19 | g_autoptr(FlPluginRegistrar) native_context_menu_registrar = 20 | fl_plugin_registry_get_registrar_for_plugin(registry, "NativeContextMenuPlugin"); 21 | native_context_menu_plugin_register_with_registrar(native_context_menu_registrar); 22 | g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = 23 | fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); 24 | screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); 25 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 26 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 27 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 28 | g_autoptr(FlPluginRegistrar) window_manager_registrar = 29 | fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); 30 | window_manager_plugin_register_with_registrar(window_manager_registrar); 31 | } 32 | -------------------------------------------------------------------------------- /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 | analyzer: 11 | errors: 12 | must_be_immutable: ignore 13 | include: package:flutter_lints/flutter.yaml 14 | 15 | linter: 16 | # The lint rules applied to this project can be customized in the 17 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 18 | # included above or to enable additional rules. A list of all available lints 19 | # and their documentation is published at 20 | # https://dart-lang.github.io/linter/lints/index.html. 21 | # 22 | # Instead of disabling a lint rule for the entire project in the 23 | # section below, it can also be suppressed for a single line of code 24 | # or a specific dart file by using the `// ignore: name_of_lint` and 25 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 26 | # producing the lint. 27 | rules: 28 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 29 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 30 | 31 | # Additional information about this file can be found at 32 | # https://dart.dev/guides/language/analysis-options 33 | -------------------------------------------------------------------------------- /.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: "17025dd88227cd9532c33fa78f5250d548d87e9a" 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: 17025dd88227cd9532c33fa78f5250d548d87e9a 17 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 18 | - platform: android 19 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 20 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 21 | - platform: ios 22 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 23 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 24 | - platform: linux 25 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 26 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 27 | - platform: macos 28 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 29 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 30 | - platform: web 31 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 32 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 33 | - platform: windows 34 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 35 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /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_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 37 | 38 | # Run the Flutter tool portions of the build. This must not be removed. 39 | add_dependencies(${BINARY_NAME} flutter_assemble) 40 | -------------------------------------------------------------------------------- /lib/models/playlist_info.dart: -------------------------------------------------------------------------------- 1 | // To parse this JSON data, do 2 | // 3 | // final playListInfo = playListInfoFromJson(jsonString); 4 | 5 | import 'dart:convert'; 6 | 7 | import 'package:vvibe/models/playlist_item.dart'; 8 | 9 | PlayListInfo playListInfoFromJson(String str) => 10 | PlayListInfo.fromJson(json.decode(str)); 11 | 12 | String playListInfoToJson(PlayListInfo data) => json.encode(data.toJson()); 13 | 14 | class PlayListInfo { 15 | String? tvgUrl; 16 | String? catchup; 17 | String? catchupSource; 18 | bool? showLogo; 19 | bool? checkAlive; 20 | bool? deinterlace; 21 | List channels; 22 | 23 | PlayListInfo({ 24 | this.tvgUrl, 25 | this.catchup, 26 | this.catchupSource, 27 | this.showLogo, 28 | this.deinterlace, 29 | this.checkAlive, 30 | required this.channels, 31 | }); 32 | 33 | factory PlayListInfo.fromJson(Map json) => PlayListInfo( 34 | tvgUrl: json["x-tvg-url"] ?? json["tvg-url"] ?? json["tvgUrl"], 35 | catchup: json["catchup"], // "append", 36 | catchupSource: json[ 37 | "catchup-source"], //"?playseek=\${(b)yyyyMMddHHmmss}-\${(e)yyyyMMddHHmmss}", 38 | showLogo: json["x-show-logo"] ?? json["show-logo"] ?? json["showLogo"], 39 | deinterlace: json["deinterlace"], 40 | checkAlive: 41 | json["x-check-alive"] ?? json["check-alive"] ?? json["checkAlive"], 42 | 43 | channels: json["channels"] == null 44 | ? [] 45 | : List.from(json["channels"]!.map((x) => x)), 46 | ); 47 | 48 | Map toJson() => { 49 | "tvgUrl": tvgUrl, 50 | "catchup": catchup, 51 | "catchupSource": catchupSource, 52 | "showLogo": showLogo, 53 | "deinterlace": deinterlace, 54 | "checkAlive": checkAlive, 55 | "channels": List.from(channels.map((x) => x)), 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /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 | std::string utf8_string; 52 | if (target_length == 0 || target_length > utf8_string.max_size()) { 53 | return utf8_string; 54 | } 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /lib/window/sub_window.dart: -------------------------------------------------------------------------------- 1 | //子窗口 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 5 | //import 'package:desktop_multi_window/desktop_multi_window.dart'; 6 | 7 | class SubWindow extends StatefulWidget { 8 | const SubWindow( 9 | {Key? key, 10 | //required this.windowController, 11 | required this.args, 12 | required this.title, 13 | required this.child, 14 | required this.theme}) 15 | : super(key: key); 16 | final ThemeData theme; 17 | //final WindowController windowController; 18 | final Map? args; 19 | final String title; 20 | final Widget child; 21 | 22 | @override 23 | _SubWindowState createState() => _SubWindowState(); 24 | } 25 | 26 | class _SubWindowState extends State { 27 | @override 28 | void initState() { 29 | super.initState(); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return MaterialApp( 35 | home: widget.child, 36 | title: widget.title, 37 | /* WindowScaffold(NotificationListener( 38 | onNotification: (VSizeChangedLayoutNotification notification) { 39 | if (notification.size.width < 1200 || 40 | notification.size.height < 700) { 41 | widget.windowController 42 | ..setFrame(const Offset(0, 0) & const Size(1280, 720+CUS_WIN_TITLEBAR_HEIGHT)) 43 | ..center(); 44 | } 45 | return true; 46 | }, 47 | child: VSizeChangedLayoutNotifier( 48 | child: Container( 49 | child: widget.child, 50 | constraints: BoxConstraints(minWidth: 1200, minHeight: 700), 51 | )))), */ 52 | theme: widget.theme, 53 | debugShowCheckedModeBanner: false, 54 | builder: EasyLoading.init(), 55 | locale: Locale('zh', 'Hans'), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/components/player/settings/setting_tabs.dart: -------------------------------------------------------------------------------- 1 | //设置modal的标签页 2 | import 'package:flutter/material.dart'; 3 | import 'package:vvibe/common/colors/colors.dart'; 4 | import 'package:vvibe/components/player/settings/player_settings.dart'; 5 | import 'package:vvibe/components/player/settings/playlist_subscription.dart'; 6 | 7 | class SettingsTabsView extends StatefulWidget { 8 | @override 9 | _SettingsTabsViewState createState() => _SettingsTabsViewState(); 10 | } 11 | 12 | class _SettingsTabsViewState extends State 13 | with TickerProviderStateMixin { 14 | final tabs = [ 15 | {'value': 'subscribe', 'label': '订阅配置'}, 16 | {'value': 'player', 'label': '播放设置'} 17 | ]; 18 | late TabController _tabController = TabController( 19 | length: tabs.length, 20 | vsync: this, 21 | ); 22 | @override 23 | void initState() { 24 | super.initState(); 25 | } 26 | 27 | @override 28 | void dispose() { 29 | super.dispose(); 30 | } 31 | 32 | Widget _buildTabBar() => TabBar( 33 | onTap: (tab) => print(tab), 34 | labelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), 35 | unselectedLabelStyle: TextStyle(fontSize: 14), 36 | isScrollable: true, 37 | labelColor: AppColors.primaryColor, 38 | indicatorWeight: 2, 39 | tabAlignment: TabAlignment.center, 40 | indicatorSize: TabBarIndicatorSize.label, 41 | controller: _tabController, 42 | unselectedLabelColor: Colors.black87, 43 | indicatorColor: AppColors.primaryColor, 44 | tabs: tabs.map((e) => Tab(text: e['label'])).toList(), 45 | ); 46 | 47 | @override 48 | Widget build(BuildContext context) { 49 | return Column( 50 | children: [ 51 | _buildTabBar(), 52 | Expanded( 53 | flex: 1, 54 | child: TabBarView( 55 | controller: _tabController, 56 | children: [PlaylistSubscription(), PlayerSettings()], 57 | ), 58 | ) 59 | ], 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /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 | return true; 30 | } 31 | 32 | void FlutterWindow::OnDestroy() { 33 | if (flutter_controller_) { 34 | flutter_controller_ = nullptr; 35 | } 36 | 37 | Win32Window::OnDestroy(); 38 | } 39 | 40 | LRESULT 41 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 42 | WPARAM const wparam, 43 | LPARAM const lparam) noexcept { 44 | // Give Flutter, including plugins, an opportunity to handle window messages. 45 | if (flutter_controller_) { 46 | std::optional result = 47 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 48 | lparam); 49 | if (result) { 50 | return *result; 51 | } 52 | } 53 | 54 | switch (message) { 55 | case WM_FONTCHANGE: 56 | flutter_controller_->engine()->ReloadSystemFonts(); 57 | break; 58 | } 59 | 60 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 61 | } 62 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Moxx 3 | * @Date: 2022-09-13 14:05:05 4 | * @LastEditors: moxun33 5 | * @LastEditTime: 2024-08-18 13:07:35 6 | * @FilePath: \vvibe\lib\main.dart 7 | * @Description: 8 | * @qmj 9 | */ 10 | import 'package:chinese_font_library/chinese_font_library.dart'; 11 | import 'package:flutter/material.dart'; 12 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 13 | import 'package:vvibe/common/values/consts.dart'; 14 | import 'package:vvibe/global.dart'; 15 | import 'package:vvibe/pages/home/vplayer.dart'; 16 | import 'package:vvibe/window/window.dart'; 17 | import 'package:vvibe/window/window_widgets.dart'; 18 | 19 | void main(List args) async { 20 | final theme = await Global.init(); 21 | 22 | await VWindow().initWindow(); 23 | runApp(MyApp(theme: theme ?? ThemeData())); 24 | /* if (args.firstOrNull == 'multi_window') { 25 | final windowId = int.parse(args[1]); 26 | final argument = args[2].isEmpty 27 | ? const {} 28 | : jsonDecode(args[2]) as Map; 29 | MyLogger.info('multi window argument $argument'); 30 | Global.init(shouldSetSize: false).then((theme) { 31 | runApp(LiveSniffWin( 32 | theme: theme ?? ThemeData(), 33 | windowController: WindowController.fromWindowId(windowId), 34 | args: argument, 35 | )); 36 | VWindow().initWindow(); 37 | }); 38 | } else { 39 | Global.init().then((theme) { 40 | runApp(MyApp(theme: theme ?? ThemeData())); 41 | VWindow().initWindow(); 42 | }); 43 | } */ 44 | } 45 | 46 | class MyApp extends StatelessWidget { 47 | final ThemeData theme; 48 | const MyApp({Key? key, required this.theme}) : super(key: key); 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | return MaterialApp( 53 | color: Colors.black12, 54 | title: APP_NAME, 55 | theme: theme.useSystemChineseFont(Brightness.dark), 56 | home: WindowScaffold( 57 | child: Vplayer(), 58 | ), 59 | debugShowCheckedModeBanner: false, 60 | builder: EasyLoading.init(), 61 | locale: Locale('zh', 'Hans'), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/utils/ffi_util.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Moxx 3 | * @Date: 2022-09-15 15:59:57 4 | * @LastEditors: moxun33 5 | * @LastEditTime: 2023-02-07 13:24:10 6 | * @FilePath: \vvibe\lib\utils\ffi_util.dart 7 | * @Description: 8 | * @qmj 9 | */ 10 | /*import 'dart:convert'; 11 | import 'dart:ffi'; 12 | 13 | import 'dart:io'; 14 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 15 | import 'package:vvibe/bridge_generated.dart'; 16 | import 'package:vvibe/common/values/values.dart'; 17 | import 'package:vvibe/models/ffprobe_info.dart'; 18 | 19 | // Re-export the bridge so it is only necessary to import this file. 20 | export 'package:vvibe/bridge_generated.dart'; 21 | 22 | const _base = 'native'; 23 | 24 | // On MacOS, the dynamic library is not bundled with the binary, 25 | // but rather directly **linked** against the binary. 26 | final _dylib = Platform.isWindows ? '$_base.dll' : 'lib$_base.so'; 27 | 28 | final Native api = NativeImpl(Platform.isIOS || Platform.isMacOS 29 | ? DynamicLibrary.executable() 30 | : DynamicLibrary.open(_dylib)); 31 | 32 | class FfiUtil { 33 | static FfiUtil _instance = new FfiUtil._(); 34 | factory FfiUtil() => _instance; 35 | 36 | FfiUtil._(); 37 | 38 | //解析ip地址信息 39 | Future getIpInfo(String ip) async { 40 | try { 41 | String dir = Directory.current.path; 42 | print('app dir $dir'); 43 | String addr = await api.getIpInfo( 44 | ip: ip, dbPath: File('${ASSETS_DIR}/ip2region.xdb').path); 45 | return addr.replaceAll('0|', ''); 46 | } catch (e) { 47 | EasyLoading.showError(e.toString()); 48 | return null; 49 | } 50 | } 51 | 52 | //获取url的媒体元数据 53 | Future getMediaInfo(String url) async { 54 | try { 55 | //'${Directory(ASSETS_DIR).path}/ffprobe.exe' 56 | String raw = await api.getMediaInfo(url: url); 57 | FFprobeInfo info = FFprobeInfo.fromJson(jsonDecode(raw)); 58 | return info; 59 | } catch (e) { 60 | //EasyLoading.showError(e.toString()); 61 | print('媒体信息获取失败$url ${e.toString()}'); 62 | return null; 63 | } 64 | } 65 | } 66 | */ -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | //#include "flutter_native_view/flutter_native_view_plugin.h" 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | //#include 9 | 10 | //auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); 11 | #include 12 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 13 | _In_ wchar_t *command_line, _In_ int show_command) { 14 | // Replace uni_links_desktop_example with your_window_title. 15 | HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"vvibe"); 16 | if (hwnd != NULL) { 17 | DispatchToUniLinksDesktop(hwnd); 18 | 19 | ::ShowWindow(hwnd, SW_NORMAL); 20 | ::SetForegroundWindow(hwnd); 21 | return EXIT_FAILURE; 22 | } 23 | // Attach to console when present (e.g., 'flutter run') or create a 24 | // new console when running with a debugger. 25 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 26 | CreateAndAttachConsole(); 27 | } 28 | 29 | // Initialize COM, so that it is available for use in the library and/or 30 | // plugins. 31 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 32 | 33 | flutter::DartProject project(L"data"); 34 | 35 | std::vector command_line_arguments = 36 | GetCommandLineArguments(); 37 | 38 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 39 | 40 | FlutterWindow window(project); 41 | Win32Window::Point origin(10, 10); 42 | Win32Window::Size size(1280, 750); 43 | 44 | if (!window.CreateAndShow(L"vvibe", origin, size)) { 45 | return EXIT_FAILURE; 46 | } 47 | window.SetQuitOnClose(true); 48 | 49 | // flutternativeview::NativeViewContainer::GetInstance()->Create(); 50 | 51 | ::MSG msg; 52 | while (::GetMessage(&msg, nullptr, 0, 0)) { 53 | ::TranslateMessage(&msg); 54 | ::DispatchMessage(&msg); 55 | } 56 | 57 | ::CoUninitialize(); 58 | return EXIT_SUCCESS; 59 | } 60 | -------------------------------------------------------------------------------- /lib/models/channel_epg.dart: -------------------------------------------------------------------------------- 1 | // To parse this JSON data, do 2 | // 3 | // final channelEpg = channelEpgFromJson(jsonString); 4 | 5 | import 'dart:convert'; 6 | 7 | import 'package:vvibe/utils/playlist/epg_util.dart'; 8 | 9 | ChannelEpg channelEpgFromJson(String str) => 10 | ChannelEpg.fromJson(json.decode(str)); 11 | 12 | String channelEpgToJson(ChannelEpg data) => json.encode(data.toJson()); 13 | 14 | class ChannelEpg { 15 | ChannelEpg({ 16 | required this.date, 17 | required this.name, 18 | this.url, 19 | this.id, 20 | required this.epg, 21 | }); 22 | 23 | DateTime date; 24 | int? id; 25 | String name; 26 | String? url; 27 | List epg; 28 | 29 | factory ChannelEpg.fromJson(Map json) => ChannelEpg( 30 | date: DateTime.parse(json["date"]), 31 | id: json["id"], 32 | name: json["name"], 33 | url: json["url"], 34 | epg: List.from( 35 | (json["epg"] ?? json["epg_data"]).map((x) => EpgDatum.fromJson(x))), 36 | ); 37 | 38 | Map toJson() => { 39 | "date": 40 | "${date.year.toString().padLeft(4, '0')}${date.month.toString().padLeft(2, '0')}${date.day.toString().padLeft(2, '0')}", 41 | "name": name, 42 | "id": id, 43 | "url": url, 44 | "epg": List.from(epg.map((x) => x.toJson())), 45 | }; 46 | } 47 | 48 | class EpgDatum { 49 | EpgDatum({ 50 | required this.start, 51 | this.desc, 52 | required this.end, 53 | required this.title, 54 | }); 55 | 56 | DateTime start; 57 | DateTime end; 58 | String title; 59 | String? desc; 60 | 61 | factory EpgDatum.fromJson(Map json) => EpgDatum( 62 | start: EpgUtil().parseEpgTime(json["start"]), 63 | end: EpgUtil().parseEpgTime(json["end"] ?? json['stop']), 64 | title: json["title"], 65 | desc: json["desc"] is String ? json["desc"] : '', 66 | ); 67 | 68 | Map toJson() => { 69 | "start": start, 70 | "desc": desc, 71 | "end": end, 72 | "title": title, 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /lib/models/playlist_item.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Moxx 3 | * @Date: 2022-09-15 15:59:57 4 | * @LastEditors: moxun33 5 | * @LastEditTime: 2023-02-17 15:51:05 6 | * @FilePath: \vvibe\lib\models\playlist_item.dart 7 | * @Description: 8 | * @qmj 9 | */ 10 | // To parse this JSON data, do 11 | // 12 | // final playListItem = playListItemFromJson(jsonString); 13 | 14 | import 'dart:convert'; 15 | 16 | /* { 17 | "name":"", 18 | "group":"", 19 | "url":"", 20 | "tvgId":"", 21 | "tvgName":"", 22 | "tvgLogo":"", 23 | "catchup":"append", 24 | "catchup-source":"?playseek=${(b)yyyyMMddHHmmss}-${(e)yyyyMMddHHmmss}" 25 | } */ 26 | PlayListItem playListItemFromJson(String str) => 27 | PlayListItem.fromJson(json.decode(str)); 28 | 29 | String playListItemToJson(PlayListItem data) => json.encode(data.toJson()); 30 | 31 | class PlayListItem { 32 | PlayListItem({ 33 | required this.name, 34 | this.group, 35 | required this.url, 36 | this.tvgId, 37 | this.tvgName, 38 | this.tvgLogo, 39 | this.catchup, 40 | this.catchupSource, 41 | this.ext, 42 | }); 43 | 44 | String name; 45 | String? group; 46 | String url; 47 | String? tvgId; 48 | String? tvgName; 49 | String? tvgLogo; 50 | String? catchup; 51 | String? catchupSource; 52 | 53 | Map? 54 | ext; //平台代理等扩展配置{'bakUrls':['备用链接列表'] 'platformHit': false, 'douyu': matchDy, 'huya': matchHy, 'bilibili': matchBl ,'playUrl':'url',manifestType:'mpd',licenseType:'clearKey',licenseKey:'key'} 55 | 56 | factory PlayListItem.fromJson(Map json) => PlayListItem( 57 | name: json["name"] ?? '未命名', 58 | group: json["group"], 59 | url: json["url"] ?? '', 60 | tvgId: json["tvgId"], 61 | tvgName: json["tvgName"], 62 | tvgLogo: json["tvgLogo"], 63 | catchup: json["catchup"], 64 | catchupSource: json["catchup-source"], 65 | ext: Map.from(json['ext'] ?? {})); 66 | 67 | Map toJson() => { 68 | "name": name, 69 | "group": group, 70 | "url": url, 71 | "tvgId": tvgId, 72 | "tvgName": tvgName, 73 | "tvgLogo": tvgLogo, 74 | "catchup": catchup, 75 | "catchup-source": catchupSource, 76 | 'ext': ext 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /lib/utils/unilink_sub.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:uni_links/uni_links.dart'; 6 | 7 | class UnilinkSub { 8 | StreamSubscription? _sub; 9 | bool _initialUriIsHandled = false; 10 | Uri? initialUri; 11 | Uri? latestUri; 12 | Object? err; 13 | 14 | /// Handle incoming links - the ones that the app will recieve from the OS 15 | /// while already started. 16 | void handleIncomingLinks(void onIncomingLinks(Uri? uri, Object? err)) { 17 | if (!kIsWeb) { 18 | // It will handle app links while the app is already started - be it in 19 | // the foreground or in the background. 20 | _sub = uriLinkStream.listen((Uri? uri) { 21 | print('got uri: $uri'); 22 | 23 | latestUri = uri; 24 | err = null; 25 | onIncomingLinks(uri, err); 26 | }, onError: (Object err) { 27 | print('got err: $err'); 28 | 29 | latestUri = null; 30 | if (err is FormatException) { 31 | err = err; 32 | } else { 33 | err = Null; 34 | } 35 | onIncomingLinks(latestUri, err); 36 | }); 37 | } 38 | } 39 | 40 | /// Handle the initial Uri - the one the app was started with 41 | /// 42 | /// **ATTENTION**: `getInitialLink`/`getInitialUri` should be handled 43 | /// ONLY ONCE in your app's lifetime, since it is not meant to change 44 | /// throughout your app's life. 45 | /// 46 | /// We handle all exceptions, since it is called from initState. 47 | Future handleInitialUri() async { 48 | // In this example app this is an almost useless guard, but it is here to 49 | // show we are not going to call getInitialUri multiple times, even if this 50 | // was a weidget that will be disposed of (ex. a navigation route change). 51 | 52 | try { 53 | final uri = await getInitialUri(); 54 | 55 | print('got initial uri: $uri'); 56 | initialUri = uri; 57 | return uri; 58 | } on PlatformException { 59 | // Platform messages may fail but we ignore the exception 60 | print('falied to get initial uri'); 61 | } on FormatException catch (e) { 62 | print('malformed initial uri: $e'); 63 | err = e; 64 | } 65 | } 66 | 67 | void destroy() { 68 | _sub?.cancel(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/utils/common.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | 6 | // Define a reusable function 7 | String genRandomStr({int length = 20}) { 8 | final random = Random(); 9 | const availableChars = 10 | 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890_'; 11 | final randomString = List.generate(length, 12 | (index) => availableChars[random.nextInt(availableChars.length)]).join(); 13 | 14 | return randomString; 15 | } 16 | 17 | // 格式化文件大小 18 | String formatFileSize(int bytes) { 19 | if (bytes <= 0) { 20 | return '0 B'; 21 | } 22 | 23 | const List units = ['KB', 'MB', 'GB', 'TB']; 24 | double size = bytes.toDouble(); 25 | int unitIndex = 0; 26 | 27 | while (size >= 1024 && unitIndex < units.length - 1) { 28 | size /= 1024; 29 | unitIndex++; 30 | } 31 | 32 | return '${size.toStringAsFixed(2)} ${units[unitIndex]}'; 33 | } 34 | 35 | class Debouncer { 36 | final int milliseconds; 37 | Timer? _timer; 38 | 39 | Debouncer({required this.milliseconds}); 40 | 41 | void run(VoidCallback action) { 42 | _timer?.cancel(); 43 | _timer = Timer(Duration(milliseconds: milliseconds), action); 44 | } 45 | 46 | void dispose() { 47 | _timer?.cancel(); 48 | } 49 | } 50 | 51 | class Throttler { 52 | final int milliseconds; 53 | Timer? _timer; 54 | bool isExecuted = false; 55 | 56 | Throttler({required this.milliseconds}); 57 | 58 | void run(VoidCallback action) { 59 | if (isExecuted) { 60 | return; 61 | } 62 | 63 | _timer = Timer(Duration(milliseconds: milliseconds), () { 64 | _timer?.cancel(); 65 | isExecuted = false; 66 | }); 67 | isExecuted = true; 68 | action(); 69 | } 70 | 71 | void dispose() { 72 | _timer?.cancel(); 73 | } 74 | } 75 | 76 | // 格式化网速 77 | String formatNetworkSpeed(int speed, [isBits = false]) { 78 | // 将比特转换为字节 79 | double _speed = isBits ? speed / 8.0 : speed.toDouble(); 80 | 81 | // 根据不同的范围格式化显示为 KB, MB, GB 等 82 | List units = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s']; 83 | int unitIndex = 0; 84 | 85 | // 根据速率大小调整单位 86 | while (_speed >= 1024 && unitIndex < units.length - 1) { 87 | _speed /= 1024; 88 | unitIndex++; 89 | } 90 | 91 | return '${_speed.toStringAsFixed(unitIndex > 1 ? 1 : 0)} ${units[unitIndex]}'; 92 | } 93 | -------------------------------------------------------------------------------- /lib/services/danmaku/danmaku_service.dart: -------------------------------------------------------------------------------- 1 | //开始连接斗鱼、忽悠、b站的弹幕 2 | import 'package:vvibe/models/live_danmaku_item.dart'; 3 | import 'package:vvibe/models/playlist_item.dart'; 4 | import 'package:vvibe/services/danmaku/danmaku_type.dart'; 5 | import 'package:vvibe/services/services.dart'; 6 | import 'package:vvibe/utils/logger.dart'; 7 | 8 | class DanmakuService { 9 | static DanmakuService _instance = new DanmakuService._(); 10 | factory DanmakuService() => _instance; 11 | 12 | DanmakuService._(); 13 | DouyuDnamakuService? _dy; 14 | BilibiliDanmakuService? _bl; 15 | HuyaDanmakuService? _hy; 16 | 17 | bool canConnDanmaku(PlayListItem item, RegExp groupReg, RegExp proxyUrlReg) { 18 | try { 19 | final groupMatch = item.group?.contains(groupReg) ?? false; 20 | final uri = Uri.parse(item.url.trim()), 21 | urlMatch1 = uri.path.contains(proxyUrlReg); 22 | 23 | return groupMatch || urlMatch1; 24 | } catch (e) { 25 | return false; 26 | } 27 | } 28 | 29 | //开始连接斗鱼、虎牙、b站的弹幕 30 | void start( 31 | PlayListItem item, void renderDanmaku(LiveDanmakuItem? data)) async { 32 | try { 33 | stop(); 34 | 35 | if (!(item.tvgId != null && item.tvgId!.isNotEmpty)) return; 36 | final String rid = item.tvgId!; 37 | MyLogger.info('即将登录弹幕 ${item.group} ${item.name} ${item.tvgId}'); 38 | final ext = item.ext ?? {}; 39 | if (canConnDanmaku( 40 | item, DanmakuType.douyuGroupReg, DanmakuType.douyuProxyUrlReg) || 41 | ext['douyu'] == true) { 42 | _dy = DouyuDnamakuService(roomId: rid, onDanmaku: renderDanmaku); 43 | _dy!.connect(); 44 | } else if (canConnDanmaku( 45 | item, DanmakuType.biliGroupReg, DanmakuType.biliProxyUrlReg) || 46 | ext['bilibili'] == true) { 47 | _bl = BilibiliDanmakuService(roomId: rid, onDanmaku: renderDanmaku); 48 | _bl?.connect(); 49 | } else if (canConnDanmaku( 50 | item, DanmakuType.huyaGroupReg, DanmakuType.huyaProxyUrlReg) || 51 | ext['huya'] == true) { 52 | _hy = HuyaDanmakuService(roomId: rid, onDanmaku: renderDanmaku); 53 | _hy?.connect(); 54 | } 55 | } catch (e) { 56 | MyLogger.error(e.toString()); 57 | } 58 | } 59 | 60 | //断开所有弹幕连接 61 | void stop() { 62 | try { 63 | _dy?.dispose(); 64 | 65 | _bl?.displose(); 66 | 67 | _hy?.displose(); 68 | } catch (e) {} 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/components/player/epg/epg_date_tabs.dart: -------------------------------------------------------------------------------- 1 | //设置modal的标签页 2 | import 'package:flutter/material.dart'; 3 | import 'package:vvibe/components/player/epg/epg_channel_date.dart'; 4 | import 'package:vvibe/models/playlist_item.dart'; 5 | import 'package:vvibe/utils/playlist/epg_util.dart'; 6 | 7 | class EpgDateTabsView extends StatefulWidget { 8 | const EpgDateTabsView( 9 | {Key? key, required this.urlItem, this.epgUrl, required this.doPlayback}) 10 | : super(key: key); 11 | 12 | final PlayListItem urlItem; 13 | final String? epgUrl; 14 | final Function doPlayback; 15 | @override 16 | _EpgDateTabsViewState createState() => _EpgDateTabsViewState(); 17 | } 18 | 19 | class _EpgDateTabsViewState extends State 20 | with TickerProviderStateMixin { 21 | final tabs = EpgUtil().genWeekDays().map((e) => {'value': e, 'label': e}); 22 | late TabController _tabController = TabController( 23 | length: tabs.length, 24 | initialIndex: tabs.length - 1, 25 | vsync: this, 26 | ); 27 | @override 28 | void initState() { 29 | super.initState(); 30 | } 31 | 32 | @override 33 | void dispose() { 34 | super.dispose(); 35 | } 36 | 37 | Widget _buildTabBar() => TabBar( 38 | onTap: (tab) => {}, 39 | labelStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 40 | unselectedLabelStyle: TextStyle(fontSize: 16), 41 | isScrollable: true, 42 | labelColor: Colors.purple, 43 | indicatorWeight: 3, 44 | controller: _tabController, 45 | indicatorPadding: EdgeInsets.symmetric(horizontal: 10), 46 | unselectedLabelColor: Colors.black87, 47 | indicatorColor: Colors.purple[300], 48 | tabs: tabs.map((e) => Tab(text: e['label'])).toList(), 49 | ); 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return Column( 54 | children: [ 55 | _buildTabBar(), 56 | Expanded( 57 | flex: 1, 58 | child: TabBarView( 59 | controller: _tabController, 60 | children: tabs 61 | .map((e) => EpgChannelDate( 62 | epgUrl: widget.epgUrl, 63 | doPlayback: widget.doPlayback, 64 | urlItem: widget.urlItem, 65 | date: e['value']!, 66 | )) 67 | .toList(), 68 | ), 69 | ) 70 | ], 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/components/player/settings/open_url_dialog.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Moxx 3 | * @Date: 2022-09-13 14:55:49 4 | * @LastEditors: moxun33 5 | * @LastEditTime: 2022-11-14 20:09:08 6 | * @FilePath: \vvibe\lib\components\player\settings\open_url_dialog.dart 7 | * @Description: 打开链接弹窗 8 | * @aqmj 9 | */ 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter/services.dart'; 12 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 13 | import 'package:vvibe/utils/playlist/playlist_util.dart'; 14 | 15 | class OpenUrlDialog extends StatefulWidget { 16 | OpenUrlDialog({Key? key, required this.onOpenUrl}) : super(key: key); 17 | final void Function(String url) onOpenUrl; 18 | @override 19 | _OpenUrlDialogState createState() => _OpenUrlDialogState(); 20 | } 21 | 22 | class _OpenUrlDialogState extends State { 23 | final TextEditingController _urlController = TextEditingController(); 24 | void _openUrl(BuildContext context) { 25 | final url = _urlController.text; 26 | if (url.isEmpty) { 27 | EasyLoading.showError('请输入播放链接'); 28 | return; 29 | } 30 | Navigator.pop(context); 31 | widget.onOpenUrl(url); 32 | } 33 | 34 | void checkClipboard() async { 35 | ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); 36 | if (data != null) { 37 | final valid = PlaylistUtil().validateUrl(data.text ?? ''); 38 | if (valid) { 39 | _urlController.text = data.text!; 40 | } 41 | } 42 | } 43 | 44 | @override 45 | void initState() { 46 | super.initState(); 47 | checkClipboard(); 48 | } 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | return AlertDialog( 53 | title: const Text('打开链接'), 54 | actions: [ 55 | TextButton( 56 | child: const Text('打开'), 57 | onPressed: () { 58 | _openUrl(context); 59 | }), 60 | ], 61 | content: SizedBox( 62 | width: 500, 63 | height: 100, 64 | child: TextField( 65 | autofocus: true, 66 | controller: _urlController, 67 | decoration: InputDecoration( 68 | hintText: "播放链接", 69 | icon: Icon(Icons.add_link_sharp), 70 | suffixIcon: IconButton( 71 | icon: Icon(Icons.close), 72 | onPressed: () { 73 | setState(() { 74 | _urlController.clear(); 75 | }); 76 | }, 77 | )), 78 | ), 79 | ), 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/components/custom_appbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:vvibe/common/colors/colors.dart'; 3 | 4 | /// appbar 返回按钮类型 5 | enum AppBarBackType { Back, Close, None } 6 | 7 | const double kNavigationBarHeight = 44.0; 8 | 9 | // 自定义 AppBar 10 | class MyAppBar extends AppBar implements PreferredSizeWidget { 11 | MyAppBar({ 12 | Key? key, 13 | Widget? title, 14 | AppBarBackType? leadingType, 15 | WillPopCallback? onWillPop, 16 | Widget? leading, 17 | Brightness? brightness, 18 | Color? backgroundColor, 19 | List? actions, 20 | bool centerTitle = true, 21 | double? elevation, 22 | }) : super( 23 | key: key, 24 | title: title, 25 | centerTitle: centerTitle, 26 | backgroundColor: backgroundColor ?? AppColors.primaryBackground, 27 | leading: leading ?? 28 | (leadingType == AppBarBackType.None 29 | ? Container() 30 | : AppBarBack( 31 | leadingType ?? AppBarBackType.Back, 32 | onWillPop: onWillPop, 33 | )), 34 | actions: actions, 35 | elevation: elevation ?? 0.5, 36 | ); 37 | @override 38 | get preferredSize => Size.fromHeight(44); 39 | } 40 | 41 | // 自定义返回按钮 42 | class AppBarBack extends StatelessWidget { 43 | final AppBarBackType _backType; 44 | final Color? color; 45 | final WillPopCallback? onWillPop; 46 | 47 | AppBarBack(this._backType, {this.onWillPop, this.color}); 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | return GestureDetector( 52 | onTap: () async { 53 | final willBack = onWillPop == null ? true : await onWillPop!(); 54 | if (!willBack) return; 55 | Navigator.pop(context); 56 | }, 57 | child: _backType == AppBarBackType.Close 58 | ? Container( 59 | child: Icon(Icons.close, 60 | color: color ?? Color(0xFF222222), size: 24.0), 61 | ) 62 | : Container( 63 | padding: EdgeInsets.only(right: 15), 64 | child: Icon(Icons.arrow_back_ios_new, 65 | size: 24.0, color: Color(0xFF222222)), 66 | ), 67 | ); 68 | } 69 | } 70 | 71 | class MyTitle extends StatelessWidget { 72 | final String _title; 73 | final Color? color; 74 | 75 | MyTitle(this._title, {this.color}); 76 | 77 | @override 78 | Widget build(BuildContext context) { 79 | return Text(_title, 80 | style: TextStyle( 81 | color: color ?? Color(0xFF222222), 82 | fontSize: 18, 83 | fontWeight: FontWeight.w500)); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vvibe 2 | 3 | image 4 | 5 | 6 | ![img](https://img.shields.io/badge/language-dart-blue.svg?color=00ACC1) 7 | ![img](https://img.shields.io/badge/flutter-00B0FF?logo=flutter) 8 | [![img](https://img.shields.io/github/downloads/moxun33/vvibe/total)](https://github.com/moxun33/vvibe/releases) 9 | [![img](https://img.shields.io/github/v/release/moxun33/vvibe?display_name=tag&include_prereleases)](https://github.com/moxun33/vvibe/releases) 10 | ![img](https://img.shields.io/github/license/moxun33/vvibe) 11 | ![img](https://img.shields.io/github/stars/moxun33/vvibe) 12 | ![img](https://img.shields.io/github/issues/moxun33/vvibe) 13 | [![img](https://github.com/moxun33/vvibe/workflows/build/badge.svg)](https://github.com/moxun33/vvibe/actions) 14 | 15 | > 视频直播观看软件 16 | 17 | ## 功能 18 | 19 | - 播放本地文件``m3u``或``txt``播放列表 20 | - 订阅远程``m3u``或``txt``播放列表 21 | - 支持三大平台的实时弹幕 (条件:1、m3u文件的``group-title``分别为(或包含)平台中文名或拼音, ``tvg-id``为真实房间id;2、代理地址,格式为``/douyu.php?id={roomid}``或pathname以``/douyu/{roomid}``结尾) 22 | - ~~发送匿名弹幕 [hack.chat](https://hack.chat)🤩🤩🤩 (科学上网)~~ 23 | - 播放列表管理,分组、搜索和实时检测 24 | - 打开单个网络链接 25 | - 单个网络链接也支持弹幕(条件同播放列表)。 26 | - 播放器基本设置 27 | - 单个网络订阅/本地文件的播放设置:UA、EPG等 28 | - 支持快捷键 29 | - ~~直播源扫描和导出~~ 30 | - ~~扫源时获取IPV4地址信息和媒体信息~~ 31 | 32 | ## 多平台 33 | 34 | 目前仅支持windows、linux,暂无其他平台支持计划 35 | 36 | ## 开发 37 | 38 | - 拉取项目代码,运行``flutter pub get``安装依赖 39 | 40 | - 启动项目 41 | 42 | ## 截图 43 | 44 | ![img](docs/player.png) 45 | ![img](docs/settings.png) 46 | 47 | ## 注意事项 48 | 49 | ## 声明 50 | 51 | - 本项目仅作为个人兴趣项目,仅用于测试与学习交流,不得用于商业用途或其他任何违法行为;使用者使用本项目时,自行承担风险,由使用该项目引发的任何法律纠纷与本人无关。 52 | - 相关资源的版权归原公司所有。 53 | - 测试数据来源于互联网公开内容,没有收集任何私有和有权限的信息(个人信息等),由此引发的任何法律纠纷与本人无关。 54 | - 弹幕接口仅用作测试,请勿用于其他非法途径。若侵权,请联系本人删除。 55 | 56 | ## 致谢 57 | 58 | - [fvp](https://github.com/wang-bin/fvp)([mdk-sdk](https://github.com/wang-bin/mdk-sdk)) 59 | 60 | ## 备注 61 | 62 | - 本应用不内置播放源,请自行准备直播源(源代码playlist目录中播放源仅供开发测试,请勿用于其他途径) 63 | 64 | - 直播平台播放源的解析可参考 [real-url](https://github.com/moxun33/real-url) , 可自行搭建服务器定时解析,推荐使用[青龙](https://github.com/whyour/qinglong),``虎牙``,``斗鱼``和``哔哩哔哩``的直播源解析的青龙脚本 [ql-scripts](https://github.com/moxun33/ql-scripts) 65 | 66 | - 若无法自动下`mdk-sdk`, 手动[下载mdk-sdk](https://sourceforge.net/projects/mdk-sdk/files/mdk-sdk-windows-desktop-vs2022.7z)后解压到 `windows/flutter/ephemeral/.plugin_symlinks/fvp/windows/`目录下; 其他平台操作类似 67 | - 视频播放器`fvp`插件的`API`持续开发中 68 | - ~~ffmpeg下载地址 https://github.com/GyanD/codexffmpeg/releases 本项目的ffmpeg版本为4.4.1~~ 69 | 70 | - 项目还使用或借鉴了未列出的其他项目,同样在此感谢。 71 | 72 | - epg 73 | 74 | ```bash 75 | 76 | http://epg.aptvapp.com/xml 77 | http://epg.51zmt.top:8000/e.xml.gz 78 | https://assets.livednow.com/e.xml.gz 79 | https://live.fanmingming.com/e.xml.gz 80 | https://epg.v1.mk/fy.xml.gz 81 | 82 | ``` 83 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "interactive", 3 | "files.associations": { 4 | "utility": "cpp", 5 | "xtree": "cpp", 6 | "memory": "cpp", 7 | "algorithm": "cpp", 8 | "any": "cpp", 9 | "array": "cpp", 10 | "atomic": "cpp", 11 | "bit": "cpp", 12 | "cctype": "cpp", 13 | "charconv": "cpp", 14 | "chrono": "cpp", 15 | "cinttypes": "cpp", 16 | "clocale": "cpp", 17 | "cmath": "cpp", 18 | "codecvt": "cpp", 19 | "compare": "cpp", 20 | "concepts": "cpp", 21 | "condition_variable": "cpp", 22 | "cstddef": "cpp", 23 | "cstdint": "cpp", 24 | "cstdio": "cpp", 25 | "cstdlib": "cpp", 26 | "cstring": "cpp", 27 | "ctime": "cpp", 28 | "cwchar": "cpp", 29 | "exception": "cpp", 30 | "filesystem": "cpp", 31 | "format": "cpp", 32 | "forward_list": "cpp", 33 | "functional": "cpp", 34 | "future": "cpp", 35 | "initializer_list": "cpp", 36 | "iomanip": "cpp", 37 | "ios": "cpp", 38 | "iosfwd": "cpp", 39 | "iostream": "cpp", 40 | "istream": "cpp", 41 | "iterator": "cpp", 42 | "limits": "cpp", 43 | "list": "cpp", 44 | "locale": "cpp", 45 | "map": "cpp", 46 | "mutex": "cpp", 47 | "new": "cpp", 48 | "optional": "cpp", 49 | "ostream": "cpp", 50 | "random": "cpp", 51 | "ratio": "cpp", 52 | "set": "cpp", 53 | "sstream": "cpp", 54 | "stdexcept": "cpp", 55 | "stop_token": "cpp", 56 | "streambuf": "cpp", 57 | "string": "cpp", 58 | "system_error": "cpp", 59 | "thread": "cpp", 60 | "tuple": "cpp", 61 | "type_traits": "cpp", 62 | "typeinfo": "cpp", 63 | "unordered_map": "cpp", 64 | "variant": "cpp", 65 | "vector": "cpp", 66 | "xfacet": "cpp", 67 | "xhash": "cpp", 68 | "xiosbase": "cpp", 69 | "xlocale": "cpp", 70 | "xlocbuf": "cpp", 71 | "xlocinfo": "cpp", 72 | "xlocmes": "cpp", 73 | "xlocmon": "cpp", 74 | "xlocnum": "cpp", 75 | "xloctime": "cpp", 76 | "xmemory": "cpp", 77 | "xstddef": "cpp", 78 | "xstring": "cpp", 79 | "xtr1common": "cpp", 80 | "xutility": "cpp" 81 | }, 82 | "cmake.configureOnOpen": false, 83 | "editor.suggest.snippetsPreventQuickSuggestions": false, 84 | "aiXcoder.showTrayIcon": true, 85 | "cmake.sourceDirectory": "${workspaceFolder}/build/windows/_deps/corrosion-src", 86 | "git.ignoreLimitWarning": true 87 | } -------------------------------------------------------------------------------- /lib/components/spinning.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | //页面加载指示器 4 | class Spinning extends StatefulWidget { 5 | Spinning({Key? key}) : super(key: key); 6 | 7 | @override 8 | _SpinningState createState() { 9 | return _SpinningState(); 10 | } 11 | } 12 | 13 | class _SpinningState extends State with TickerProviderStateMixin { 14 | late final AnimationController _controller = AnimationController( 15 | duration: const Duration(seconds: 1), 16 | vsync: this, 17 | )..repeat(reverse: false); 18 | 19 | // Create an animation with value of type "double" 20 | late final Animation _animation = CurvedAnimation( 21 | parent: _controller, 22 | curve: Curves.linear, 23 | ); 24 | 25 | @override 26 | void dispose() { 27 | _controller.dispose(); 28 | super.dispose(); 29 | } 30 | 31 | Widget Cirle(Color color) => Container( 32 | width: 20, 33 | height: 20, 34 | margin: const EdgeInsets.all(2), 35 | decoration: BoxDecoration(color: color, shape: BoxShape.circle), 36 | ); 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | // TODO: implement build 41 | return Center( 42 | child: RotationTransition( 43 | turns: _animation, 44 | child: Container( 45 | width: 40, 46 | color: Colors.transparent, 47 | padding: const EdgeInsets.all(8.0), 48 | child: Wrap( 49 | children: [ 50 | Icon( 51 | Icons.filter_vintage_sharp, 52 | color: Colors.purple, 53 | ), 54 | ], 55 | ), 56 | ), 57 | ), 58 | ); 59 | // This button is used to pause/resume the animation 60 | } 61 | } 62 | 63 | //小尺寸加载指示器 64 | class SmallSpinning extends StatefulWidget { 65 | const SmallSpinning({Key? key}) : super(key: key); 66 | 67 | @override 68 | _SmallSpinningState createState() => _SmallSpinningState(); 69 | } 70 | 71 | class _SmallSpinningState extends State 72 | with TickerProviderStateMixin { 73 | late final AnimationController _controller = AnimationController( 74 | duration: const Duration(seconds: 1), 75 | vsync: this, 76 | )..repeat(reverse: false); 77 | 78 | // Create an animation with value of type "double" 79 | late final Animation _animation = CurvedAnimation( 80 | parent: _controller, 81 | curve: Curves.linear, 82 | ); 83 | @override 84 | void dispose() { 85 | _controller.dispose(); 86 | super.dispose(); 87 | } 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | return RotationTransition( 92 | turns: _animation, 93 | child: Icon( 94 | size: 10, 95 | Icons.filter_vintage_sharp, 96 | color: Colors.purple, 97 | ), 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/components/sniff/sniff_res_table.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:vvibe/common/values/enum.dart'; 4 | import 'package:vvibe/models/url_sniff_res.dart'; 5 | import 'package:vvibe/utils/playlist/sniff_util.dart'; 6 | 7 | // ignore: must_be_immutable 8 | class SniffResTable extends StatelessWidget { 9 | SniffResTable({Key? key, required this.data, required this.validOnly}) 10 | : super(key: key); 11 | List data = []; 12 | bool validOnly = false; 13 | //渲染状态标签 14 | Widget _renderStatus(UrlSniffResStatus? status) { 15 | Color color = Colors.black; 16 | String text = ''; 17 | switch (status) { 18 | case UrlSniffResStatus.success: 19 | color = Colors.green; 20 | text = '有效'; 21 | break; 22 | case UrlSniffResStatus.timeout: 23 | color = Colors.black; 24 | text = '超时'; 25 | break; 26 | default: 27 | color = Colors.red; 28 | text = '无效'; 29 | break; 30 | } 31 | return Text( 32 | text, 33 | textAlign: TextAlign.center, 34 | style: TextStyle(fontSize: 14, color: color), 35 | ); 36 | } 37 | 38 | DataColumn _columnHeader(String text) { 39 | return DataColumn( 40 | label: Text( 41 | text, 42 | style: TextStyle(fontSize: 16), 43 | )); 44 | } 45 | 46 | DataCell _cell(String? text) { 47 | return DataCell( 48 | Text(text ?? ''), 49 | ); 50 | } 51 | 52 | DataCell _urlCell(String? text) { 53 | return DataCell(Row( 54 | children: [ 55 | SelectableText(text ?? ''), 56 | IconButton( 57 | onPressed: () { 58 | Clipboard.setData(ClipboardData(text: text ?? '')); 59 | }, 60 | icon: Icon( 61 | Icons.copy, 62 | size: 14, 63 | )) 64 | ], 65 | )); 66 | } 67 | 68 | List _getData(List list, bool _validOnly) => 69 | _validOnly != false 70 | ? list 71 | .where((element) => element.status == UrlSniffResStatus.success) 72 | .toList() 73 | : list; 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | return SingleChildScrollView( 78 | child: DataTable( 79 | dataRowHeight: 30, 80 | columns: [ 81 | _columnHeader('频道'), 82 | _columnHeader('状态'), 83 | _columnHeader('分辨率'), 84 | _columnHeader('地区/运营商'), 85 | _columnHeader('链接'), 86 | ], 87 | rows: _getData(data, validOnly).map((UrlSniffRes e) { 88 | return DataRow(cells: [ 89 | _cell('${e.index.toString()}号'), 90 | DataCell(_renderStatus(e.status)), 91 | _cell(SniffUtil().getVideoSize(e.mediaInfo)), 92 | _cell(e.ipInfo), 93 | _urlCell(e.url), 94 | ]); 95 | }).toList())); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /lib/global.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 6 | import 'package:fvp/fvp.dart' as fvp; 7 | import 'package:package_info_plus/package_info_plus.dart'; 8 | import 'package:uni_links_desktop/uni_links_desktop.dart'; 9 | import 'package:vvibe/common/values/values.dart'; 10 | import 'package:vvibe/components/spinning.dart'; 11 | import 'package:vvibe/models/login_mode.dart'; 12 | import 'package:vvibe/theme.dart'; 13 | import 'package:vvibe/utils/playlist/epg_util.dart'; 14 | import 'package:vvibe/utils/utils.dart'; 15 | import 'package:window_manager/window_manager.dart'; 16 | 17 | /// 全局配置 18 | class Global { 19 | /// 用户配置 20 | static UserLoginResponseModel? profile = UserLoginResponseModel(token: null); 21 | 22 | /// 是否第一次打开 23 | static bool isFirstOpen = false; 24 | 25 | /// 是否离线登录 26 | static bool isOfflineLogin = true; 27 | 28 | /// 是否 release 29 | static bool get isRelease => IS_RELEASE; 30 | // packageInfo 31 | static PackageInfo? packageInfo; 32 | 33 | /// init 34 | static Future init({bool shouldSetSize = true}) async { 35 | // 运行初始 36 | WidgetsFlutterBinding.ensureInitialized(); 37 | 38 | await windowManager.ensureInitialized(); 39 | if (Platform.isWindows) { 40 | registerProtocol('vvibe'); 41 | } 42 | // set logger before registerWith() 43 | /* if (!IS_RELEASE) Logger.root.level = Level.ALL; 44 | Logger.root.onRecord.listen((record) { 45 | try{ final df = DateFormat("HH:mm:ss.SSS"); 46 | final content = 47 | '${record.loggerName}.${record.level.name}: ${df.format(record.time)}: ${record.message}'; 48 | print(content);} catch (e) {} 49 | //if (IS_RELEASE) LogFile.log(content + '\n'); 50 | }); */ 51 | fvp.registerWith(); 52 | // Ruquest 模块初始化 53 | Request(); 54 | // 本地存储初始化 55 | await LoacalStorage.init(); 56 | 57 | //播放列表截图目录 58 | await PlaylistUtil().getSnapshotDir(); 59 | 60 | // 读取设备第一次打开 61 | isFirstOpen = !LoacalStorage().getBool(STORAGE_DEVICE_ALREADY_OPEN_KEY); 62 | if (isFirstOpen) { 63 | LoacalStorage().setBool(STORAGE_DEVICE_ALREADY_OPEN_KEY, true); 64 | } 65 | 66 | // 读取离线用户信息 67 | var _profileJSON = LoacalStorage().getJSON(STORAGE_USER_PROFILE_KEY); 68 | if (_profileJSON != null) { 69 | profile = UserLoginResponseModel.fromJson(_profileJSON); 70 | isOfflineLogin = true; 71 | } 72 | 73 | // android 状态栏为透明的沉浸 74 | if (Platform.isAndroid) { 75 | SystemUiOverlayStyle systemUiOverlayStyle = 76 | SystemUiOverlayStyle(statusBarColor: Colors.transparent); 77 | SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle); 78 | } 79 | //自定义easyloading 80 | EasyLoading.instance 81 | ..indicatorWidget = SizedBox( 82 | width: 40, 83 | child: Spinning(), 84 | ); 85 | if (shouldSetSize) { 86 | EpgUtil().downloadEpgDataAync(); 87 | // VVFFmpeg().checkFfmpegDllAync(); 88 | } 89 | 90 | packageInfo = await PackageInfo.fromPlatform(); 91 | 92 | return genTheme(); 93 | } 94 | 95 | // 持久化 用户信息 96 | static Future saveProfile(UserLoginResponseModel userResponse) { 97 | profile = userResponse; 98 | return LoacalStorage() 99 | .setJSON(STORAGE_USER_PROFILE_KEY, userResponse.toJson()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/components/updater/updater_widgets.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:vvibe/common/colors/colors.dart'; 3 | import 'package:vvibe/common/values/enum.dart'; 4 | import 'package:vvibe/utils/playlist/playlist_util.dart'; 5 | import 'package:vvibe/utils/updater.dart'; 6 | import 'package:window_manager/window_manager.dart'; 7 | 8 | class UpdaterWidgets { 9 | static Widget updateChipBuilder({ 10 | required BuildContext context, 11 | required String? latestVersion, 12 | required String appVersion, 13 | required UpdatStatus status, 14 | required void Function() checkForUpdate, 15 | required void Function() openDialog, 16 | required void Function() startUpdate, 17 | required Future Function() launchInstaller, 18 | required void Function() dismissUpdate, 19 | }) { 20 | if (status == UpdatStatus.available || 21 | status == UpdatStatus.availableWithChangelog) { 22 | return Row( 23 | children: [ 24 | Chip( 25 | label: Text( 26 | '发现新版本: $latestVersion', 27 | style: TextStyle( 28 | color: Colors.white, 29 | ), 30 | ), 31 | deleteButtonTooltipMessage: '取消升级', 32 | onDeleted: () { 33 | dismissUpdate(); 34 | }, 35 | ), 36 | Tooltip( 37 | message: '立即升级', 38 | child: IconButton( 39 | color: AppColors.primaryColor, 40 | icon: Icon(Icons.check, color: AppColors.primaryColor), 41 | onPressed: () { 42 | startUpdate(); 43 | }, 44 | )), 45 | ], 46 | ); 47 | } 48 | if (status == UpdatStatus.readyToInstall) { 49 | return Row(children: [ 50 | Chip( 51 | label: Text( 52 | '新版本: $latestVersion 已就绪', 53 | style: TextStyle( 54 | color: Colors.white, 55 | ), 56 | ), 57 | deleteButtonTooltipMessage: '取消安装', 58 | onDeleted: () { 59 | dismissUpdate(); 60 | }, 61 | ), 62 | Tooltip( 63 | message: '立即安装', 64 | child: IconButton( 65 | color: AppColors.primaryColor, 66 | icon: Icon(Icons.check, color: AppColors.primaryColor), 67 | onPressed: () { 68 | launchInstaller(); 69 | }, 70 | )) 71 | ]); 72 | } 73 | if (status == UpdatStatus.error) { 74 | return Chip( 75 | label: Text( 76 | '自动升级 $latestVersion 失败', 77 | style: TextStyle( 78 | color: Colors.white, 79 | ), 80 | ), 81 | deleteButtonTooltipMessage: '手动升级', 82 | deleteIcon: Icon(Icons.download_for_offline_outlined), 83 | onDeleted: () { 84 | dismissUpdate(); 85 | PlaylistUtil().openAppExecSubDir(UpdaterUtil.downloadDir); 86 | windowManager.close(); 87 | }, 88 | ); 89 | } 90 | 91 | return SizedBox(); 92 | } 93 | 94 | static updateDialogBuilder({ 95 | required BuildContext context, 96 | required String? latestVersion, 97 | required String appVersion, 98 | required UpdatStatus status, 99 | required String? changelog, 100 | required void Function() checkForUpdate, 101 | required void Function() openDialog, 102 | required void Function() startUpdate, 103 | required Future Function() launchInstaller, 104 | required void Function() dismissUpdate, 105 | }) { 106 | openDialog(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "vvibe" "\0" 93 | VALUE "FileDescription", "vvibe" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "vvibe" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2022 org.eu.vvibe. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "vvibe.exe" "\0" 98 | VALUE "ProductName", "vvibe" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates and shows a win32 window with |title| and position and size using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size to will treat the width height passed in to this function 35 | // as logical pixels and scale to appropriate for the default monitor. Returns 36 | // true if the window was created successfully. 37 | bool CreateAndShow(const std::wstring& title, 38 | const Point& origin, 39 | const Size& size); 40 | 41 | // Release OS resources associated with window. 42 | void Destroy(); 43 | 44 | // Inserts |content| into the window tree. 45 | void SetChildContent(HWND content); 46 | 47 | // Returns the backing Window handle to enable clients to set icon and other 48 | // window properties. Returns nullptr if the window has been destroyed. 49 | HWND GetHandle(); 50 | 51 | // If true, closing this window will quit the application. 52 | void SetQuitOnClose(bool quit_on_close); 53 | 54 | // Return a RECT representing the bounds of the current client area. 55 | RECT GetClientArea(); 56 | 57 | protected: 58 | // Processes and route salient window messages for mouse handling, 59 | // size change and DPI. Delegates handling of these to member overloads that 60 | // inheriting classes can handle. 61 | virtual LRESULT MessageHandler(HWND window, 62 | UINT const message, 63 | WPARAM const wparam, 64 | LPARAM const lparam) noexcept; 65 | 66 | // Called when CreateAndShow is called, allowing subclass window-related 67 | // setup. Subclasses should return false if setup fails. 68 | virtual bool OnCreate(); 69 | 70 | // Called when Destroy is called. 71 | virtual void OnDestroy(); 72 | 73 | private: 74 | friend class WindowClassRegistrar; 75 | 76 | // OS callback called by message pump. Handles the WM_NCCREATE message which 77 | // is passed when the non-client area is being created and enables automatic 78 | // non-client DPI scaling so that the non-client area automatically 79 | // responsponds to changes in DPI. All other messages are handled by 80 | // MessageHandler. 81 | static LRESULT CALLBACK WndProc(HWND const window, 82 | UINT const message, 83 | WPARAM const wparam, 84 | LPARAM const lparam) noexcept; 85 | 86 | // Retrieves a class instance pointer for |window| 87 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 88 | 89 | bool quit_on_close_ = false; 90 | 91 | // window handle for top level window. 92 | HWND window_handle_ = nullptr; 93 | 94 | // window handle for hosted content. 95 | HWND child_content_ = nullptr; 96 | }; 97 | 98 | #endif // RUNNER_WIN32_WINDOW_H_ 99 | -------------------------------------------------------------------------------- /lib/utils/dart_tars_protocol/tarscodec.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'tars_output_stream.dart'; 4 | import 'tars_input_stream.dart'; 5 | import 'tars_struct.dart'; 6 | 7 | // credit https://github.com/xiaoyaocz/dart_tars_protocol 8 | 9 | /*void main() { 10 | var testUid = 1354740567; 11 | //[0, 1, 29, 0, 0, 23, 2, 80, 191, 179, 87, 16, 1, 38, 0, 54, 0, 66, 80, 191, 179, 87, 82, 80, 191, 179, 87, 108, 124] 12 | print(regDataEncode(testUid)); 13 | } 14 | */ 15 | 16 | class User extends TarsStruct { 17 | String user; 18 | 19 | User({this.user = ''}); 20 | 21 | @override 22 | void readFrom(TarsInputStream _is) { 23 | user = _is.readString(2, false); 24 | } 25 | 26 | @override 27 | dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); 28 | } 29 | 30 | Uint8List huyaWsHeartbeat() { 31 | var huyaHeartbeat = [ 32 | 0x00, 33 | 0x03, 34 | 0x1d, 35 | 0x00, 36 | 0x00, 37 | 0x69, 38 | 0x00, 39 | 0x00, 40 | 0x00, 41 | 0x69, 42 | 0x10, 43 | 0x03, 44 | 0x2c, 45 | 0x3c, 46 | 0x4c, 47 | 0x56, 48 | 0x08, 49 | 0x6f, 50 | 0x6e, 51 | 0x6c, 52 | 0x69, 53 | 0x6e, 54 | 0x65, 55 | 0x75, 56 | 0x69, 57 | 0x66, 58 | 0x0f, 59 | 0x4f, 60 | 0x6e, 61 | 0x55, 62 | 0x73, 63 | 0x65, 64 | 0x72, 65 | 0x48, 66 | 0x65, 67 | 0x61, 68 | 0x72, 69 | 0x74, 70 | 0x42, 71 | 0x65, 72 | 0x61, 73 | 0x74, 74 | 0x7d, 75 | 0x00, 76 | 0x00, 77 | 0x3c, 78 | 0x08, 79 | 0x00, 80 | 0x01, 81 | 0x06, 82 | 0x04, 83 | 0x74, 84 | 0x52, 85 | 0x65, 86 | 0x71, 87 | 0x1d, 88 | 0x00, 89 | 0x00, 90 | 0x2f, 91 | 0x0a, 92 | 0x0a, 93 | 0x0c, 94 | 0x16, 95 | 0x00, 96 | 0x26, 97 | 0x00, 98 | 0x36, 99 | 0x07, 100 | 0x61, 101 | 0x64, 102 | 0x72, 103 | 0x5f, 104 | 0x77, 105 | 0x61, 106 | 0x70, 107 | 0x46, 108 | 0x00, 109 | 0x0b, 110 | 0x12, 111 | 0x03, 112 | 0xae, 113 | 0xf0, 114 | 0x0f, 115 | 0x22, 116 | 0x03, 117 | 0xae, 118 | 0xf0, 119 | 0x0f, 120 | 0x3c, 121 | 0x42, 122 | 0x6d, 123 | 0x52, 124 | 0x02, 125 | 0x60, 126 | 0x5c, 127 | 0x60, 128 | 0x01, 129 | 0x7c, 130 | 0x82, 131 | 0x00, 132 | 0x0b, 133 | 0xb0, 134 | 0x1f, 135 | 0x9c, 136 | 0xac, 137 | 0x0b, 138 | 0x8c, 139 | 0x98, 140 | 0x0c, 141 | 0xa8, 142 | 0x0c 143 | ]; 144 | var u8l = Uint8List.fromList(huyaHeartbeat); 145 | return u8l; 146 | } 147 | 148 | List danmakuDecode(Uint8List bytes) { 149 | //解码弹幕数据 150 | var ios = TarsInputStream(bytes); 151 | var type = ios.readInt(0, false); 152 | 153 | if (type == 7) { 154 | var stream = TarsInputStream(ios.readBytes(1, false)); 155 | if (stream.readInt(1, false) == 1400) { 156 | var content = TarsInputStream(stream.readBytes(2, false)); 157 | var rawUser = content.read(User(), 0, false); 158 | var user = rawUser.user; 159 | var msg = content.readString(3, false); 160 | return [user, msg]; 161 | } else { 162 | return ['', '']; 163 | } 164 | } else { 165 | return ['', '']; 166 | } 167 | } 168 | 169 | Uint8List regDataEncode(lUid) { 170 | var tid = lUid; 171 | var sid = lUid; 172 | 173 | //encode 174 | var oos = TarsOutputStream(); 175 | oos.write(lUid, 0); 176 | oos.write(true, 1); 177 | oos.write('', 2); 178 | oos.write('', 3); 179 | oos.write(tid, 4); 180 | oos.write(sid, 5); 181 | oos.write(0, 6); 182 | oos.write(0, 7); 183 | 184 | var wscmd = TarsOutputStream(); 185 | wscmd.write(1, 0); 186 | wscmd.write(oos.toUint8List(), 1); 187 | var res = wscmd.toUint8List(); 188 | return res; 189 | } 190 | -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # Set fallback configurations for older versions of the flutter tool. 14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM) 15 | set(FLUTTER_TARGET_PLATFORM "windows-x64") 16 | endif() 17 | 18 | # === Flutter Library === 19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 20 | 21 | # Published to parent scope for install step. 22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 26 | 27 | list(APPEND FLUTTER_LIBRARY_HEADERS 28 | "flutter_export.h" 29 | "flutter_windows.h" 30 | "flutter_messenger.h" 31 | "flutter_plugin_registrar.h" 32 | "flutter_texture_registrar.h" 33 | ) 34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 35 | add_library(flutter INTERFACE) 36 | target_include_directories(flutter INTERFACE 37 | "${EPHEMERAL_DIR}" 38 | ) 39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 40 | add_dependencies(flutter flutter_assemble) 41 | 42 | # === Wrapper === 43 | list(APPEND CPP_WRAPPER_SOURCES_CORE 44 | "core_implementations.cc" 45 | "standard_codec.cc" 46 | ) 47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 49 | "plugin_registrar.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 52 | list(APPEND CPP_WRAPPER_SOURCES_APP 53 | "flutter_engine.cc" 54 | "flutter_view_controller.cc" 55 | ) 56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 57 | 58 | # Wrapper sources needed for a plugin. 59 | add_library(flutter_wrapper_plugin STATIC 60 | ${CPP_WRAPPER_SOURCES_CORE} 61 | ${CPP_WRAPPER_SOURCES_PLUGIN} 62 | ) 63 | apply_standard_settings(flutter_wrapper_plugin) 64 | set_target_properties(flutter_wrapper_plugin PROPERTIES 65 | POSITION_INDEPENDENT_CODE ON) 66 | set_target_properties(flutter_wrapper_plugin PROPERTIES 67 | CXX_VISIBILITY_PRESET hidden) 68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 69 | target_include_directories(flutter_wrapper_plugin PUBLIC 70 | "${WRAPPER_ROOT}/include" 71 | ) 72 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 73 | 74 | # Wrapper sources needed for the runner. 75 | add_library(flutter_wrapper_app STATIC 76 | ${CPP_WRAPPER_SOURCES_CORE} 77 | ${CPP_WRAPPER_SOURCES_APP} 78 | ) 79 | apply_standard_settings(flutter_wrapper_app) 80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 81 | target_include_directories(flutter_wrapper_app PUBLIC 82 | "${WRAPPER_ROOT}/include" 83 | ) 84 | add_dependencies(flutter_wrapper_app flutter_assemble) 85 | 86 | # === Flutter tool backend === 87 | # _phony_ is a non-existent file to force this command to run every time, 88 | # since currently there's no way to get a full input/output list from the 89 | # flutter tool. 90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 92 | add_custom_command( 93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 95 | ${CPP_WRAPPER_SOURCES_APP} 96 | ${PHONY_OUTPUT} 97 | COMMAND ${CMAKE_COMMAND} -E env 98 | ${FLUTTER_TOOL_ENVIRONMENT} 99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 100 | ${FLUTTER_TARGET_PLATFORM} $ 101 | VERBATIM 102 | ) 103 | add_custom_target(flutter_assemble DEPENDS 104 | "${FLUTTER_LIBRARY}" 105 | ${FLUTTER_LIBRARY_HEADERS} 106 | ${CPP_WRAPPER_SOURCES_CORE} 107 | ${CPP_WRAPPER_SOURCES_PLUGIN} 108 | ${CPP_WRAPPER_SOURCES_APP} 109 | ) 110 | -------------------------------------------------------------------------------- /lib/services/hackchat/hackchat.dart: -------------------------------------------------------------------------------- 1 | //hack.chat 连接 2 | import 'dart:convert'; 3 | import 'package:vvibe/utils/logger.dart'; 4 | import 'package:web_socket_channel/io.dart'; 5 | 6 | class Hackchat { 7 | Hackchat( 8 | {required this.nickname, 9 | this.onChat, 10 | this.msgData, 11 | this.onClose, 12 | this.onError}); 13 | final String nickname; 14 | final String room = 'vvibe-community'; 15 | Function? msgData; 16 | Function? onChat; 17 | Function? onClose; 18 | Function? onError; 19 | 20 | IOWebSocketChannel? _ws; 21 | 22 | String? _sessionId; 23 | //初始化 24 | init() { 25 | MyLogger.info('开始连接hackchat'); 26 | _ws = IOWebSocketChannel.connect('wss://hack.chat/chat-ws', 27 | connectTimeout: const Duration(seconds: 30), 28 | headers: { 29 | 'Origin': 'https://hack.chat', 30 | 'User-Agent': 31 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36' 32 | }); 33 | MyLogger.info('连接hackchat成功'); 34 | startSession(); 35 | _setListener(); 36 | } 37 | 38 | sendMsg(String text) { 39 | send({"cmd": "chat", "channel": room, "text": text}); 40 | } 41 | 42 | startSession() { 43 | send({"cmd": "session", "isBot": false}); 44 | } 45 | 46 | resumeSession() { 47 | if (_sessionId == null) return; 48 | 49 | send({"cmd": "session", "isBot": false, 'id': _sessionId}); 50 | } 51 | 52 | send(Map data) { 53 | try { 54 | if (_ws == null) return; 55 | 56 | _ws!.sink.add(jsonEncode(data)); 57 | } catch (e) { 58 | MyLogger.error('hackchat sending error:' + e.toString()); 59 | } 60 | } 61 | 62 | get readyState => _ws?.innerWebSocket?.readyState; 63 | 64 | close() { 65 | if (_ws == null) return; 66 | _sessionId = null; 67 | disconnectUser(); 68 | _ws!.sink.close(); 69 | } 70 | 71 | disconnectUser() { 72 | send({'cmd': 'disconnect'}); 73 | } 74 | 75 | _setListener() { 76 | if (_ws == null) return; 77 | 78 | _ws!.stream.listen( 79 | _onReceive, 80 | onDone: () { 81 | MyLogger.warn('hackchat连接关闭'); 82 | close(); 83 | 84 | if (onClose != null) { 85 | onClose!(); 86 | } 87 | }, 88 | onError: (error) { 89 | MyLogger.error('hackchat发生错误 $error'); 90 | if (onError != null) { 91 | onError!(); 92 | } 93 | }, 94 | cancelOnError: true, 95 | ); 96 | } 97 | 98 | void _onReceive(msg) { 99 | final Map obj = jsonDecode(msg); 100 | switch (obj['cmd']) { 101 | case 'session': 102 | _onSessionReceive(obj); 103 | break; 104 | case 'chat': 105 | _onChatReceive(obj); 106 | 107 | break; 108 | case 'warn': 109 | _onWarn(obj); 110 | break; 111 | case 'onlineRemove': 112 | _onWarn(obj); 113 | break; 114 | case 'onlineAdd': 115 | _onWarn(obj); 116 | break; 117 | case 'onlineSet': 118 | _onWarn(obj); 119 | break; 120 | default: 121 | } 122 | if (msgData != null) { 123 | msgData!(obj); 124 | } 125 | //MyLogger.info("收到hackchat数据:" + msg); 126 | } 127 | 128 | _onChatReceive(Map data) { 129 | if (onChat != null) { 130 | onChat!(data); 131 | } 132 | } 133 | 134 | _onSessionReceive(Map data) { 135 | _joinRoom(); 136 | if (data['sessionID'] != null) { 137 | resumeSession(); 138 | } 139 | } 140 | 141 | _onOnlineRemove(Map data) {} 142 | _onOnlineAdd(Map data) {} 143 | _onOnlineSet(Map data) {} 144 | 145 | //加入频道 146 | void _joinRoom() { 147 | send({"cmd": "join", "nick": nickname, "pass": "", "channel": room}); 148 | } 149 | 150 | _onWarn(Map data) { 151 | final text = data['text']; 152 | if (text == null) return; 153 | MyLogger.warn('hackchat $text'); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/services/danmaku/huya_danmaku_service.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Moxx 3 | * @Date: 2022-09-08 10:27:05 4 | * @Last Modified by: Moxx 5 | * @Last Modified time: 2022-09-08 12:22:40 6 | */ 7 | import 'dart:async'; 8 | import 'dart:convert'; 9 | 10 | import 'package:dio/dio.dart'; 11 | import 'package:flutter/foundation.dart'; 12 | import 'package:html/parser.dart' show parse; 13 | import 'package:vvibe/models/huya_room_profile.dart'; 14 | import 'package:vvibe/models/live_danmaku_item.dart'; 15 | import 'package:vvibe/utils/dart_tars_protocol/tarscodec.dart'; 16 | import 'package:vvibe/utils/logger.dart'; 17 | import 'package:web_socket_channel/io.dart'; 18 | 19 | class HuyaDanmakuService { 20 | HuyaDanmakuService({required this.roomId, required this.onDanmaku}); 21 | final String roomId; 22 | final void Function(LiveDanmakuItem? danmaku) onDanmaku; 23 | 24 | Timer? timer; 25 | IOWebSocketChannel? _channel; 26 | int totleTime = 0; 27 | 28 | void connect() async { 29 | MyLogger.info("虎牙登录弹幕"); 30 | 31 | _channel = IOWebSocketChannel.connect("wss://cdnws.api.huya.com"); 32 | 33 | timer = Timer.periodic(const Duration(seconds: 30), (callback) { 34 | totleTime += 30; 35 | heartBeat(); 36 | MyLogger.info("huya弹幕时间: $totleTime s"); 37 | }); 38 | await login(); 39 | setListener(); 40 | } 41 | 42 | //发送心跳包 43 | void heartBeat() { 44 | Uint8List heartbeat = huyaWsHeartbeat(); 45 | _channel?.sink.add(heartbeat); 46 | } 47 | 48 | //设置监听 49 | void setListener() { 50 | _channel?.stream.listen((msg) { 51 | Uint8List list = Uint8List.fromList(msg); 52 | decode(list); 53 | }); 54 | } 55 | 56 | Future?> _getChatInfo(String roomId) async { 57 | var resp = await Dio(new BaseOptions(headers: { 58 | 'User-Agent': 59 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148' 60 | })) 61 | .get( 62 | 'https://m.huya.com/$roomId', 63 | ) 64 | // ignore: body_might_complete_normally_catch_error 65 | .catchError((e) { 66 | MyLogger.error('huya弹幕错误:' + e); 67 | }); 68 | String value = resp.data; 69 | var dataLive = parse(value); 70 | var body = dataLive.getElementsByTagName('body')[0]; 71 | var script = body.getElementsByTagName('script').where((s) { 72 | return s.text.contains('indow.HNF_GLOBAL_INIT'); 73 | }); 74 | String jsonStr = 75 | script.first.text.replaceAll('window.HNF_GLOBAL_INIT = ', '').trim(); 76 | jsonStr = jsonStr.substring(0, jsonStr.indexOf(',"tLiveStreamInf')) + '}}}'; 77 | // debugPrint('弹幕srt ' + jsonStr); 78 | final json = jsonDecode(jsonStr); 79 | return json; 80 | //HuyaRoomGlobalProfile.fromJson(json); 81 | } 82 | 83 | Future login() async { 84 | try { 85 | // final HuyaRoomGlobalProfile globalProfile = await _getChatInfo(roomId); 86 | final globalProfile = await _getChatInfo(roomId); 87 | final int? danmakuId = globalProfile?['roomProfile']['lUid']; 88 | if (danmakuId == null) return null; 89 | Uint8List regData = regDataEncode(danmakuId); 90 | _channel?.sink.add(regData); 91 | // MyLogger.info("虎牙弹幕 login success"); 92 | Uint8List heartbeat = huyaWsHeartbeat(); 93 | //print("heartbeat"); 94 | _channel?.sink.add(heartbeat); 95 | //return globalProfile; 96 | } catch (e) { 97 | return null; 98 | } 99 | return null; 100 | } 101 | 102 | //对消息进行解码 103 | decode(Uint8List list) { 104 | List danmaku = danmakuDecode(list); 105 | String nickname = danmaku[0]; 106 | String message = danmaku[1]; 107 | //TODO: 屏蔽词功能 108 | if (message != '') { 109 | MyLogger.info('虎牙弹幕 --> $nickname: $message'); 110 | // addDanmaku(LiveDanmakuItem(nickname, message)); 111 | onDanmaku(LiveDanmakuItem(name: nickname, msg: message, uid: '')); 112 | } 113 | } 114 | 115 | void displose() { 116 | timer?.cancel(); 117 | _channel?.sink.close(); 118 | _channel = null; 119 | MyLogger.info('关闭虎牙ws'); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(vvibe LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "vvibe") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(SET CMP0063 NEW) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | # Generated plugin build rules, which manage building the plugins and adding 56 | # them to the application. 57 | include(flutter/generated_plugins.cmake) 58 | 59 | #include(./rust.cmake) 60 | 61 | # === Installation === 62 | # Support files are copied into place next to the executable, so that it can 63 | # run in place. This is done instead of making a separate bundle (as on Linux) 64 | # so that building and running from within Visual Studio will work. 65 | set(BUILD_BUNDLE_DIR "$") 66 | # Make the "install" step default, as it's required to run. 67 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 68 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 69 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 70 | endif() 71 | 72 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 73 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 74 | 75 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 76 | COMPONENT Runtime) 77 | 78 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 79 | COMPONENT Runtime) 80 | 81 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 82 | COMPONENT Runtime) 83 | 84 | if(PLUGIN_BUNDLED_LIBRARIES) 85 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 86 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 87 | COMPONENT Runtime) 88 | endif() 89 | 90 | # Fully re-copy the assets directory on each build to avoid having stale files 91 | # from a previous install. 92 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 93 | install(CODE " 94 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 95 | " COMPONENT Runtime) 96 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 97 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 98 | 99 | # Install the AOT library on non-Debug builds only. 100 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 101 | CONFIGURATIONS Profile;Release 102 | COMPONENT Runtime) 103 | -------------------------------------------------------------------------------- /lib/components/player/player_context_menu.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Moxx 3 | * @Date: 2022-09-13 14:05:05 4 | * @LastEditors: moxun33 5 | * @LastEditTime: 2023-05-16 00:11:04 6 | * @FilePath: \vvibe\lib\components\player\player_context_menu.dart 7 | * @Description: 8 | * @qmj 9 | */ 10 | //import 'package:desktop_multi_window/desktop_multi_window.dart'; 11 | import 'dart:io'; 12 | 13 | import 'package:flutter/material.dart'; 14 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 15 | import 'package:native_context_menu/native_context_menu.dart'; 16 | import 'package:vvibe/common/values/consts.dart'; 17 | import 'package:vvibe/components/player/settings/open_url_dialog.dart'; 18 | import 'package:vvibe/components/player/settings/setting_alert_dialog.dart'; 19 | import 'package:vvibe/global.dart'; 20 | import 'package:vvibe/services/event_bus.dart'; 21 | 22 | class PlayerContextMenu extends StatefulWidget { 23 | PlayerContextMenu( 24 | {Key? key, 25 | required this.onOpenUrl, 26 | required this.showPlaylist, 27 | required this.playListShowed, 28 | required this.child}) 29 | : super(key: key); 30 | final void Function(String url) onOpenUrl; 31 | final void Function() showPlaylist; 32 | final bool playListShowed; 33 | final Widget child; 34 | @override 35 | _PlayerContextMenuState createState() => _PlayerContextMenuState(); 36 | } 37 | 38 | class _PlayerContextMenuState extends State { 39 | void _showNewWin(String key, {String? title}) async { 40 | /* final window = await DesktopMultiWindow.createWindow(jsonEncode({ 41 | 'key': key, 42 | })); 43 | window 44 | ..setTitle(title ?? 'VVibe') 45 | ..setFrame(const Offset(0, 0) & Size(1280, 720 + CUS_WIN_TITLEBAR_HEIGHT)) 46 | ..center() 47 | ..show(); */ 48 | } 49 | checkUpdate() { 50 | if (Platform.isWindows) { 51 | eventBus.emit('check-for-update'); 52 | } 53 | } 54 | 55 | void _onItemSelect(BuildContext context, MenuItem item) { 56 | final title = item.title; 57 | switch (title) { 58 | case '打开链接': 59 | showDialog( 60 | context: context, 61 | builder: (context) { 62 | return OpenUrlDialog( 63 | onOpenUrl: widget.onOpenUrl, 64 | ); 65 | }); 66 | break; 67 | case '播放列表': 68 | if (widget.playListShowed) return; 69 | widget.showPlaylist(); 70 | break; 71 | case '扫描直播源': 72 | _showNewWin('sniff', title: 'VVibe 直播源扫描'); 73 | 74 | break; 75 | case '检测直播源': 76 | EasyLoading.showInfo('TODO'); 77 | 78 | break; 79 | case '应用设置': 80 | showDialog( 81 | context: context, 82 | builder: (context) { 83 | return SettingAlertDialog(); 84 | }); 85 | break; 86 | case '检查更新': 87 | checkUpdate(); 88 | break; 89 | case '关于应用': 90 | showDialog( 91 | context: context, 92 | builder: (context) { 93 | return AlertDialog( 94 | title: Text('关于${APP_NAME}'), 95 | content: Text( 96 | '${APP_NAME} v${Global.packageInfo?.version ?? '0.0.0'}'), 97 | actions: [ 98 | TextButton( 99 | onPressed: () { 100 | Navigator.of(context).pop(); 101 | }, 102 | child: Text('关闭')) 103 | ], 104 | ); 105 | }); 106 | break; 107 | 108 | default: 109 | break; 110 | } 111 | } 112 | 113 | @override 114 | Widget build(BuildContext context) { 115 | return ContextMenuRegion( 116 | // onDismissed: () => setState(() {}), 117 | onItemSelected: (item) { 118 | _onItemSelect(context, item); 119 | }, 120 | menuItems: [ 121 | MenuItem(title: '打开链接'), 122 | MenuItem(title: '播放列表'), 123 | /* MenuItem(title: '扫描直播源'), 124 | MenuItem(title: '检测直播源'), */ 125 | MenuItem(title: '应用设置'), 126 | MenuItem(title: '检查更新'), 127 | MenuItem(title: '关于应用'), 128 | ], 129 | child: widget.child, 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/utils/LogFile.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:intl/intl.dart'; 4 | import 'package:vvibe/common/values/consts.dart'; 5 | import 'package:vvibe/utils/logger.dart'; 6 | 7 | class LogFile { 8 | LogFile._() { 9 | createDirectory(); 10 | } 11 | static LogFile? _instance; 12 | static LogFile get instance => _getOrCreateInstance(); 13 | static LogFile _getOrCreateInstance() { 14 | if (_instance != null) { 15 | return _instance!; 16 | } else { 17 | _instance = LogFile._(); 18 | return _instance!; 19 | } 20 | } 21 | 22 | File? currentFile; 23 | static bool isFirst = true; 24 | 25 | static log(String content) { 26 | if (isFirst) { 27 | isFirst = false; 28 | LogFile.instance 29 | .writeFile('-----------------app启动 日志开始-----------------------\n ' 30 | '$content'); 31 | } else { 32 | LogFile.instance.writeFile(content); 33 | } 34 | } 35 | 36 | // 写入一次 文件才会生成 37 | writeFile(String content) async { 38 | if (fileName.isNotEmpty) { 39 | try { 40 | // print('写入日志文件内容----> $content'); 41 | IOSink sink = currentFile!.openWrite(mode: FileMode.append); 42 | sink.writeln(content); 43 | await sink.flush(); 44 | await sink.close(); 45 | } catch (e) { 46 | MyLogger.error('写入日志文件异常----> $e'); 47 | } 48 | } 49 | } 50 | 51 | String fileType = '.txt'; 52 | 53 | Future readFile() async { 54 | try { 55 | bool exist = await File(fileName).exists(); 56 | if (!exist) { 57 | print('文件${fileName}不存在'); 58 | return ''; 59 | } 60 | File file = File(fileName); 61 | String contents = await file.readAsString(); 62 | print('read 内容----> $contents'); 63 | return contents; 64 | } catch (e) { 65 | return ''; 66 | } 67 | } 68 | 69 | String fileName = ''; 70 | 71 | createFileName() { 72 | if (fileName.isNotEmpty) { 73 | return fileName; 74 | } 75 | var time = DateTime.now(); 76 | // 一天创建一个日志文件 半个月前的日志文件删除 77 | String name = formatDateTime(time); 78 | fileName = '$currentDirectory/$name$fileType'; 79 | currentFile = File(fileName); 80 | return fileName; 81 | } 82 | 83 | String formatDateTime(DateTime dateTime, [String format = 'yyyy-MM-dd']) { 84 | return DateFormat(format).format(dateTime); 85 | } 86 | 87 | String currentDirectory = ''; 88 | //data目录(在应用根目录下) 89 | Future createDir( 90 | {String dirName = IS_RELEASE ? 'data/logs' : 'assets/logs'}) async { 91 | final dir = Directory(dirName); 92 | currentDirectory = dir.path; 93 | if (!await (dir.exists())) { 94 | await dir.create(); 95 | } 96 | return dir; 97 | } 98 | 99 | void createDirectory() async { 100 | // 创建Directory对象 101 | Directory dir = await createDir(); 102 | 103 | // 检查目录是否存在,如果不存在,则创建目录 104 | if (!await dir.exists()) { 105 | // 设置recursive为true以确保创建任何必要的父目录 106 | await dir.create(recursive: true); 107 | print('logs Directory created successfully!'); 108 | } else { 109 | print('logs Directory already exists.'); 110 | } 111 | createFileName(); 112 | deleteOldFiles(); 113 | } 114 | 115 | showFileList() {} 116 | 117 | deleteAllFiles() {} 118 | 119 | deleteOldFiles() { 120 | /* 121 | FileTool.listFilesInDirectory(currentDirectory).then((List value) { 122 | var nowTime = DateTime.now(); 123 | value.mapIndex((index, element) { 124 | try { 125 | List parts = element.split('/'); // 使用 '/' 分割路径 126 | // 获取最后一段路径 127 | String lastSegment = parts.isNotEmpty ? parts.last : ''; 128 | if (lastSegment.endsWith(fileType)) { 129 | lastSegment = lastSegment.substring(0, lastSegment.length - 4); 130 | } 131 | // jdLog('获取的文件名字22------$lastSegment'); 132 | DateTime? time = TimeTool.parse(lastSegment); 133 | if (time is DateTime) { 134 | Duration difference = time.difference(nowTime); 135 | int days = difference.inDays; 136 | if (days > 7) { 137 | // 删除超过7天的文件 138 | jdLog('离现在差距$days天 删除了-------->'); 139 | FileTool.deleteFile(element); 140 | } 141 | } 142 | } catch (onError) { 143 | jdLog('转换后的onError--------> $onError'); 144 | } 145 | }); 146 | }); */ 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: vvibe 2 | description: have a good video watching vibe. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: "none" # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 0.12.4 19 | 20 | environment: 21 | sdk: ">=2.17.0 <3.11.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | 27 | # 日期格式 28 | intl: ^0.19.0 29 | # loaing 30 | flutter_easyloading: ^3.0.5 31 | # http 32 | dio: ^5.6.0 33 | # storage + cache 34 | shared_preferences: ^2.3.2 35 | # webview 36 | #webview_flutter: ^2.0.8 37 | #flutter_cache_manager: ^3.1.0 38 | web_socket_channel: ^2.4.5 39 | html: ^0.15.4 40 | fvp: ^0.35.0 41 | collection: ^1.19.0 42 | dropdown_button2: ^2.3.9 43 | flutter_barrage: ^0.5.5 44 | #json_theme: 6.5.0 45 | uuid: ^3.0.7 46 | native_context_menu: ^0.2.2+5 47 | # dio cache 48 | dio_cache_interceptor: ^3.5.1 49 | shortid: ^0.1.2 50 | #bitsdojo_window: ^0.1.6 51 | window_manager: 0.4.3 52 | synchronized: 3.1.0 53 | xml2json: ^6.2.3 54 | ip2region_plus: ^1.0.2 55 | dart_ping: ^9.0.1 56 | url_launcher: ^6.3.0 57 | cached_network_image: ^3.3.1 58 | logging: ^1.2.0 59 | chinese_font_library: ^1.2.0 60 | crypto: ^3.0.5 61 | video_player: ^2.9.2 62 | package_info_plus: ^8.1.3 63 | archive: ^4.0.2 64 | uni_links_desktop: ^0.2.0 65 | uni_links: ^0.5.1 66 | 67 | 68 | dev_dependencies: 69 | flutter_test: 70 | sdk: flutter 71 | ffigen: ^7.2.6 72 | freezed: ^2.1.0+1 73 | #替换图标 74 | flutter_launcher_icons: ^0.10.0 75 | 76 | dependency_overrides: 77 | flutter_launcher_icons: ^0.13.1 78 | flutter_easyloading: ^3.0.3 79 | intl: 0.18.0 80 | meta: 1.12.0 81 | 82 | flutter_icons: 83 | image_path: "assets//logo.png" 84 | min_sdk_android: 21 # android min sdk min:16, default 21 85 | web: 86 | generate: true 87 | image_path: "assets/logo.png" 88 | background_color: "#hexcode" 89 | theme_color: "#hexcode" 90 | windows: 91 | generate: true 92 | image_path: "assets/logo.png" 93 | icon_size: 64 # min:48, max:256, default: 48 94 | 95 | # For information on the generic Dart part of this file, see the 96 | # following page: https://dart.dev/tools/pub/pubspec 97 | 98 | # The following section is specific to Flutter. 99 | flutter: 100 | # The following line ensures that the Material Icons font is 101 | # included with your application, so that you can use the icons in 102 | # the material Icons class. 103 | uses-material-design: true 104 | 105 | # To add assets to your application, add an assets section, like this: 106 | assets: 107 | - assets/ 108 | # - images/a_dot_ham.jpeg 109 | 110 | # An image asset can refer to one or more resolution-specific "variants", see 111 | # https://flutter.dev/assets-and-images/#resolution-aware. 112 | 113 | # For details regarding adding assets from package dependencies, see 114 | # https://flutter.dev/assets-and-images/#from-packages 115 | 116 | # To add custom fonts to your application, add a fonts section here, 117 | # in this "flutter" section. Each entry in this list should have a 118 | # "family" key with the font family name, and a "fonts" key with a 119 | # list giving the asset and other descriptors for the font. For 120 | # example: 121 | fonts: 122 | - family: WenQuanWeiMiHei 123 | fonts: 124 | - asset: assets/fonts/WenQuanWeiMiHei.ttf 125 | # - family: Trajan Pro 126 | # fonts: 127 | # - asset: fonts/TrajanPro.ttf 128 | # - asset: fonts/TrajanPro_Bold.ttf 129 | # weight: 700 130 | # 131 | # For details regarding fonts from package dependencies, 132 | # see https://flutter.dev/custom-fonts/#from-packages 133 | -------------------------------------------------------------------------------- /lib/components/updater/updater.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 3 | import 'package:vvibe/common/colors/colors.dart'; 4 | import 'package:vvibe/common/values/enum.dart'; 5 | import 'package:vvibe/services/event_bus.dart'; 6 | import 'package:vvibe/utils/updater.dart'; 7 | 8 | class Updater extends StatefulWidget { 9 | Updater({Key? key, required this.child}) : super(key: key); 10 | Widget child; 11 | @override 12 | _UpdaterState createState() => _UpdaterState(); 13 | } 14 | 15 | class _UpdaterState extends State { 16 | UpdatStatus status = UpdatStatus.idle; 17 | Map info = {}; 18 | @override 19 | void initState() { 20 | super.initState(); 21 | 22 | //checkForUpdate(); 23 | eventBus.on('check-for-update', (e) { 24 | checkForUpdate(true); 25 | }); 26 | } 27 | 28 | checkForUpdate([bool showLoading = false]) async { 29 | EasyLoading.dismiss(); 30 | if (showLoading) { 31 | EasyLoading.show(status: '正在检查更新!'); 32 | } 33 | UpdaterUtil.initDownloadDir(); 34 | setState(() { 35 | status = UpdatStatus.checking; 36 | }); 37 | final _info = await UpdaterUtil.checkForUpdate(); 38 | if (_info == null) { 39 | setState(() { 40 | status = UpdatStatus.error; 41 | }); 42 | if (showLoading) { 43 | EasyLoading.showError('检查更新失败'); 44 | } 45 | return; 46 | } 47 | if (mounted) { 48 | setState(() { 49 | status = 50 | _info['available'] ? UpdatStatus.available : UpdatStatus.upToDate; 51 | info = _info; 52 | }); 53 | } 54 | print('need update $status'); 55 | 56 | EasyLoading.dismiss(); 57 | if (status == UpdatStatus.upToDate) { 58 | UpdaterUtil.clearDownloaDir(); 59 | } 60 | } 61 | 62 | dismissUpdate() { 63 | setState(() { 64 | status = UpdatStatus.idle; 65 | }); 66 | } 67 | 68 | startUpdate() async { 69 | setState(() { 70 | status = UpdatStatus.downloading; 71 | }); 72 | final st = await UpdaterUtil.startDownload(info['latest']); 73 | setState(() { 74 | status = st != null ? st : UpdatStatus.error; 75 | }); 76 | } 77 | 78 | startInstallUpdate() async { 79 | setState(() { 80 | status = UpdatStatus.idle; 81 | }); 82 | UpdaterUtil.startInstallUpdate(); 83 | } 84 | 85 | String get text { 86 | switch (status) { 87 | case UpdatStatus.available: 88 | return '发现新版本: ${info['latest'] ?? ''}'; 89 | case UpdatStatus.readyToInstall: 90 | return '更新已下载'; 91 | default: 92 | return ''; 93 | } 94 | } 95 | 96 | String get cancelTooltip { 97 | switch (status) { 98 | case UpdatStatus.available: 99 | return '取消升级'; 100 | case UpdatStatus.readyToInstall: 101 | return '取消安装'; 102 | default: 103 | return ''; 104 | } 105 | } 106 | 107 | String get confirmTooltip { 108 | switch (status) { 109 | case UpdatStatus.available: 110 | return '立即升级'; 111 | case UpdatStatus.readyToInstall: 112 | return '立即安装'; 113 | default: 114 | return ''; 115 | } 116 | } 117 | 118 | Function get cancelCb { 119 | switch (status) { 120 | case UpdatStatus.available: 121 | case UpdatStatus.readyToInstall: 122 | return dismissUpdate; 123 | default: 124 | return () {}; 125 | } 126 | } 127 | 128 | Function get confirmCb { 129 | switch (status) { 130 | case UpdatStatus.available: 131 | return startUpdate; 132 | case UpdatStatus.readyToInstall: 133 | return startInstallUpdate; 134 | default: 135 | return () {}; 136 | } 137 | } 138 | 139 | bool get showChip { 140 | switch (status) { 141 | case UpdatStatus.available: 142 | case UpdatStatus.readyToInstall: 143 | return true; 144 | default: 145 | return false; 146 | } 147 | } 148 | 149 | @override 150 | Widget build(BuildContext context) { 151 | return Stack( 152 | children: [ 153 | widget.child, 154 | Positioned( 155 | bottom: 10, 156 | right: 10, 157 | child: Opacity( 158 | opacity: showChip ? 1 : 0, 159 | child: Row( 160 | children: [ 161 | Chip( 162 | label: Text( 163 | text, 164 | style: TextStyle( 165 | color: Colors.white, 166 | ), 167 | ), 168 | deleteButtonTooltipMessage: cancelTooltip, 169 | onDeleted: () { 170 | cancelCb(); 171 | }, 172 | ), 173 | Tooltip( 174 | message: confirmTooltip, 175 | child: IconButton( 176 | color: AppColors.primaryColor, 177 | icon: Icon(Icons.check, color: AppColors.primaryColor), 178 | onPressed: () { 179 | confirmCb(); 180 | }, 181 | )), 182 | ], 183 | ), 184 | )) 185 | ], 186 | ); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /lib/components/player/settings/player_settings.dart: -------------------------------------------------------------------------------- 1 | //播放器的设置弹窗 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 4 | import 'package:vvibe/common/values/values.dart'; 5 | import 'package:vvibe/utils/playlist/epg_util.dart'; 6 | import 'package:vvibe/utils/utils.dart'; 7 | 8 | import 'widgets/settings_widgets.dart'; 9 | 10 | class PlayerSettings extends StatefulWidget { 11 | const PlayerSettings({Key? key}) : super(key: key); 12 | 13 | @override 14 | _PlayerSettingsState createState() => _PlayerSettingsState(); 15 | } 16 | 17 | class _PlayerSettingsState extends State { 18 | final TextEditingController _uaTextCtl = TextEditingController(); 19 | final TextEditingController _epgUrlTextCtl = TextEditingController(); 20 | final TextEditingController _danmuFSizeTextCtl = TextEditingController(); 21 | bool _checkAlive = true; 22 | bool _deinterlace = false; 23 | bool _showLogo = false; 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _initData(); 28 | } 29 | 30 | void _initData() async { 31 | final v = await LoacalStorage().getJSON(PLAYER_SETTINGS); 32 | 33 | if (v != null) { 34 | _uaTextCtl.text = v['ua'] ?? ''; 35 | _epgUrlTextCtl.text = v['epg'] ?? ''; 36 | _danmuFSizeTextCtl.text = v['dmFSize'].toString(); 37 | setState(() { 38 | _checkAlive = PlaylistUtil().isBoolValid(v, false); 39 | _deinterlace = PlaylistUtil().isBoolValid(v['deinterlace'], false); 40 | _showLogo = PlaylistUtil().isBoolValid(v['showLogo']); 41 | }); 42 | } 43 | } 44 | 45 | get _buildInputRow => SettingsWidgets.buildInputRow; 46 | get _buildSwitch => SettingsWidgets.buildSwitch; 47 | 48 | void save() async { 49 | final ua = _uaTextCtl.text, 50 | epg = _epgUrlTextCtl.text, 51 | dmFSize = _danmuFSizeTextCtl.text; 52 | if (double.tryParse(dmFSize) == null) { 53 | EasyLoading.showError('弹幕尺寸只能输入数字'); 54 | return; 55 | } 56 | final _map = { 57 | 'ua': ua.isNotEmpty ? ua : DEF_REQ_UA, 58 | 'epg': epg.isNotEmpty ? epg : DEF_EPG_URL, 59 | 'dmFSize': dmFSize.isNotEmpty ? int.parse(dmFSize) : DEF_DM_FONT_SIZE, 60 | 'checkAlive': _checkAlive.toString(), 61 | 'deinterlace': _deinterlace.toString(), 62 | 'showLogo': _showLogo.toString(), 63 | }; 64 | await LoacalStorage().setJSON(PLAYER_SETTINGS, _map); 65 | EasyLoading.showSuccess('保存成功'); 66 | EpgUtil().downloadEpgDataAync(); 67 | } 68 | 69 | @override 70 | Widget build(BuildContext context) { 71 | return SizedBox( 72 | height: 1500, 73 | child: Column( 74 | children: [ 75 | Row( 76 | children: [ 77 | _buildInputRow(_uaTextCtl, 78 | label: 'User-Agent', 79 | inputWidth: 450.0, 80 | decoration: InputDecoration( 81 | hintText: '全局请求User-Agent,默认 ${DEF_REQ_UA}')), 82 | SizedBox( 83 | width: 50, 84 | ), 85 | _buildInputRow(_epgUrlTextCtl, 86 | label: 'EPG地址', 87 | inputWidth: 350.0, 88 | decoration: 89 | InputDecoration(hintText: 'EPG地址,默认 ${DEF_EPG_URL}')), 90 | ], 91 | ), 92 | Row(children: [ 93 | _buildInputRow(_danmuFSizeTextCtl, 94 | label: '弹幕大小', 95 | inputWidth: 450.0, 96 | decoration: InputDecoration(hintText: '弹幕字体大小,默认20')), 97 | ]), 98 | SizedBox( 99 | height: 20, 100 | ), 101 | Row( 102 | children: [ 103 | Row( 104 | children: [ 105 | _buildSwitch('实时检测', _checkAlive, (value) { 106 | setState(() { 107 | _checkAlive = value; 108 | }); 109 | }), 110 | SizedBox( 111 | width: 50, 112 | ), 113 | _buildSwitch('频道图标', _showLogo, (value) { 114 | setState(() { 115 | _showLogo = value; 116 | }); 117 | }), 118 | SizedBox( 119 | width: 50, 120 | ), 121 | _buildSwitch('反交错', _deinterlace, (value) { 122 | setState(() { 123 | _deinterlace = value; 124 | }); 125 | }), 126 | ], 127 | ), 128 | ], 129 | ), 130 | SizedBox( 131 | height: 50, 132 | ), 133 | Center( 134 | child: ElevatedButton( 135 | child: Padding( 136 | padding: const EdgeInsets.all(10), child: Text("立即保存")), 137 | onPressed: () { 138 | // 通过_formKey.currentState 获取FormState后, 139 | // 调用validate()方法校验用户名密码是否合法,校验 140 | // 通过后再提交数据。 141 | save(); 142 | }, 143 | ), 144 | ) 145 | ], 146 | ), 147 | ); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.13) 3 | project(runner LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "vvibe") 8 | # The unique GTK application identifier for this application. See: 9 | # https://wiki.gnome.org/HowDoI/ChooseApplicationID 10 | set(APPLICATION_ID "com.vvibe") 11 | 12 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 13 | # versions of CMake. 14 | cmake_policy(SET CMP0063 NEW) 15 | 16 | # Load bundled libraries from the lib/ directory relative to the binary. 17 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 18 | 19 | # Root filesystem for cross-building. 20 | if(FLUTTER_TARGET_PLATFORM_SYSROOT) 21 | set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) 22 | set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) 23 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 24 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 25 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 26 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 27 | endif() 28 | 29 | # Define build configuration options. 30 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 31 | set(CMAKE_BUILD_TYPE "Debug" CACHE 32 | STRING "Flutter build mode" FORCE) 33 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 34 | "Debug" "Profile" "Release") 35 | endif() 36 | 37 | # Compilation settings that should be applied to most targets. 38 | # 39 | # Be cautious about adding new options here, as plugins use this function by 40 | # default. In most cases, you should add new options to specific targets instead 41 | # of modifying this function. 42 | function(APPLY_STANDARD_SETTINGS TARGET) 43 | target_compile_features(${TARGET} PUBLIC cxx_std_14) 44 | target_compile_options(${TARGET} PRIVATE -Wall -Werror) 45 | target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") 46 | target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") 47 | endfunction() 48 | 49 | # Flutter library and tool build rules. 50 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 51 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 52 | 53 | # System-level dependencies. 54 | find_package(PkgConfig REQUIRED) 55 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 56 | 57 | # Application build; see runner/CMakeLists.txt. 58 | add_subdirectory("runner") 59 | 60 | # Run the Flutter tool portions of the build. This must not be removed. 61 | add_dependencies(${BINARY_NAME} flutter_assemble) 62 | 63 | # Only the install-generated bundle's copy of the executable will launch 64 | # correctly, since the resources must in the right relative locations. To avoid 65 | # people trying to run the unbundled copy, put it in a subdirectory instead of 66 | # the default top-level location. 67 | set_target_properties(${BINARY_NAME} 68 | PROPERTIES 69 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" 70 | ) 71 | 72 | 73 | # Generated plugin build rules, which manage building the plugins and adding 74 | # them to the application. 75 | include(flutter/generated_plugins.cmake) 76 | 77 | 78 | # === Installation === 79 | # By default, "installing" just makes a relocatable bundle in the build 80 | # directory. 81 | set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") 82 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 83 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 84 | endif() 85 | 86 | # Start with a clean build bundle directory every time. 87 | install(CODE " 88 | file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") 89 | " COMPONENT Runtime) 90 | 91 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 92 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") 93 | 94 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 95 | COMPONENT Runtime) 96 | 97 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 98 | COMPONENT Runtime) 99 | 100 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 101 | COMPONENT Runtime) 102 | 103 | foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) 104 | install(FILES "${bundled_library}" 105 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 106 | COMPONENT Runtime) 107 | endforeach(bundled_library) 108 | 109 | # Copy the native assets provided by the build.dart from all packages. 110 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") 111 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 112 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 113 | COMPONENT Runtime) 114 | 115 | # Fully re-copy the assets directory on each build to avoid having stale files 116 | # from a previous install. 117 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 118 | install(CODE " 119 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 120 | " COMPONENT Runtime) 121 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 122 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 123 | 124 | # Install the AOT library on non-Debug builds only. 125 | if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") 126 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 127 | COMPONENT Runtime) 128 | endif() 129 | -------------------------------------------------------------------------------- /lib/components/player/settings/play_file_setting_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 3 | import 'package:vvibe/common/values/consts.dart'; 4 | import 'package:vvibe/utils/local_storage.dart'; 5 | import 'package:vvibe/utils/playlist/playlist_util.dart'; 6 | 7 | import './widgets/settings_widgets.dart'; 8 | 9 | class PlayFileSettingDialog extends StatefulWidget { 10 | PlayFileSettingDialog({Key? key, required this.file}) : super(key: key); 11 | Map file; 12 | 13 | @override 14 | _PlayFileSettingDialogState createState() => _PlayFileSettingDialogState(); 15 | } 16 | 17 | class _PlayFileSettingDialogState extends State { 18 | final TextEditingController _uaTextCtl = TextEditingController(); 19 | final TextEditingController _epgUrlTextCtl = TextEditingController(); 20 | final TextEditingController _bgController = TextEditingController(); 21 | late String name, url, ua, epg, blackGroups; 22 | bool _checkAlive = false; 23 | bool _showLogo = false; 24 | bool _deinterlace = false; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | _initData(); 30 | } 31 | 32 | Map get file => widget.file; 33 | String get cacheKey => '${file['name'] ?? 'default'}_PLAY_SETTINGS'; 34 | void _initData() async { 35 | final v = await LoacalStorage().getJSON(cacheKey); 36 | if (v != null) { 37 | print(v); 38 | _uaTextCtl.text = v['ua'] ?? ''; 39 | _epgUrlTextCtl.text = v['epg'] ?? ''; 40 | _epgUrlTextCtl.text = v['epg'] ?? ''; 41 | _bgController.text = v['blackGroups'] ?? ''; 42 | setState(() { 43 | _checkAlive = PlaylistUtil().isBoolValid(v['checkAlive'], false); 44 | _showLogo = PlaylistUtil().isBoolValid(v['showLogo']); 45 | _deinterlace = PlaylistUtil().isBoolValid(v['deinterlace'], false); 46 | }); 47 | } 48 | } 49 | 50 | _submit() { 51 | final values = {}; 52 | values['ua'] = _uaTextCtl.text; 53 | values['epg'] = _epgUrlTextCtl.text; 54 | values['blackGroups'] = _bgController.text; 55 | values['checkAlive'] = _checkAlive.toString(); 56 | values['showLogo'] = _showLogo.toString(); 57 | values['deinterlace'] = _deinterlace.toString(); 58 | LoacalStorage().setJSON(cacheKey, values); 59 | EasyLoading.showSuccess('保存成功'); 60 | } 61 | 62 | get _buildInputRow => SettingsWidgets.buildInputRow; 63 | get _buildSwitch => SettingsWidgets.buildSwitch; 64 | @override 65 | Widget build(BuildContext context) { 66 | return AlertDialog( 67 | title: Text('${widget.file['name']} 播放设置'), 68 | actions: [ 69 | //TextButton(child: const Text('保存'), onPressed: () {}), 70 | ], 71 | content: SizedBox( 72 | width: 800, 73 | height: 500, 74 | child: Column( 75 | children: [ 76 | _buildInputRow(_uaTextCtl, 77 | label: 'User-Agent', 78 | decoration: InputDecoration( 79 | hintText: '当前文件请求User-Agent,默认 ${DEF_REQ_UA}')), 80 | _buildInputRow(_epgUrlTextCtl, 81 | label: 'EPG地址', 82 | decoration: InputDecoration( 83 | hintText: '当前文件EPG地址,支持.xml/.xml.gz,默认 ${DEF_EPG_URL}')), 84 | _buildInputRow(_bgController, 85 | label: '屏蔽分组', 86 | decoration: InputDecoration(hintText: '屏蔽分组, 英文逗号分隔')), 87 | Padding( 88 | padding: EdgeInsets.only(top: 20), 89 | child: Row( 90 | children: [ 91 | _buildSwitch('实时检测', _checkAlive, (value) { 92 | setState(() { 93 | _checkAlive = value; 94 | }); 95 | }), 96 | SizedBox( 97 | width: 50, 98 | ), 99 | _buildSwitch('频道图标', _showLogo, (value) { 100 | setState(() { 101 | _showLogo = value; 102 | }); 103 | }), 104 | SizedBox( 105 | width: 50, 106 | ), 107 | _buildSwitch('反交错', _deinterlace, (value) { 108 | setState(() { 109 | _deinterlace = value; 110 | }); 111 | }), 112 | ], 113 | )), 114 | Padding( 115 | padding: EdgeInsets.only(top: 50), 116 | child: Center( 117 | child: SizedBox( 118 | width: 130, 119 | child: ElevatedButton( 120 | child: Padding( 121 | padding: const EdgeInsets.all(10), 122 | child: Row( 123 | children: [ 124 | Text("立即保存"), 125 | ], 126 | )), 127 | onPressed: () { 128 | _submit(); 129 | }, 130 | ), 131 | ))) 132 | ], 133 | ), 134 | )); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /linux/runner/my_application.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | #include 4 | #ifdef GDK_WINDOWING_X11 5 | #include 6 | #endif 7 | 8 | #include "flutter/generated_plugin_registrant.h" 9 | 10 | struct _MyApplication { 11 | GtkApplication parent_instance; 12 | char** dart_entrypoint_arguments; 13 | }; 14 | 15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 16 | 17 | // Implements GApplication::activate. 18 | static void my_application_activate(GApplication* application) { 19 | MyApplication* self = MY_APPLICATION(application); 20 | GtkWindow* window = 21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 22 | 23 | // Use a header bar when running in GNOME as this is the common style used 24 | // by applications and is the setup most users will be using (e.g. Ubuntu 25 | // desktop). 26 | // If running on X and not using GNOME then just use a traditional title bar 27 | // in case the window manager does more exotic layout, e.g. tiling. 28 | // If running on Wayland assume the header bar will work (may need changing 29 | // if future cases occur). 30 | gboolean use_header_bar = TRUE; 31 | #ifdef GDK_WINDOWING_X11 32 | GdkScreen* screen = gtk_window_get_screen(window); 33 | if (GDK_IS_X11_SCREEN(screen)) { 34 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); 35 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) { 36 | use_header_bar = FALSE; 37 | } 38 | } 39 | #endif 40 | if (use_header_bar) { 41 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 42 | gtk_widget_show(GTK_WIDGET(header_bar)); 43 | gtk_header_bar_set_title(header_bar, "vvibe"); 44 | gtk_header_bar_set_show_close_button(header_bar, TRUE); 45 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 46 | } else { 47 | gtk_window_set_title(window, "vvibe"); 48 | } 49 | 50 | gtk_window_set_default_size(window, 1280, 750); 51 | // gtk_widget_show(GTK_WIDGET(window)); 52 | gtk_widget_realize(GTK_WIDGET(window)); 53 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 54 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 55 | 56 | FlView* view = fl_view_new(project); 57 | gtk_widget_show(GTK_WIDGET(view)); 58 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 59 | 60 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 61 | 62 | gtk_widget_grab_focus(GTK_WIDGET(view)); 63 | } 64 | 65 | // Implements GApplication::local_command_line. 66 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { 67 | MyApplication* self = MY_APPLICATION(application); 68 | // Strip out the first argument as it is the binary name. 69 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); 70 | 71 | g_autoptr(GError) error = nullptr; 72 | if (!g_application_register(application, nullptr, &error)) { 73 | g_warning("Failed to register: %s", error->message); 74 | *exit_status = 1; 75 | return TRUE; 76 | } 77 | 78 | g_application_activate(application); 79 | *exit_status = 0; 80 | 81 | return TRUE; 82 | } 83 | 84 | // Implements GApplication::startup. 85 | static void my_application_startup(GApplication* application) { 86 | //MyApplication* self = MY_APPLICATION(object); 87 | 88 | // Perform any actions required at application startup. 89 | 90 | G_APPLICATION_CLASS(my_application_parent_class)->startup(application); 91 | } 92 | 93 | // Implements GApplication::shutdown. 94 | static void my_application_shutdown(GApplication* application) { 95 | //MyApplication* self = MY_APPLICATION(object); 96 | 97 | // Perform any actions required at application shutdown. 98 | 99 | G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); 100 | } 101 | 102 | // Implements GObject::dispose. 103 | static void my_application_dispose(GObject* object) { 104 | MyApplication* self = MY_APPLICATION(object); 105 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 106 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 107 | } 108 | 109 | static void my_application_class_init(MyApplicationClass* klass) { 110 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 111 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; 112 | G_APPLICATION_CLASS(klass)->startup = my_application_startup; 113 | G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; 114 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 115 | } 116 | 117 | static void my_application_init(MyApplication* self) {} 118 | 119 | MyApplication* my_application_new() { 120 | // Set the program name to the application ID, which helps various systems 121 | // like GTK and desktop environments map this running application to its 122 | // corresponding .desktop file. This ensures better integration by allowing 123 | // the application to be recognized beyond its binary name. 124 | g_set_prgname(APPLICATION_ID); 125 | 126 | return MY_APPLICATION(g_object_new(my_application_get_type(), 127 | "application-id", APPLICATION_ID, 128 | "flags", G_APPLICATION_NON_UNIQUE, 129 | nullptr)); 130 | } 131 | -------------------------------------------------------------------------------- /lib/components/player/epg/epg_channel_date.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 3 | import 'package:intl/intl.dart'; 4 | import 'package:vvibe/models/channel_epg.dart'; 5 | import 'package:vvibe/models/playlist_item.dart'; 6 | import 'package:vvibe/utils/playlist/epg_util.dart'; 7 | import 'package:vvibe/utils/utils.dart'; 8 | 9 | class EpgChannelDate extends StatefulWidget { 10 | const EpgChannelDate( 11 | {Key? key, 12 | required this.urlItem, 13 | required String this.date, 14 | this.epgUrl, 15 | required this.doPlayback}) 16 | : super(key: key); 17 | final PlayListItem urlItem; 18 | final String date; 19 | final String? epgUrl; 20 | final Function doPlayback; 21 | @override 22 | _EpgChannelDateState createState() => _EpgChannelDateState(); 23 | } 24 | 25 | class _EpgChannelDateState extends State { 26 | ChannelEpg? data = null; 27 | @override 28 | void initState() { 29 | super.initState(); 30 | getEpgData(); 31 | } 32 | 33 | void getEpgData() async { 34 | try { 35 | await EpgUtil().downloadEpgDataAync(epgUrl: widget.epgUrl); 36 | final name = widget.urlItem.tvgName!.isNotEmpty 37 | ? widget.urlItem.tvgName 38 | : widget.urlItem.name; 39 | final id = widget.urlItem.tvgId; 40 | if (name == null || name == '' && (id == null || id == '')) { 41 | EasyLoading.showError('缺少频道名称,无法获取节目单'); 42 | return; 43 | } 44 | EasyLoading.show(status: '正在加载节目单'); 45 | ChannelEpg? _data = await EpgUtil().getChannelDateEpg(id, widget.date); 46 | if (_data == null) { 47 | _data = await EpgUtil().getChannelDateEpg(name, widget.date); 48 | } 49 | setState(() { 50 | data = _data; 51 | }); 52 | EasyLoading.dismiss(); 53 | } catch (e) { 54 | EasyLoading.showError('加载节目单失败: ' + e.toString()); 55 | EasyLoading.dismiss(); 56 | } 57 | } 58 | 59 | @override 60 | void dispose() { 61 | super.dispose(); 62 | } 63 | 64 | String _toSeekTime(DateTime time) { 65 | return DateFormat('yyyyMMddHHmmss').format(time); 66 | } 67 | 68 | bool canUrlPlayback() { 69 | final url = widget.urlItem.url; 70 | return (widget.urlItem.catchup != null && 71 | widget.urlItem.catchupSource != null) || 72 | url.indexOf('PLTV') > -1 || 73 | url.indexOf('TVOD') > -1; 74 | } 75 | 76 | String getPlayseek(EpgDatum epg) { 77 | return _toSeekTime(epg.start) + '-' + _toSeekTime(epg.end); 78 | //_toSeekTime(epg.start) + '-' + _toSeekTime(epg.end); 79 | } 80 | 81 | Widget _setBtn(EpgDatum epg, 82 | {bool isLive = false, bool played = false, bool toPlay = false}) { 83 | final canPlayback = canUrlPlayback(); 84 | final text = Text( 85 | isLive ? '正在直播' : (toPlay ? '未播放' : (canPlayback ? '回看' : '已播放')), 86 | style: TextStyle( 87 | color: isLive 88 | ? Colors.purple 89 | : (toPlay ? Colors.grey[600] : Colors.blue[300]))); 90 | if (played && !isLive && canPlayback) { 91 | return TextButton( 92 | onPressed: () { 93 | widget.doPlayback(getPlayseek(epg)); 94 | }, 95 | child: text); 96 | } 97 | return Padding( 98 | padding: const EdgeInsets.only(left: 28), 99 | child: text, 100 | ); 101 | } 102 | 103 | Widget _EpgRow(EpgDatum epg) { 104 | DateTime now = DateTime.now(); 105 | DateTime st = (epg.start); 106 | DateTime et = (epg.end); 107 | final isLive = now.isAfter(st) && now.isBefore(et), 108 | played = st.isBefore(now), 109 | toPlay = et.isAfter(now); 110 | return Container( 111 | padding: const EdgeInsets.only(top: 2, bottom: 2), 112 | child: Flex(direction: Axis.horizontal, children: [ 113 | Expanded( 114 | child: Row( 115 | children: [ 116 | Text( 117 | DateFormat('HH:mm').format(epg.start), 118 | style: TextStyle(color: Colors.purple), 119 | ), 120 | SizedBox( 121 | width: 20, 122 | ), 123 | Text(epg.title), 124 | SizedBox( 125 | width: 20, 126 | ), 127 | Text( 128 | '结束于:${DateFormat('HH:mm').format(epg.end)}', 129 | style: TextStyle(color: Colors.grey[600], fontSize: 12), 130 | ), 131 | ], 132 | )), 133 | SizedBox( 134 | width: 100, 135 | child: _setBtn(epg, isLive: isLive, played: played, toPlay: toPlay), 136 | ), 137 | ]), 138 | decoration: BoxDecoration( 139 | color: isLive ? Colors.grey[100] : Colors.transparent, 140 | border: Border(bottom: BorderSide(width: 0.5, color: Colors.grey))), 141 | ); 142 | } 143 | 144 | Widget _buildList() { 145 | List _epg = data?.epg ?? []; 146 | if (_epg.length != 0) { 147 | return ListView.builder( 148 | shrinkWrap: true, 149 | itemCount: _epg.length, 150 | itemExtent: 25.0, 151 | cacheExtent: getDeviceHeight(context) - 120.0, 152 | itemBuilder: (context, index) { 153 | final e = _epg[index]; 154 | return _EpgRow(e); 155 | }); 156 | } else { 157 | return Center( 158 | child: SizedBox( 159 | child: Text('节目单为空'), 160 | ), 161 | ); 162 | } 163 | } 164 | 165 | @override 166 | Widget build(BuildContext context) { 167 | return Container( 168 | child: _buildList(), 169 | ); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /lib/services/danmaku/bilibili_danmaku_service.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Moxx 3 | * @Date: 2022-09-08 09:29:34 4 | * @Last Modified by: Moxx 5 | * @Last Modified time: 2022-09-08 12:19:28 6 | */ 7 | 8 | import 'dart:async'; 9 | import 'dart:convert'; 10 | import 'dart:io'; 11 | import 'dart:math'; 12 | import 'package:flutter/foundation.dart'; 13 | import 'package:vvibe/models/live_danmaku_item.dart'; 14 | import 'package:vvibe/utils/logger.dart'; 15 | import 'package:web_socket_channel/io.dart'; 16 | 17 | class BilibiliDanmakuService { 18 | BilibiliDanmakuService({required this.roomId, required this.onDanmaku}); 19 | final String roomId; 20 | final void Function(LiveDanmakuItem? danmaku) onDanmaku; 21 | 22 | Timer? timer; 23 | IOWebSocketChannel? _channel; 24 | int totleTime = 0; 25 | 26 | //开始连接 27 | void connect() { 28 | MyLogger.info('b站登录弹幕'); 29 | timer = Timer.periodic(const Duration(seconds: 30), (callback) { 30 | totleTime += 30; 31 | //sendXinTiaoBao(); 32 | MyLogger.info("bilibili时间: $totleTime s"); 33 | // _channel!.sink.close(); 34 | // initLive(); 35 | sendHeartBeat(); 36 | }); 37 | initLive(); 38 | } 39 | 40 | //初始化 41 | Future initLive() async { 42 | _channel = 43 | IOWebSocketChannel.connect('wss://broadcastlv.chat.bilibili.com/sub'); 44 | joinRoom(roomId); 45 | setListener(); 46 | } 47 | 48 | void sendHeartBeat() { 49 | List code = [0, 0, 0, 0, 0, 16, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1]; 50 | _channel?.sink.add(Uint8List.fromList(code)); 51 | } 52 | 53 | //加入房间 54 | void joinRoom(String id) { 55 | String msg = "{" 56 | "\"roomid\":${int.parse(id)}," 57 | "\"uId\":0," 58 | "\"protover\":2," 59 | "\"platform\":\"web\"," 60 | "\"clientver\":\"1.10.6\"," 61 | "\"type\":2," 62 | "\"key\":\"" 63 | "\"}"; 64 | MyLogger.info('B站login'); 65 | _channel?.sink.add(encode(7, msg: msg)); 66 | sendHeartBeat(); 67 | } 68 | 69 | //对消息编码 70 | Uint8List encode(int op, {String? msg}) { 71 | List header = [0, 0, 0, 0, 0, 16, 0, 1, 0, 0, 0, op, 0, 0, 0, 1]; 72 | if (msg != null) { 73 | List msgCode = utf8.encode(msg); 74 | header.addAll(msgCode); 75 | } 76 | Uint8List uint8list = Uint8List.fromList(header); 77 | uint8list = writeInt(uint8list, 0, 4, header.length); 78 | return uint8list; 79 | } 80 | 81 | //对消息进行解码 82 | decode(Uint8List list) { 83 | try { 84 | int headerLen = readInt(list, 4, 2); 85 | int ver = readInt(list, 6, 2); 86 | int op = readInt(list, 8, 4); 87 | 88 | switch (op) { 89 | case 8: 90 | MyLogger.info("B站进入房间"); 91 | break; 92 | case 5: 93 | int offset = 0; 94 | while (offset < list.length) { 95 | int packLen = readInt(list, offset + 0, 4); 96 | int headerLen = readInt(list, offset + 4, 2); 97 | Uint8List body; 98 | if (ver == 2) { 99 | body = list.sublist(offset + headerLen, offset + packLen); 100 | decode(ZLibDecoder().convert(body) as Uint8List); 101 | offset += packLen; 102 | continue; 103 | } else { 104 | body = list.sublist(offset + headerLen, offset + packLen); 105 | } 106 | String data = utf8.decode(body); 107 | offset += packLen; 108 | Map jd = json.decode(data); 109 | switch (jd["cmd"]) { 110 | case "DANMU_MSG": 111 | String msg = jd["info"][1].toString(); 112 | String name = jd["info"][2][1].toString(); 113 | String uid = jd["info"][2][0].toString(); 114 | 115 | final extStr = jd["info"][0][15]; 116 | 117 | final extMap = extStr['extra'] ?? {}; 118 | print(extMap); 119 | 120 | /* Color color = 121 | ColorUtil.fromDecimal(extMap['color']?.toString()); */ 122 | // addDanmaku(LiveDanmakuItem(name, msg)); 123 | MyLogger.info('B站弹幕--> $uid $name: $msg '); 124 | onDanmaku(LiveDanmakuItem( 125 | name: name, 126 | msg: msg, 127 | uid: uid, 128 | //color: color, 129 | ext: {}, 130 | )); 131 | break; 132 | default: 133 | } 134 | } 135 | break; 136 | case 3: 137 | //int people = readInt(list, headerLen, 4); 138 | //MyLogger.info("B站房间人气: $people"); 139 | break; 140 | default: 141 | } 142 | } catch (e) { 143 | MyLogger.error('解析bili弹幕异常 ${e.toString()}'); 144 | } 145 | } 146 | 147 | //设置监听 148 | void setListener() { 149 | _channel!.stream.listen((msg) { 150 | Uint8List list = Uint8List.fromList(msg); 151 | decode(list); 152 | }); 153 | } 154 | 155 | //写入编码 156 | Uint8List writeInt(Uint8List src, int start, int len, int value) { 157 | int i = 0; 158 | while (i < len) { 159 | src[start + i] = value ~/ pow(256, len - i - 1); 160 | i++; 161 | } 162 | return src; 163 | } 164 | 165 | //从编码读出数字 166 | int readInt(Uint8List src, int start, int len) { 167 | int res = 0; 168 | for (int i = len - 1; i >= 0; i--) { 169 | res += pow(256, len - i - 1) * src[start + i] as int; 170 | } 171 | return res; 172 | } 173 | 174 | void displose() { 175 | timer?.cancel(); 176 | _channel?.sink.close(); 177 | _channel = null; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /lib/utils/ip2region.dart: -------------------------------------------------------------------------------- 1 | /* // ip2region 2 | 3 | import 'dart:async'; 4 | import 'dart:convert'; 5 | import 'dart:io'; 6 | 7 | const int VectorIndexSize = 8; 8 | const int VectorIndexCols = 256; 9 | const int VectorIndexLength = 256 * 256 * (4 + 4); 10 | const int SegmentIndexSize = 14; 11 | final RegExp IP_REGEX = RegExp( 12 | r'^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$'); 13 | 14 | class Searcher { 15 | late String _dbFile; 16 | late List _vectorIndex; 17 | List? _buffer; 18 | 19 | Searcher(this._dbFile, this._vectorIndex, this._buffer); 20 | 21 | Future> getStartEndPtr( 22 | int idx, RandomAccessFile fd, Map ioStatus) async { 23 | if (_vectorIndex.isNotEmpty) { 24 | final sPtr = _vectorIndex 25 | .sublist(idx, idx + 4) 26 | .buffer 27 | .asByteData() 28 | .getUint32(0, Endian.little); 29 | final ePtr = _vectorIndex 30 | .sublist(idx + 4, idx + 8) 31 | .buffer 32 | .asByteData() 33 | .getUint32(0, Endian.little); 34 | return {'sPtr': sPtr, 'ePtr': ePtr}; 35 | } else { 36 | final buf = await getBuffer(256 + idx, 8, fd, ioStatus); 37 | final sPtr = buf.buffer.asByteData().getUint32(0, Endian.little); 38 | final ePtr = buf.buffer.asByteData().getUint32(4, Endian.little); 39 | return {'sPtr': sPtr, 'ePtr': ePtr}; 40 | } 41 | } 42 | 43 | Future> getBuffer(int offset, int length, RandomAccessFile fd, 44 | Map ioStatus) async { 45 | if (_buffer != null) { 46 | return _buffer!.sublist(offset, offset + length); 47 | } else { 48 | final buf = List.filled(length, 0); 49 | ioStatus['ioCount'] = ioStatus['ioCount']! + 1; 50 | await fd.readInto(buf, 0, length, offset); 51 | return buf; 52 | } 53 | } 54 | 55 | Future openFilePromise() async { 56 | final file = File(_dbFile); 57 | return await file.open(); 58 | } 59 | 60 | Future> search(String ip) async { 61 | final startTime = DateTime.now().microsecondsSinceEpoch; 62 | final ioStatus = {'ioCount': 0}; 63 | 64 | if (!isValidIp(ip)) { 65 | throw Exception('IP: $ip is invalid'); 66 | } 67 | 68 | RandomAccessFile? fd; 69 | 70 | if (_buffer == null) { 71 | fd = await openFilePromise(); 72 | } 73 | 74 | final ps = ip.split('.'); 75 | final i0 = int.parse(ps[0]); 76 | final i1 = int.parse(ps[1]); 77 | final i2 = int.parse(ps[2]); 78 | final i3 = int.parse(ps[3]); 79 | 80 | final ipInt = i0 * 256 * 256 * 256 + i1 * 256 * 256 + i2 * 256 + i3; 81 | final idx = i0 * VectorIndexCols * VectorIndexSize + i1 * VectorIndexSize; 82 | final ptrMap = await getStartEndPtr(idx, fd!, ioStatus); 83 | final sPtr = ptrMap['sPtr']!; 84 | final ePtr = ptrMap['ePtr']!; 85 | var l = 0; 86 | var h = (ePtr - sPtr) ~/ SegmentIndexSize; 87 | String? result; 88 | 89 | while (l <= h) { 90 | final m = (l + h) >> 1; 91 | 92 | final p = sPtr + m * SegmentIndexSize; 93 | 94 | final buff = await getBuffer(p, SegmentIndexSize, fd, ioStatus); 95 | 96 | final sip = buff.buffer.asByteData().getUint32(0, Endian.little); 97 | 98 | if (ipInt < sip) { 99 | h = m - 1; 100 | } else { 101 | final eip = buff.buffer.asByteData().getUint32(4, Endian.little); 102 | if (ipInt > eip) { 103 | l = m + 1; 104 | } else { 105 | final dataLen = buff.buffer.asByteData().getUint16(8, Endian.little); 106 | final dataPtr = buff.buffer.asByteData().getUint32(10, Endian.little); 107 | 108 | final data = 109 | utf8.decode(await getBuffer(dataPtr, dataLen, fd, ioStatus)); 110 | 111 | result = data; 112 | break; 113 | } 114 | } 115 | } 116 | if (fd != null) { 117 | await fd.close(); 118 | } 119 | 120 | final diff = DateTime.now().microsecondsSinceEpoch - startTime; 121 | final took = diff / 1000; 122 | return {'region': result, 'ioCount': ioStatus['ioCount'], 'took': took}; 123 | } 124 | } 125 | 126 | void _checkFile(String filePath) { 127 | final file = File(filePath); 128 | if (!file.existsSync()) { 129 | throw Exception('$filePath does not exist'); 130 | } 131 | if (!file.statSync().modeString().contains('r')) { 132 | throw Exception('$filePath is not readable'); 133 | } 134 | } 135 | 136 | bool isValidIp(String ip) { 137 | return IP_REGEX.hasMatch(ip); 138 | } 139 | 140 | Searcher newWithFileOnly(String dbPath) { 141 | _checkFile(dbPath); 142 | 143 | return Searcher(dbPath, [], null); 144 | } 145 | 146 | Searcher newWithVectorIndex(String dbPath, List vectorIndex) { 147 | _checkFile(dbPath); 148 | 149 | if (vectorIndex.isEmpty) { 150 | throw Exception('vectorIndex is invalid'); 151 | } 152 | 153 | return Searcher(dbPath, vectorIndex, null); 154 | } 155 | 156 | Searcher newWithBuffer(List buffer) { 157 | if (buffer.isEmpty) { 158 | throw Exception('buffer is invalid'); 159 | } 160 | 161 | return Searcher(null, null, buffer); 162 | } 163 | 164 | List loadVectorIndexFromFile(String dbPath) { 165 | final file = File(dbPath); 166 | 167 | final fd = file.openSync(); 168 | final buffer = List.filled(VectorIndexLength, 0); 169 | fd.readIntoSync(buffer, 0, VectorIndexLength, 256); 170 | fd.closeSync(); 171 | return buffer; 172 | } 173 | 174 | List loadContentFromFile(String dbPath) { 175 | final file = File(dbPath); 176 | final stats = file.statSync(); 177 | final buffer = List.filled(stats.size, 0); 178 | final fd = file.openSync(); 179 | fd.readIntoSync(buffer, 0, stats.size); 180 | fd.closeSync(); 181 | return buffer; 182 | } 183 | */ -------------------------------------------------------------------------------- /lib/services/danmaku/douyu_danmaku_service.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Moxx 3 | * @Date: 2022-09-07 14:10:22 4 | * @Last Modified by: Moxx 5 | * @Last Modified time: 2022-09-08 12:19:29 6 | */ 7 | import 'dart:async'; 8 | import 'dart:convert'; 9 | 10 | import 'dart:ui'; 11 | 12 | import 'package:flutter/foundation.dart'; 13 | import 'package:vvibe/models/live_danmaku_item.dart'; 14 | import 'package:vvibe/utils/color_util.dart'; 15 | import 'package:vvibe/utils/logger.dart'; 16 | import 'package:web_socket_channel/io.dart'; 17 | 18 | class DouyuDnamakuService { 19 | DouyuDnamakuService({required this.roomId, required this.onDanmaku}); 20 | 21 | final String roomId; 22 | final void Function(LiveDanmakuItem? danmaku) onDanmaku; 23 | 24 | Timer? timer; 25 | IOWebSocketChannel? _channel; 26 | int totleTime = 0; 27 | 28 | //开始ws连接 29 | void connect() { 30 | _channel = IOWebSocketChannel.connect("wss://danmuproxy.douyu.com:8506"); 31 | login(); 32 | setListener(); 33 | timer = Timer.periodic(const Duration(seconds: 45), (callback) { 34 | totleTime += 45; 35 | heartBeat(); 36 | //print("时间: $totleTime s"); 37 | }); 38 | } 39 | 40 | //发送心跳包 41 | void heartBeat() { 42 | String heartbeat = 'type@=mrkl/'; 43 | _channel?.sink.add(encode(heartbeat)); 44 | } 45 | 46 | //设置监听 47 | void setListener() { 48 | _channel?.stream.listen((msg) { 49 | try { 50 | Uint8List list = Uint8List.fromList(msg); 51 | final LiveDanmakuItem? danmaku = decode(list); 52 | if (danmaku != null && danmaku.msg.isNotEmpty) { 53 | onDanmaku(danmaku); 54 | } 55 | } catch (e) { 56 | MyLogger.info(e.toString()); 57 | } 58 | }); 59 | } 60 | 61 | void login() { 62 | MyLogger.info("斗鱼 登录弹幕"); 63 | String roomID = roomId.toString(); 64 | String login = 65 | "type@=loginreq/room_id@=$roomID/dfl@=sn@A=105@Sss@A=1/username@=61609154/uid@=61609154/ver@=20190610/aver@=218101901/ct@=0/"; 66 | // print(login); 67 | _channel?.sink.add(encode(login)); 68 | String joingroup = "type@=joingroup/rid@=$roomID/gid@=-9999/"; 69 | // print(joingroup); 70 | _channel?.sink.add(encode(joingroup)); 71 | String heartbeat = 'type@=mrkl/'; 72 | //print(heartbeat); 73 | _channel?.sink.add(encode(heartbeat)); 74 | } 75 | 76 | Uint8List encode(String msg) { 77 | ByteData header = ByteData(12); 78 | //定义协议头 79 | header.setInt32(0, msg.length + 9, Endian.little); 80 | header.setInt32(4, msg.length + 9, Endian.little); 81 | header.setInt32(8, 689, Endian.little); 82 | List data = header.buffer.asUint8List().toList(); 83 | List msgData = utf8.encode(msg); 84 | data.addAll(msgData); 85 | //结尾 \0 协议规定 86 | data.add(0); 87 | return Uint8List.fromList(data); 88 | } 89 | 90 | //解析消息 91 | Map parseMsg(String msg) { 92 | final baseArr = msg.split("/"); 93 | Map msgMap = {}; 94 | 95 | for (var i = 0; i < baseArr.length; i++) { 96 | final kv = baseArr[i].split('@='); 97 | if (kv.length > 1) { 98 | final v = kv[0] == 'ic' ? kv[1].replaceAll('@S', '/') : kv[1]; 99 | msgMap[kv[0]] = v; 100 | } 101 | } 102 | return msgMap; 103 | } 104 | 105 | //弹幕颜色 106 | Color setDanmakuColor(String color) { 107 | int num = (int.tryParse(color) ?? 10) - 1; 108 | switch (num) { 109 | case 0: 110 | return ColorUtil.fromHex('#ff0000'); 111 | case 1: 112 | return ColorUtil.fromHex('#1e87f0'); 113 | case 2: 114 | return ColorUtil.fromHex('#7ac84b'); 115 | case 3: 116 | return ColorUtil.fromHex('#ff7f00'); 117 | case 4: 118 | return ColorUtil.fromHex('#9b39f4'); 119 | case 5: 120 | return ColorUtil.fromHex('#ff69b4'); 121 | 122 | default: 123 | return ColorUtil.fromDecimal(''); 124 | } 125 | } 126 | 127 | //对消息进行解码 128 | decode(Uint8List list) { 129 | try { 130 | //消息总长度 131 | int totalLength = list.length; 132 | // 当前消息长度 133 | int len = 0; 134 | int decodedMsgLen = 0; 135 | // 单条消息的 buffer 136 | Uint8List singleMsgBuffer; 137 | Uint8List lenStr; 138 | LiveDanmakuItem? danmaku; 139 | while (decodedMsgLen < totalLength) { 140 | try { 141 | lenStr = list.sublist(decodedMsgLen, decodedMsgLen + 4); 142 | len = lenStr.buffer.asByteData().getInt32(0, Endian.little) + 4; 143 | singleMsgBuffer = list.sublist(decodedMsgLen, decodedMsgLen + len); 144 | decodedMsgLen += len; 145 | String byteDatas = utf8 146 | .decode(singleMsgBuffer.sublist(12, singleMsgBuffer.length - 2)); 147 | //type@=chatmsg/rid@=4549169/uid@=115902484/nn@=消息内容/txt@=坑/cid@=486d1c603c494315b011110000000000/ic@=avatar_v3@S202208@S788d2957c66f46529a6ec0b8520c3489/level@=33/sahf@=0/col@=5/rg@=4/cst@=1662646767542/bnn@=橙記/bl@=22/brid@=4549169/hc@=eaccdb9a398c4648d7821dca31d4fb97/diaf@=1/hl@=1/ifs@=1/el@=/lk@=/fl@=22/hb@=1232@S/dms@=5/pdg@=29/pdk@=88/ail@=1446@S/ext@=/ 148 | 149 | if (byteDatas.startsWith("type@=chatmsg")) { 150 | final msgMap = parseMsg(byteDatas); 151 | //MyLogger.info(msgMap.toString()); 152 | final nickname = msgMap['nn'] ?? ''; 153 | final uid = msgMap['uid'] ?? ''; 154 | final content = msgMap['txt'] ?? ''; 155 | final Color color = setDanmakuColor(msgMap['col'] ?? ''); 156 | final String ic = msgMap['ic'] ?? ''; 157 | final Map ext = { 158 | 'avatar': "https://apic.douyucdn.cn/upload/${ic}_big.jpg" 159 | }; 160 | MyLogger.info( 161 | '斗鱼弹幕-->$uid $nickname: $content $color ${msgMap['col']}'); 162 | // print(msgMap); 163 | danmaku = LiveDanmakuItem( 164 | name: nickname, msg: content, uid: uid, ext: ext, color: color); 165 | } 166 | } catch (e) { 167 | MyLogger.error('斗鱼弹幕解析异常: $e'); 168 | decodedMsgLen = totalLength; //ignore this message data 169 | } 170 | } 171 | return danmaku; 172 | } catch (e) { 173 | MyLogger.error('斗鱼弹幕解析ERROR: $e'); 174 | } 175 | } 176 | 177 | //销毁连接 178 | void dispose() { 179 | timer?.cancel(); 180 | _channel?.sink.close(); 181 | _channel = null; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: build 4 | 5 | # Controls when the workflow will run 6 | on: 7 | push: 8 | branches: 9 | - master 10 | tags: 11 | - "v*" 12 | paths-ignore: 13 | - '.editorconfig' 14 | - '.gitignore' 15 | - '*.md' 16 | 17 | # Allows you to run this workflow manually from the Actions tab 18 | workflow_dispatch: 19 | 20 | env: 21 | # APP name 22 | APP_NAME: vvibe 23 | FVP_DEPS_LATEST: 1 24 | FVP_DEPS_URL: https://github.com/wang-bin/mdk-sdk/releases/latest/download 25 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 26 | jobs: 27 | 28 | Windows: 29 | # The type of runner that the job will run on 30 | runs-on: windows-latest 31 | 32 | # Steps represent a sequence of tasks that will be executed as part of the job 33 | steps: 34 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 35 | - uses: actions/checkout@v4 36 | 37 | - uses: subosito/flutter-action@v2 38 | with: 39 | flutter-version: 3.29.2 40 | 41 | - name: Install Dependences 42 | run: | 43 | flutter doctor --verbose 44 | flutter config --enable-windows-desktop 45 | flutter pub get 46 | # nightly will crash 47 | #curl https://master.dl.sourceforge.net/project/mdk-sdk/mdk-sdk-windows-desktop-vs2022-x64.7z?viasf=1 -o mdk-sdk-windows-desktop-vs2022-x64.7z 48 | #mv mdk-sdk-windows-desktop-vs2022-x64.7z windows/flutter/ephemeral/.plugin_symlinks/fvp/windows 49 | 50 | - name: Build Windows 51 | run: | 52 | flutter build windows --verbose --release 53 | cp -r build/windows/x64/runner/Release . 54 | # 读取当前版本号用于文件名 55 | CURRENT_VERSION=$(grep 'version:' pubspec.yaml | awk '{print $2}' | sed 's/\+.*//') 56 | 7z a vvibe-${CURRENT_VERSION}-windows-x64.zip Release 57 | 58 | # 构建步骤中不再创建draft release,统一由UpdateVersionAndRelease任务处理 59 | 60 | - name: Upload Windows artifact 61 | uses: actions/upload-artifact@v4 62 | with: 63 | name: vvibe-windows-x64 64 | path: vvibe-*-windows-x64.zip 65 | Linux: 66 | runs-on: ubuntu-20.04 67 | 68 | steps: 69 | - uses: actions/checkout@v4 70 | 71 | - uses: subosito/flutter-action@v2 72 | with: 73 | flutter-version: 3.27.2 74 | 75 | - name: Install Dependences 76 | run: | 77 | sudo apt-get update -y 78 | sudo apt-get install -y unzip xz-utils zip libglu1-mesa cmake clang ninja-build pkg-config libgtk-3-dev libpulse-dev liblzma-dev libstdc++-10-dev libasound2-dev 79 | 80 | - name: Build Linux 81 | run: | 82 | flutter config --enable-linux-desktop 83 | flutter doctor --verbose 84 | flutter pub get 85 | # nightly will crash 86 | #curl https://master.dl.sourceforge.net/project/mdk-sdk/mdk-sdk-linux-x64.tar.xz?viasf=1 -o mdk-sdk-linux-x64.tar.xz 87 | #mv mdk-sdk-linux-x64.tar.xz linux/flutter/ephemeral/.plugin_symlinks/fvp/linux 88 | flutter build linux --verbose --release 89 | cp -r build/linux/x64/release/bundle . 90 | # 读取当前版本号用于文件名 91 | CURRENT_VERSION=$(grep 'version:' pubspec.yaml | awk '{print $2}' | sed 's/\+.*//') 92 | tar Jcvf vvibe-${CURRENT_VERSION}-linux-x64.tar.xz bundle 93 | 94 | # 构建步骤中不再创建draft release,统一由UpdateVersionAndRelease任务处理 95 | 96 | - name: Upload Linux artifact 97 | uses: actions/upload-artifact@v4 98 | with: 99 | name: vvibe-linux-x64 100 | path: vvibe-*-linux-x64.tar.xz 101 | 102 | # 更新版本并创建release的任务 103 | UpdateVersionAndRelease: 104 | needs: [Windows, Linux] 105 | runs-on: ubuntu-latest 106 | # 只在push到master分支时运行,不在tag触发时运行 107 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 108 | steps: 109 | - uses: actions/checkout@v4 110 | with: 111 | fetch-depth: 0 112 | token: ${{ secrets.GH_TOKEN }} 113 | 114 | - uses: subosito/flutter-action@v2 115 | with: 116 | flutter-version: 3.29.2 117 | 118 | - name: Check commit message format 119 | id: check_commit 120 | run: | 121 | # 检查commit message是否包含年月日格式(如2025.1010) 122 | if [[ "${{ github.event.head_commit.message }}" =~ [0-9]{4}\.[0-9]{4} ]]; then 123 | echo "format_match=true" >> $GITHUB_OUTPUT 124 | else 125 | echo "format_match=false" >> $GITHUB_OUTPUT 126 | echo "Commit message does not contain date format (YYYY.MMDD), skipping version update." 127 | fi 128 | 129 | - name: Update pubspec.yml version 130 | if: steps.check_commit.outputs.format_match == 'true' 131 | run: | 132 | # 读取当前版本 133 | CURRENT_VERSION=$(grep 'version:' pubspec.yaml | awk '{print $2}' | sed 's/\+.*//') 134 | 135 | # 解析版本号 136 | MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1) 137 | MINOR=$(echo $CURRENT_VERSION | cut -d. -f2) 138 | PATCH=$(echo $CURRENT_VERSION | cut -d. -f3) 139 | 140 | # 增加patch版本 141 | NEW_PATCH=$((PATCH + 1)) 142 | NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH" 143 | 144 | # 更新pubspec.yml 145 | sed -i "s/version: $CURRENT_VERSION/version: $NEW_VERSION/g" pubspec.yaml 146 | 147 | # 提交更改 148 | git config --global user.name 'GitHub Actions Bot' 149 | git config --global user.email 'actions@github.com' 150 | git add pubspec.yaml 151 | git commit -m "Bump version to $NEW_VERSION" 152 | git push origin master 153 | 154 | # 输出新版本用于后续步骤 155 | echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV 156 | 157 | - name: Download Windows artifact 158 | if: steps.check_commit.outputs.format_match == 'true' 159 | uses: actions/download-artifact@v4 160 | with: 161 | name: vvibe-windows-x64 162 | path: . 163 | 164 | - name: Download Linux artifact 165 | if: steps.check_commit.outputs.format_match == 'true' 166 | uses: actions/download-artifact@v4 167 | with: 168 | name: vvibe-linux-x64 169 | path: . 170 | 171 | - name: Create Release 172 | if: steps.check_commit.outputs.format_match == 'true' 173 | uses: softprops/action-gh-release@v2 174 | with: 175 | tag_name: v${{ env.NEW_VERSION }} 176 | name: Release v${{ env.NEW_VERSION }} 177 | draft: false 178 | prerelease: false 179 | token: ${{ secrets.GH_TOKEN }} 180 | files: | 181 | vvibe-*-windows-x64.zip 182 | vvibe-*-linux-x64.tar.xz 183 | 184 | --------------------------------------------------------------------------------