From 1a48e586da63de44ccb3a253cdc86c69edc47518 Mon Sep 17 00:00:00 2001 From: taskmemz <1450594750@qq.com> Date: Sun, 24 May 2026 10:22:09 +0000 Subject: [PATCH 1/9] Apply my modifications to Selene --- lib/models/search_result.dart | 12 +- lib/screens/anime_screen.dart | 1 - lib/screens/live_player_screen.dart | 12 +- lib/screens/login_screen.dart | 249 +++++++++++++++++-- lib/screens/player_screen.dart | 28 ++- lib/screens/search_screen.dart | 2 +- lib/services/api_service.dart | 65 ++++- lib/services/douban_service.dart | 4 +- lib/services/local_search_cache_service.dart | 2 +- lib/services/page_cache_service.dart | 2 +- lib/services/sse_search_service.dart | 2 +- lib/services/user_data_service.dart | 45 ++++ lib/services/version_service.dart | 2 +- lib/widgets/continue_watching_section.dart | 1 - lib/widgets/dlna_player_controls.dart | 8 +- lib/widgets/favorites_grid.dart | 4 +- lib/widgets/mobile_player_controls.dart | 3 +- lib/widgets/player_details_panel.dart | 4 +- lib/widgets/search_result_agg_grid.dart | 4 +- lib/widgets/user_menu.dart | 8 +- lib/widgets/video_menu_bottom_sheet.dart | 12 +- lib/widgets/video_player_widget.dart | 8 +- lib/widgets/windows_title_bar.dart | 2 +- 23 files changed, 395 insertions(+), 85 deletions(-) diff --git a/lib/models/search_result.dart b/lib/models/search_result.dart index aeeebf23..9a820a47 100644 --- a/lib/models/search_result.dart +++ b/lib/models/search_result.dart @@ -150,10 +150,9 @@ class SearchStartEvent extends SearchEvent { SearchStartEvent({ required this.query, required this.totalSources, - required int timestamp, + required super.timestamp, }) : super( type: SearchEventType.start, - timestamp: timestamp, ); factory SearchStartEvent.fromJson(Map json) { @@ -175,10 +174,9 @@ class SearchSourceResultEvent extends SearchEvent { required this.source, required this.sourceName, required this.results, - required int timestamp, + required super.timestamp, }) : super( type: SearchEventType.sourceResult, - timestamp: timestamp, ); factory SearchSourceResultEvent.fromJson(Map json) { @@ -206,10 +204,9 @@ class SearchSourceErrorEvent extends SearchEvent { required this.source, required this.sourceName, required this.error, - required int timestamp, + required super.timestamp, }) : super( type: SearchEventType.sourceError, - timestamp: timestamp, ); factory SearchSourceErrorEvent.fromJson(Map json) { @@ -230,10 +227,9 @@ class SearchCompleteEvent extends SearchEvent { SearchCompleteEvent({ required this.totalResults, required this.completedSources, - required int timestamp, + required super.timestamp, }) : super( type: SearchEventType.complete, - timestamp: timestamp, ); factory SearchCompleteEvent.fromJson(Map json) { diff --git a/lib/screens/anime_screen.dart b/lib/screens/anime_screen.dart index 132adac5..9c8d25f5 100644 --- a/lib/screens/anime_screen.dart +++ b/lib/screens/anime_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; import '../services/theme_service.dart'; import '../widgets/capsule_tab_switcher.dart'; diff --git a/lib/screens/live_player_screen.dart b/lib/screens/live_player_screen.dart index 87ffb110..647b9f9e 100644 --- a/lib/screens/live_player_screen.dart +++ b/lib/screens/live_player_screen.dart @@ -433,10 +433,12 @@ class _LivePlayerScreenState extends State children: [ Column( children: [ - // Windows 自定义标题栏 + // Windows 自定义标题栏(跟随主题) if (Platform.isWindows) - const WindowsTitleBar( - customBackgroundColor: Color(0xFF000000), + WindowsTitleBar( + customBackgroundColor: isDarkMode + ? const Color(0xFF121212) + : const Color(0xFFf5f5f5), ), // 主要内容 Expanded( @@ -1798,8 +1800,8 @@ class _LivePlayerScreenState extends State Container( width: 4, height: 4, - decoration: BoxDecoration( - color: const Color(0xFF27ae60), + decoration: const BoxDecoration( + color: Color(0xFF27ae60), shape: BoxShape.circle, ), ), diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index c2fbe4cc..9b437793 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -28,6 +28,11 @@ class _LoginScreenState extends State { bool _isLoading = false; bool _isFormValid = false; bool _isLocalMode = false; + bool _showAdvancedSettings = false; + bool _enableBrowserHeaders = false; + final _userAgentController = TextEditingController(); + final _customHeaderNameController = TextEditingController(); + final _customHeaderValueController = TextEditingController(); // 点击计数器相关 int _logoTapCount = 0; @@ -41,6 +46,23 @@ class _LoginScreenState extends State { _passwordController.addListener(_validateForm); _subscriptionUrlController.addListener(_validateForm); _loadSavedUserData(); + _loadAdvancedSettings(); + } + + void _loadAdvancedSettings() async { + final ua = await UserDataService.getCustomUserAgent(); + final enabled = await UserDataService.getEnableBrowserHeaders(); + final header = await UserDataService.getCustomHeader(); + if (mounted) { + setState(() { + _userAgentController.text = ua; + _enableBrowserHeaders = enabled; + if (header.isNotEmpty) { + _customHeaderNameController.text = header.keys.first; + _customHeaderValueController.text = header.values.first; + } + }); + } } void _loadSavedUserData() async { @@ -82,6 +104,9 @@ class _LoginScreenState extends State { _usernameController.dispose(); _passwordController.dispose(); _subscriptionUrlController.dispose(); + _userAgentController.dispose(); + _customHeaderNameController.dispose(); + _customHeaderValueController.dispose(); _tapTimer?.cancel(); super.dispose(); } @@ -257,16 +282,23 @@ class _LoginScreenState extends State { } String _parseCookies(http.Response response) { - // 解析 Set-Cookie 头部 - List cookies = []; - - // 获取所有 Set-Cookie 头部 - final setCookieHeaders = response.headers['set-cookie']; - if (setCookieHeaders != null) { - // HTTP 头部通常是 String 类型 - final cookieParts = setCookieHeaders.split(';'); - if (cookieParts.isNotEmpty) { - cookies.add(cookieParts[0].trim()); + final allCookies = response.headers['set-cookie']; + if (allCookies == null || allCookies.isEmpty) return ''; + + final lines = allCookies + .split('\n') + .expand((line) => line.split(',')) + .where((s) => s.trim().isNotEmpty) + .toList(); + + final cookies = []; + for (final line in lines) { + final semicolonIdx = line.indexOf(';'); + final nameValue = semicolonIdx > 0 + ? line.substring(0, semicolonIdx).trim() + : line.trim(); + if (nameValue.contains('=')) { + cookies.add(nameValue); } } @@ -301,16 +333,37 @@ class _LoginScreenState extends State { }); try { + // 保存高级设置 + await UserDataService.saveCustomUserAgent(_userAgentController.text); + await UserDataService.saveEnableBrowserHeaders(_enableBrowserHeaders); + await UserDataService.saveCustomHeader( + _customHeaderNameController.text, + _customHeaderValueController.text); + // 处理 URL String baseUrl = _processUrl(_urlController.text); String loginUrl = '$baseUrl/api/login'; + final loginHeaders = { + 'Content-Type': 'application/json', + }; + loginHeaders['User-Agent'] = _userAgentController.text; + if (_enableBrowserHeaders) { + loginHeaders['Accept-Language'] = 'zh-CN,zh;q=0.9,en;q=0.8'; + loginHeaders['Sec-Fetch-Site'] = 'same-origin'; + loginHeaders['Sec-Fetch-Mode'] = 'cors'; + loginHeaders['Sec-Fetch-Dest'] = 'empty'; + } + if (_customHeaderNameController.text.isNotEmpty && + _customHeaderValueController.text.isNotEmpty) { + loginHeaders[_customHeaderNameController.text.trim()] = + _customHeaderValueController.text.trim(); + } + // 发送登录请求 final response = await http.post( Uri.parse(loginUrl), - headers: { - 'Content-Type': 'application/json', - }, + headers: loginHeaders, body: json.encode({ 'username': _usernameController.text, 'password': _passwordController.text, @@ -502,6 +555,168 @@ class _LoginScreenState extends State { } } + Widget _buildAdvancedSettingsToggle() { + return Padding( + padding: const EdgeInsets.only(top: 12), + child: GestureDetector( + onTap: () { + setState(() { + _showAdvancedSettings = !_showAdvancedSettings; + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _showAdvancedSettings + ? Icons.expand_less + : Icons.expand_more, + color: const Color(0xFF7f8c8d), + size: 18, + ), + const SizedBox(width: 4), + Text( + '高级设置', + style: FontUtils.poppins( + fontSize: 13, + color: const Color(0xFF7f8c8d), + ), + ), + ], + ), + ), + ); + } + + Widget _buildAdvancedSettingsPanel() { + return Padding( + padding: const EdgeInsets.only(top: 12), + child: Container( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'User-Agent', + style: FontUtils.poppins( + fontSize: 12, + fontWeight: FontWeight.w600, + color: const Color(0xFF2c3e50), + ), + ), + const Spacer(), + SizedBox( + height: 20, + child: Switch.adaptive( + value: _enableBrowserHeaders, + onChanged: (v) { + setState(() { + _enableBrowserHeaders = v; + }); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + Text( + '浏览器头', + style: FontUtils.poppins( + fontSize: 12, + color: const Color(0xFF7f8c8d), + ), + ), + ], + ), + const SizedBox(height: 8), + TextFormField( + controller: _userAgentController, + maxLines: 2, + minLines: 1, + style: FontUtils.poppins( + fontSize: 12, + color: const Color(0xFF2c3e50), + ), + decoration: InputDecoration( + hintText: '自定义 User-Agent', + hintStyle: FontUtils.poppins( + fontSize: 12, + color: const Color(0xFFbdc3c7), + ), + filled: true, + fillColor: Colors.white.withValues(alpha: 0.5), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 10), + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: TextFormField( + controller: _customHeaderNameController, + style: FontUtils.poppins( + fontSize: 12, + color: const Color(0xFF2c3e50), + ), + decoration: InputDecoration( + hintText: '自定义头名称', + hintStyle: FontUtils.poppins( + fontSize: 12, + color: const Color(0xFFbdc3c7), + ), + filled: true, + fillColor: Colors.white.withValues(alpha: 0.5), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 10), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: TextFormField( + controller: _customHeaderValueController, + style: FontUtils.poppins( + fontSize: 12, + color: const Color(0xFF2c3e50), + ), + decoration: InputDecoration( + hintText: '自定义头值', + hintStyle: FontUtils.poppins( + fontSize: 12, + color: const Color(0xFFbdc3c7), + ), + filled: true, + fillColor: Colors.white.withValues(alpha: 0.5), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 10), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { final isTablet = DeviceUtils.isTablet(context); @@ -775,7 +990,7 @@ class _LoginScreenState extends State { ? Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - SizedBox( + const SizedBox( height: 18, width: 18, child: CircularProgressIndicator( @@ -805,6 +1020,9 @@ class _LoginScreenState extends State { ), ), ), + // 高级设置折叠面板 + _buildAdvancedSettingsToggle(), + if (_showAdvancedSettings) _buildAdvancedSettingsPanel(), ], ), ), @@ -1072,6 +1290,9 @@ class _LoginScreenState extends State { ), ), ), + // 高级设置折叠面板 + _buildAdvancedSettingsToggle(), + if (_showAdvancedSettings) _buildAdvancedSettingsPanel(), ], ), ), diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index c0e45455..15f4fd05 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -545,7 +545,7 @@ class _PlayerScreenState extends State // 异步保存播放记录(不等待结果) PageCacheService().savePlayRecord(playRecord, context).then((_) { debugPrint( - '保存播放进度 [场景: $scene]: source: $currentSourceSnapshot, id: $currentIDSnapshot, 第${currentEpisodeIndexSnapshot + 1}集, 时间: ${playTime}秒'); + '保存播放进度 [场景: $scene]: source: $currentSourceSnapshot, id: $currentIDSnapshot, 第${currentEpisodeIndexSnapshot + 1}集, 时间: $playTime秒'); }).catchError((e) { debugPrint('保存播放进度失败 [场景: $scene]: $e'); }); @@ -1165,7 +1165,7 @@ class _PlayerScreenState extends State // 等待下一帧,确保 MobileVideoPlayerWidget 已经重新创建 WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && currentDetail != null) { - debugPrint('恢复播放: 第${resumeEpisodeIndex + 1}集, ${resumeSeconds}秒'); + debugPrint('恢复播放: 第${resumeEpisodeIndex + 1}集, $resumeSeconds秒'); // 调用 startPlay 重新初始化播放器 startPlay(resumeEpisodeIndex, resumeSeconds); } @@ -1435,12 +1435,12 @@ class _PlayerScreenState extends State return LayoutBuilder( builder: (context, constraints) { final double screenWidth = constraints.maxWidth; - final double padding = 16.0; - final double spacing = 12.0; + const double padding = 16.0; + const double spacing = 12.0; final crossAxisCount = _isTablet ? 6 : 3; final double availableWidth = screenWidth - (padding * 2) - (spacing * (crossAxisCount - 1)); - final double minItemWidth = 80.0; + const double minItemWidth = 80.0; final double calculatedItemWidth = availableWidth / crossAxisCount; final double itemWidth = math.max(calculatedItemWidth, minItemWidth); final double itemHeight = itemWidth * 2.0; @@ -1639,7 +1639,7 @@ class _PlayerScreenState extends State builder: (context, constraints) { // 计算按钮宽度:根据设备类型调整 final screenWidth = constraints.maxWidth; - final horizontalPadding = 32.0; // 左右各16 + const horizontalPadding = 32.0; // 左右各16 final availableWidth = screenWidth - horizontalPadding; final cardsPerView = _isTablet ? 6.2 : 3.2; final buttonWidth = (availableWidth / cardsPerView) - 6; // 减去右边距6 @@ -1800,7 +1800,7 @@ class _PlayerScreenState extends State builder: (context) { return StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Container( + return SizedBox( height: panelHeight, width: double.infinity, child: PlayerEpisodesPanel( @@ -1901,7 +1901,7 @@ class _PlayerScreenState extends State builder: (context) { return StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Container( + return SizedBox( height: panelHeight, width: double.infinity, child: PlayerDetailsPanel( @@ -2038,7 +2038,7 @@ class _PlayerScreenState extends State builder: (context, constraints) { // 计算卡片宽度:根据设备类型调整 final screenWidth = constraints.maxWidth; - final horizontalPadding = 32.0; // 左右各16 + const horizontalPadding = 32.0; // 左右各16 final availableWidth = screenWidth - horizontalPadding; final cardsPerView = _isTablet ? 6.2 : 3.2; final cardWidth = (availableWidth / cardsPerView) - 6; // 减去右边距6 @@ -2168,7 +2168,7 @@ class _PlayerScreenState extends State builder: (context) { return StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Container( + return SizedBox( height: panelHeight, width: double.infinity, child: PlayerSourcesPanel( @@ -2619,10 +2619,12 @@ class _PlayerScreenState extends State ), child: Column( children: [ - // Windows 自定义标题栏(播放页使用纯黑背景) + // Windows 自定义标题栏(播放页跟随主题) if (Platform.isWindows) - const WindowsTitleBar( - customBackgroundColor: Color(0xFF000000), + WindowsTitleBar( + customBackgroundColor: isDarkMode + ? const Color(0xFF121212) + : const Color(0xFFf5f5f5), ), // 主要内容 Expanded( diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 1fffbebb..aa45f50b 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -38,7 +38,7 @@ class _SearchScreenState extends State final ScrollController _scrollController = ScrollController(); String _searchQuery = ''; List _searchHistory = []; - List _searchResults = []; + final List _searchResults = []; bool _hasSearched = false; bool _hasReceivedStart = false; // 是否已收到start消息 String? _searchError; diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index 5b0eddc7..02c16339 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -73,6 +73,30 @@ class ApiService { return '$cleanBaseUrl$cleanEndpoint'; } + /// 添加浏览器特征头 + static Future _addBrowserHeaders(Map headers) async { + final ua = await UserDataService.getCustomUserAgent(); + headers['User-Agent'] = ua; + + final enabled = await UserDataService.getEnableBrowserHeaders(); + if (enabled) { + headers['Accept-Language'] = 'zh-CN,zh;q=0.9,en;q=0.8'; + headers['Accept-Encoding'] = 'gzip, deflate, br'; + headers['Sec-Fetch-Site'] = 'same-origin'; + headers['Sec-Fetch-Mode'] = 'cors'; + headers['Sec-Fetch-Dest'] = 'empty'; + headers['Sec-Ch-Ua'] = + '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"'; + headers['Sec-Ch-Ua-Mobile'] = '?0'; + headers['Sec-Ch-Ua-Platform'] = '"Windows"'; + } + + final customHeader = await UserDataService.getCustomHeader(); + if (customHeader.isNotEmpty) { + headers.addAll(customHeader); + } + } + /// 构建请求头 static Future> _buildHeaders({ Map? additionalHeaders, @@ -83,6 +107,9 @@ class ApiService { 'Accept': 'application/json', }; + // 添加浏览器特征头 + await _addBrowserHeaders(headers); + // 添加认证cookies if (includeAuth) { final cookies = await _getCookies(); @@ -574,13 +601,16 @@ class ApiService { } String loginUrl = '$baseUrl/api/login'; + final loginHeaders = { + 'Content-Type': 'application/json', + }; + await _addBrowserHeaders(loginHeaders); + // 发送登录请求 final response = await http .post( Uri.parse(loginUrl), - headers: { - 'Content-Type': 'application/json', - }, + headers: loginHeaders, body: json.encode({ 'username': username, 'password': password, @@ -802,17 +832,26 @@ class ApiService { } } - /// 解析 Set-Cookie 头部 + /// 解析 Set-Cookie 头部,提取所有 Cookie 的 name=value static String _parseCookies(http.Response response) { - List cookies = []; - - // 获取所有 Set-Cookie 头部 - final setCookieHeaders = response.headers['set-cookie']; - if (setCookieHeaders != null) { - // HTTP 头部通常是 String 类型 - final cookieParts = setCookieHeaders.split(';'); - if (cookieParts.isNotEmpty) { - cookies.add(cookieParts[0].trim()); + final allCookies = response.headers['set-cookie']; + if (allCookies == null || allCookies.isEmpty) return ''; + + // 按 \n(Dart http 包多值拼接方式)或 ","(HTTP 标准)切分多个 cookie + final lines = allCookies + .split('\n') + .expand((line) => line.split(',')) + .where((s) => s.trim().isNotEmpty) + .toList(); + + final cookies = []; + for (final line in lines) { + final semicolonIdx = line.indexOf(';'); + final nameValue = semicolonIdx > 0 + ? line.substring(0, semicolonIdx).trim() + : line.trim(); + if (nameValue.contains('=')) { + cookies.add(nameValue); } } diff --git a/lib/services/douban_service.dart b/lib/services/douban_service.dart index a69b889d..22597550 100644 --- a/lib/services/douban_service.dart +++ b/lib/services/douban_service.dart @@ -203,9 +203,9 @@ class DoubanService { // 为了保持与现有代码的兼容性,将时长转换为字符串 String? duration; if (episodeLength != null) { - duration = '${episodeLength}分钟'; + duration = '$episodeLength分钟'; } else if (movieDuration != null) { - duration = '${movieDuration}分钟'; + duration = '$movieDuration分钟'; } // 提取剧情简介 - 两个正则都匹配,选择内容更长的 diff --git a/lib/services/local_search_cache_service.dart b/lib/services/local_search_cache_service.dart index 66f0af1e..e7b86d3b 100644 --- a/lib/services/local_search_cache_service.dart +++ b/lib/services/local_search_cache_service.dart @@ -141,7 +141,7 @@ class LocalSearchCacheService { if (_cleanupTimer != null) return; // 避免重复启动 _cleanupTimer = Timer.periodic( - Duration(milliseconds: _cacheCleanupIntervalMs), + const Duration(milliseconds: _cacheCleanupIntervalMs), (_) { _performCacheCleanup(); }, diff --git a/lib/services/page_cache_service.dart b/lib/services/page_cache_service.dart index 1a673edc..8fd7f75b 100644 --- a/lib/services/page_cache_service.dart +++ b/lib/services/page_cache_service.dart @@ -554,7 +554,7 @@ class PageCacheService final existingItem = cachedData[existingIndex]; final updatedHistory = [ existingItem, - ...cachedData.where((item) => item != query).toList() + ...cachedData.where((item) => item != query) ]; setCache(cacheKey, updatedHistory); } diff --git a/lib/services/sse_search_service.dart b/lib/services/sse_search_service.dart index 8dac40b3..c2a4931a 100644 --- a/lib/services/sse_search_service.dart +++ b/lib/services/sse_search_service.dart @@ -270,7 +270,7 @@ class SSESearchService { _buffer = ''; // 使用流式 UTF-8 解码器,自动处理跨 chunk 的多字节字符 - final utf8Decoder = const Utf8Decoder(allowMalformed: false); + const utf8Decoder = Utf8Decoder(allowMalformed: false); // 流式处理 SSE 数据 await for (final chunk in response.stream.transform(utf8Decoder)) { diff --git a/lib/services/user_data_service.dart b/lib/services/user_data_service.dart index 7e89e160..b93695e4 100644 --- a/lib/services/user_data_service.dart +++ b/lib/services/user_data_service.dart @@ -11,6 +11,10 @@ class UserDataService { static const String _preferSpeedTestKey = 'prefer_speed_test'; static const String _localSearchKey = 'local_search'; static const String _isLocalModeKey = 'is_local_mode'; + static const String _customUserAgentKey = 'custom_user_agent'; + static const String _enableBrowserHeadersKey = 'enable_browser_headers'; + static const String _customHeaderNameKey = 'custom_header_name'; + static const String _customHeaderValueKey = 'custom_header_value'; // 内存缓存 static bool? _isLocalModeCache; @@ -257,4 +261,45 @@ class UserDataService { static bool getIsLocalModeSync() { return _isLocalModeCache ?? false; } + + static const String _defaultUserAgent = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/124.0.0.0 Safari/537.36'; + + static Future saveCustomUserAgent(String ua) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_customUserAgentKey, ua); + } + + static Future getCustomUserAgent() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_customUserAgentKey) ?? _defaultUserAgent; + } + + static Future saveEnableBrowserHeaders(bool enabled) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_enableBrowserHeadersKey, enabled); + } + + static Future getEnableBrowserHeaders() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_enableBrowserHeadersKey) ?? false; + } + + static Future saveCustomHeader(String name, String value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_customHeaderNameKey, name); + await prefs.setString(_customHeaderValueKey, value); + } + + static Future> getCustomHeader() async { + final prefs = await SharedPreferences.getInstance(); + final name = prefs.getString(_customHeaderNameKey) ?? ''; + final value = prefs.getString(_customHeaderValueKey) ?? ''; + if (name.isNotEmpty && value.isNotEmpty) { + return {name: value}; + } + return {}; + } } diff --git a/lib/services/version_service.dart b/lib/services/version_service.dart index a9415bcc..63f79835 100644 --- a/lib/services/version_service.dart +++ b/lib/services/version_service.dart @@ -81,7 +81,7 @@ class VersionService { // 检查上次检查时间(每天最多提示一次) final lastCheck = prefs.getInt(_lastCheckKey) ?? 0; final now = DateTime.now().millisecondsSinceEpoch; - final dayInMs = 24 * 60 * 60 * 1000; + const dayInMs = 24 * 60 * 60 * 1000; if (now - lastCheck < dayInMs) { return false; diff --git a/lib/widgets/continue_watching_section.dart b/lib/widgets/continue_watching_section.dart index a9f1c7fc..d89f082d 100644 --- a/lib/widgets/continue_watching_section.dart +++ b/lib/widgets/continue_watching_section.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/play_record.dart'; import '../models/video_info.dart'; -import '../services/api_service.dart'; import '../services/page_cache_service.dart'; import '../services/theme_service.dart'; import '../utils/device_utils.dart'; diff --git a/lib/widgets/dlna_player_controls.dart b/lib/widgets/dlna_player_controls.dart index ffc1c35e..b15a182a 100644 --- a/lib/widgets/dlna_player_controls.dart +++ b/lib/widgets/dlna_player_controls.dart @@ -304,16 +304,16 @@ class _DLNAPlayerControlsState extends State { ), ), // 中央加载指示器 - Center( + const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - const CircularProgressIndicator( + CircularProgressIndicator( color: Colors.white, strokeWidth: 3, ), - const SizedBox(height: 16), - const Text( + SizedBox(height: 16), + Text( '视频加载中...', style: TextStyle( color: Colors.white, diff --git a/lib/widgets/favorites_grid.dart b/lib/widgets/favorites_grid.dart index 419326a6..0c31772c 100644 --- a/lib/widgets/favorites_grid.dart +++ b/lib/widgets/favorites_grid.dart @@ -349,10 +349,10 @@ class _FavoritesGridState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( + const Icon( Icons.error_outline, size: 80, - color: const Color(0xFFbdc3c7), + color: Color(0xFFbdc3c7), ), const SizedBox(height: 24), Text( diff --git a/lib/widgets/mobile_player_controls.dart b/lib/widgets/mobile_player_controls.dart index a5bd2fb6..eee0ac8b 100644 --- a/lib/widgets/mobile_player_controls.dart +++ b/lib/widgets/mobile_player_controls.dart @@ -244,8 +244,9 @@ class _MobilePlayerControlsState extends State { } void _onSwipeUpdate(DragUpdateDetails details) { - if (_isLocked || !_isSeekingViaSwipe || widget.live || _screenSize == null) + if (_isLocked || !_isSeekingViaSwipe || widget.live || _screenSize == null) { return; + } final screenWidth = _screenSize!.width; final swipeDistance = details.globalPosition.dx - _swipeStartX; final swipeRatio = swipeDistance / (screenWidth * 0.5); diff --git a/lib/widgets/player_details_panel.dart b/lib/widgets/player_details_panel.dart index f0408d39..261cc364 100644 --- a/lib/widgets/player_details_panel.dart +++ b/lib/widgets/player_details_panel.dart @@ -168,7 +168,7 @@ class PlayerDetailsPanel extends StatelessWidget { ], if (totalEpisodes != null && totalEpisodes > 1) ...[ Text( - '全${totalEpisodes}集', + '全$totalEpisodes集', style: theme.textTheme.bodyMedium?.copyWith( color: isDarkMode ? Colors.grey[400] @@ -364,7 +364,7 @@ class PlayerDetailsPanel extends StatelessWidget { const SizedBox(height: 4), if (totalEpisodes > 1) Text( - '全${totalEpisodes}集', + '全$totalEpisodes集', style: theme.textTheme.bodyMedium?.copyWith( color: isDarkMode ? Colors.grey[400] diff --git a/lib/widgets/search_result_agg_grid.dart b/lib/widgets/search_result_agg_grid.dart index 5316176e..306d4828 100644 --- a/lib/widgets/search_result_agg_grid.dart +++ b/lib/widgets/search_result_agg_grid.dart @@ -158,10 +158,10 @@ class _SearchResultAggGridState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( + const Icon( Icons.search_off, size: 80, - color: const Color(0xFFbdc3c7), + color: Color(0xFFbdc3c7), ), const SizedBox(height: 24), Text( diff --git a/lib/widgets/user_menu.dart b/lib/widgets/user_menu.dart index f2f6cb03..f8e3d582 100644 --- a/lib/widgets/user_menu.dart +++ b/lib/widgets/user_menu.dart @@ -850,10 +850,10 @@ class _UserMenuState extends State { ), child: Row( children: [ - Icon( + const Icon( LucideIcons.trash2, size: 20, - color: const Color(0xFFf59e0b), + color: Color(0xFFf59e0b), ), const SizedBox(width: 12), Text( @@ -890,10 +890,10 @@ class _UserMenuState extends State { ), child: Row( children: [ - Icon( + const Icon( LucideIcons.download, size: 20, - color: const Color(0xFF3b82f6), + color: Color(0xFF3b82f6), ), const SizedBox(width: 12), Text( diff --git a/lib/widgets/video_menu_bottom_sheet.dart b/lib/widgets/video_menu_bottom_sheet.dart index df956867..eaeb317d 100644 --- a/lib/widgets/video_menu_bottom_sheet.dart +++ b/lib/widgets/video_menu_bottom_sheet.dart @@ -68,7 +68,7 @@ class CollapsibleScrollPhysics extends ScrollPhysics { @override ScrollPhysics buildParent(ScrollPhysics? ancestor) { // 根据平台选择合适的父物理效果 - final parentPhysics = isIOS ? BouncingScrollPhysics() : ClampingScrollPhysics(); + final parentPhysics = isIOS ? const BouncingScrollPhysics() : const ClampingScrollPhysics(); return parent?.applyTo(ancestor ?? parentPhysics) ?? parentPhysics; } } @@ -803,7 +803,7 @@ class _VideoMenuBottomSheetState extends State // 关闭按钮 GestureDetector( onTap: widget.onClose, - child: Container( + child: SizedBox( width: 32, height: 32, child: Icon( @@ -938,10 +938,10 @@ class _VideoMenuBottomSheetState extends State child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( + const Icon( Icons.star, size: 16, - color: const Color(0xFFFFB800), + color: Color(0xFFFFB800), ), const SizedBox(width: 4), Text( @@ -1102,10 +1102,10 @@ class _VideoMenuBottomSheetState extends State child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( + const Icon( Icons.star, size: 16, - color: const Color(0xFFE91E63), + color: Color(0xFFE91E63), ), const SizedBox(width: 4), Text( diff --git a/lib/widgets/video_player_widget.dart b/lib/widgets/video_player_widget.dart index 29e38375..fea486c1 100644 --- a/lib/widgets/video_player_widget.dart +++ b/lib/widgets/video_player_widget.dart @@ -168,7 +168,13 @@ class _VideoPlayerWidgetState extends State if (_playerDisposed) { return; } - _player = Player(); + _player = Player( + configuration: PlayerConfiguration( + title: 'Selene', + bufferSize: 32 * 1024 * 1024, + logLevel: MPVLogLevel.error, + ), + ); _videoController = VideoController(_player!); _setupPlayerListeners(); if (_currentUrl != null) { diff --git a/lib/widgets/windows_title_bar.dart b/lib/widgets/windows_title_bar.dart index c1a31eb7..a672da66 100644 --- a/lib/widgets/windows_title_bar.dart +++ b/lib/widgets/windows_title_bar.dart @@ -170,7 +170,7 @@ class _WindowsButtonHoverState extends State<_WindowsButtonHover> { color: backgroundColor ?? Colors.transparent, child: Center( child: widget.isCloseButton && _isHovered - ? Icon( + ? const Icon( Icons.close, size: 16, color: Colors.white, From 14c5368136d4660b4214594ff71c465bd6854091 Mon Sep 17 00:00:00 2001 From: taskmemz <1450594750@qq.com> Date: Fri, 19 Jun 2026 07:12:31 +0000 Subject: [PATCH 2/9] =?UTF-8?q?restore:=20=E6=81=A2=E5=A4=8D=E8=AF=AF?= =?UTF-8?q?=E5=88=A0=E7=9A=84=20GitHub=20Actions=20=E5=85=A8=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E6=9E=84=E5=BB=BA=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 185 +++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..12d85b8c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,185 @@ +name: Flutter 全平台客户端构建 (Android独立+Linux独立+全平台) + +on: + workflow_dispatch: + inputs: + version: + description: '版本号 (留空则从 pubspec.yaml 读取)' + required: false + +jobs: + build-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: 安装 Linux 构建依赖 + run: | + sudo apt-get update + sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Run flutter pub get + run: flutter pub get + + - name: 构建 Linux + run: flutter build linux --release + + - name: 打包 Linux + run: | + cd build/linux/x64/release/bundles + tar czf selene-linux.tar.gz selene + + - name: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 + with: + name: linux-build + path: build/linux/x64/release/bundles/selene-linux.tar.gz + + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Run flutter pub get + run: flutter pub get + + - name: 构建 Windows + run: flutter build windows --release + + - name: 打包 Windows + run: | + Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath selene-windows.zip + + - name: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 + with: + name: windows-build + path: selene-windows.zip + + build-apple: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Run flutter pub get + run: flutter pub get + + - name: 构建 iOS + run: flutter build ios --release --no-codesign + + - name: 构建 macOS + run: flutter build macos --release + + - name: 打包 iOS & macOS + run: | + # iOS + cd build/ios/iphoneos + mkdir -p Payload + cp -r Runner.app Payload/ + zip -r ../../ios-build.ipa Payload/ + cd ../.. + + # macOS + cd build/macos/Build/Products/Release + zip -r ../../../macos-build.zip selene.app + cd ../../../.. + + - name: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 + with: + name: apple-build + path: | + build/ios-build.ipa + build/macos/Build/Products/Release/macos-build.zip + + build-android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Java 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: 同意 Android 许可证 + run: yes | sdkmanager --licenses || true + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Run flutter pub get + run: flutter pub get + + - name: 构建 Android 分架构APK + run: | + flutter build apk --release \ + --split-per-abi \ + --target-platform android-arm64,android-arm + + - name: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 + with: + name: android-build + path: build/app/outputs/flutter-apk/*.apk + + upload-to-release: + needs: [build-linux, build-windows, build-apple, build-android] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: 下载 Android APK + uses: actions/download-artifact@v4 + with: + name: android-build + path: artifacts/android + + - name: 下载 Linux + uses: actions/download-artifact@v4 + with: + name: linux-build + path: artifacts/linux + + - name: 下载 Windows + uses: actions/download-artifact@v4 + with: + name: windows-build + path: artifacts/windows + + - name: 下载 iOS+macOS + uses: actions/download-artifact@v4 + with: + name: apple-build + path: artifacts/apple + + - name: 上传全平台到 GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: | + artifacts/android/*.apk + artifacts/linux/*.tar.gz + artifacts/windows/*.zip + artifacts/apple/*.ipa + artifacts/apple/*.zip From f663d24e4eac2ae6154eedc50c017ee7acfbe388 Mon Sep 17 00:00:00 2001 From: taskmemz <1450594750@qq.com> Date: Fri, 19 Jun 2026 07:14:38 +0000 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20workflow=20?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E9=94=99=E8=AF=AF=E3=80=81Release=20?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E3=80=81Android=20=E6=B7=B7=E6=B7=86?= =?UTF-8?q?=EF=BC=8C=E7=BB=9F=E4=B8=80=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 133 ++++++++++++++++++------------------- 1 file changed, 63 insertions(+), 70 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 12d85b8c..2bf8868c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,14 +1,14 @@ -name: Flutter 全平台客户端构建 (Android独立+Linux独立+全平台) +name: Selene 全平台构建与发布 on: workflow_dispatch: - inputs: - version: - description: '版本号 (留空则从 pubspec.yaml 读取)' - required: false + +permissions: + contents: write jobs: build-linux: + name: Linux 构建 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -18,168 +18,161 @@ jobs: sudo apt-get update sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev - - name: Setup Flutter + - name: 配置 Flutter uses: subosito/flutter-action@v2 with: channel: stable - - name: Run flutter pub get + - name: 获取依赖 run: flutter pub get - name: 构建 Linux run: flutter build linux --release - - name: 打包 Linux + - name: 打包 run: | cd build/linux/x64/release/bundles tar czf selene-linux.tar.gz selene - - name: actions/upload-artifact@v4 + - name: 上传构建产物 uses: actions/upload-artifact@v4 with: - name: linux-build + name: selene-linux path: build/linux/x64/release/bundles/selene-linux.tar.gz build-windows: + name: Windows 构建 runs-on: windows-latest steps: - uses: actions/checkout@v4 - - name: Setup Flutter + - name: 配置 Flutter uses: subosito/flutter-action@v2 with: channel: stable - - name: Run flutter pub get + - name: 获取依赖 run: flutter pub get - name: 构建 Windows run: flutter build windows --release - - name: 打包 Windows + - name: 打包 run: | Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath selene-windows.zip - - name: actions/upload-artifact@v4 + - name: 上传构建产物 uses: actions/upload-artifact@v4 with: - name: windows-build + name: selene-windows path: selene-windows.zip build-apple: + name: Apple 构建 (iOS & macOS) runs-on: macos-latest steps: - uses: actions/checkout@v4 - - name: Setup Flutter + - name: 配置 Flutter uses: subosito/flutter-action@v2 with: channel: stable - - name: Run flutter pub get + - name: 获取依赖 run: flutter pub get - name: 构建 iOS run: flutter build ios --release --no-codesign - - name: 构建 macOS - run: flutter build macos --release - - - name: 打包 iOS & macOS + - name: 打包 iOS IPA run: | - # iOS cd build/ios/iphoneos mkdir -p Payload cp -r Runner.app Payload/ - zip -r ../../ios-build.ipa Payload/ - cd ../.. + zip -r ../../../selene-ios.ipa Payload/ - # macOS - cd build/macos/Build/Products/Release - zip -r ../../../macos-build.zip selene.app - cd ../../../.. + - name: 构建 macOS + run: flutter build macos --release + + - name: 打包 macOS DMG + run: | + hdiutil create -volname "Selene" \ + -srcfolder build/macos/Build/Products/Release/selene.app \ + -ov -format UDZO \ + selene-macos.dmg - - name: actions/upload-artifact@v4 + - name: 上传构建产物 uses: actions/upload-artifact@v4 with: - name: apple-build + name: selene-apple path: | - build/ios-build.ipa - build/macos/Build/Products/Release/macos-build.zip + selene-ios.ipa + selene-macos.dmg build-android: + name: Android 构建 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Setup Java 17 + - name: 配置 Java 17 uses: actions/setup-java@v4 with: - distribution: 'temurin' + distribution: temurin java-version: '17' - - name: 同意 Android 许可证 + - name: 同意 Android SDK 许可证 run: yes | sdkmanager --licenses || true - - name: Setup Flutter + - name: 配置 Flutter uses: subosito/flutter-action@v2 with: channel: stable - - name: Run flutter pub get + - name: 获取依赖 run: flutter pub get - - name: 构建 Android 分架构APK + - name: 构建 Android 分架构 APK run: | flutter build apk --release \ --split-per-abi \ + --obfuscate \ + --split-debug-info=build/app/outputs/symbols \ --target-platform android-arm64,android-arm - - name: actions/upload-artifact@v4 + - name: 上传构建产物 uses: actions/upload-artifact@v4 with: - name: android-build + name: selene-android path: build/app/outputs/flutter-apk/*.apk - upload-to-release: + release: + name: 发布到 GitHub Release needs: [build-linux, build-windows, build-apple, build-android] runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') - permissions: - contents: write steps: - uses: actions/checkout@v4 - - name: 下载 Android APK + - name: 下载全部构建产物 uses: actions/download-artifact@v4 with: - name: android-build - path: artifacts/android + path: artifacts - - name: 下载 Linux - uses: actions/download-artifact@v4 - with: - name: linux-build - path: artifacts/linux - - - name: 下载 Windows - uses: actions/download-artifact@v4 - with: - name: windows-build - path: artifacts/windows - - - name: 下载 iOS+macOS - uses: actions/download-artifact@v4 - with: - name: apple-build - path: artifacts/apple + - name: 读取版本号 + id: version + run: | + VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: *//' | cut -d'+' -f1) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" - - name: 上传全平台到 GitHub Release + - name: 创建 Release 并上传产物 uses: softprops/action-gh-release@v2 with: + tag_name: v${{ steps.version.outputs.version }} + name: Selene v${{ steps.version.outputs.version }} + generate_release_notes: true files: | - artifacts/android/*.apk - artifacts/linux/*.tar.gz - artifacts/windows/*.zip - artifacts/apple/*.ipa - artifacts/apple/*.zip + artifacts/selene-android/*.apk + artifacts/selene-linux/*.tar.gz + artifacts/selene-windows/*.zip + artifacts/selene-apple/*.ipa + artifacts/selene-apple/*.dmg From 369db0353c2ad0b523b9d88cac65d859cc980cb5 Mon Sep 17 00:00:00 2001 From: taskmemz <1450594750@qq.com> Date: Fri, 19 Jun 2026 07:32:12 +0000 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20=E8=A1=A5=E5=85=85=20Linux=20ALSA=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=EF=BC=8C=E4=BF=AE=E5=A4=8D=20Android=20SDK?= =?UTF-8?q?=20=E8=AE=B8=E5=8F=AF=E8=AF=81=E6=8E=A5=E5=8F=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2bf8868c..be054622 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: - name: 安装 Linux 构建依赖 run: | sudo apt-get update - sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev + sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev libasound2-dev - name: 配置 Flutter uses: subosito/flutter-action@v2 @@ -122,7 +122,11 @@ jobs: java-version: '17' - name: 同意 Android SDK 许可证 - run: yes | sdkmanager --licenses || true + run: | + mkdir -p "$ANDROID_HOME/licenses" + echo "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" >> "$ANDROID_HOME/licenses/android-sdk-license" + echo "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_HOME/licenses/android-sdk-preview-license" + yes | sdkmanager --licenses || true - name: 配置 Flutter uses: subosito/flutter-action@v2 From 5e0cea4f5029eefa069ce326476d9be35fbdefa6 Mon Sep 17 00:00:00 2001 From: taskmemz <1450594750@qq.com> Date: Fri, 19 Jun 2026 07:39:57 +0000 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20=E8=A1=A5=E5=85=85=20libmpv-dev=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=EF=BC=8C=E4=BF=AE=E5=A4=8D=20NDK=20=E8=AE=B8?= =?UTF-8?q?=E5=8F=AF=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index be054622..836dd603 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: - name: 安装 Linux 构建依赖 run: | sudo apt-get update - sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev libasound2-dev + sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev libasound2-dev libmpv-dev - name: 配置 Flutter uses: subosito/flutter-action@v2 @@ -124,9 +124,11 @@ jobs: - name: 同意 Android SDK 许可证 run: | mkdir -p "$ANDROID_HOME/licenses" - echo "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" >> "$ANDROID_HOME/licenses/android-sdk-license" - echo "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_HOME/licenses/android-sdk-preview-license" + echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" >> "$ANDROID_HOME/licenses/android-sdk-license" + echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_HOME/licenses/android-sdk-license" + echo -e "\nd56f5187479451eabf01fb78af6dfcb131a6481e" >> "$ANDROID_HOME/licenses/android-sdk-license" yes | sdkmanager --licenses || true + sdkmanager "ndk;29.0.14033849" || true - name: 配置 Flutter uses: subosito/flutter-action@v2 From 44a6ffb95ce885542b1c7c0c13bbd41c66d628e6 Mon Sep 17 00:00:00 2001 From: taskmemz <1450594750@qq.com> Date: Fri, 19 Jun 2026 07:45:36 +0000 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Linux=20?= =?UTF-8?q?=E6=89=93=E5=8C=85=E8=B7=AF=E5=BE=84=EF=BC=8C=E7=94=A8=20setup-?= =?UTF-8?q?android=20action=20=E5=A4=84=E7=90=86=20NDK=20=E8=AE=B8?= =?UTF-8?q?=E5=8F=AF=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 836dd603..70b77a57 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,7 +31,14 @@ jobs: - name: 打包 run: | - cd build/linux/x64/release/bundles + find build/linux -name "selene" -type f 2>/dev/null + BUNDLE_DIR=$(find build/linux -name "bundles" -type d 2>/dev/null | head -1) + if [ -z "$BUNDLE_DIR" ]; then + echo "Error: Could not find build output directory" + find build/linux -type f | head -20 + exit 1 + fi + cd "$BUNDLE_DIR" tar czf selene-linux.tar.gz selene - name: 上传构建产物 @@ -121,14 +128,13 @@ jobs: distribution: temurin java-version: '17' - - name: 同意 Android SDK 许可证 + - name: 配置 Android SDK 和 NDK + uses: android-actions/setup-android@v3 + + - name: 安装 NDK run: | - mkdir -p "$ANDROID_HOME/licenses" - echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" >> "$ANDROID_HOME/licenses/android-sdk-license" - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_HOME/licenses/android-sdk-license" - echo -e "\nd56f5187479451eabf01fb78af6dfcb131a6481e" >> "$ANDROID_HOME/licenses/android-sdk-license" yes | sdkmanager --licenses || true - sdkmanager "ndk;29.0.14033849" || true + sdkmanager --install "ndk;29.0.14033849" || true - name: 配置 Flutter uses: subosito/flutter-action@v2 From e70cf5a14218545efd0a41e6ed6f159420a064d6 Mon Sep 17 00:00:00 2001 From: taskmemz <1450594750@qq.com> Date: Fri, 19 Jun 2026 07:49:43 +0000 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=20Linux=20?= =?UTF-8?q?=E6=89=93=E5=8C=85=E8=B7=AF=E5=BE=84=20bundle/Selene?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 70b77a57..3eb81a32 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,15 +31,8 @@ jobs: - name: 打包 run: | - find build/linux -name "selene" -type f 2>/dev/null - BUNDLE_DIR=$(find build/linux -name "bundles" -type d 2>/dev/null | head -1) - if [ -z "$BUNDLE_DIR" ]; then - echo "Error: Could not find build output directory" - find build/linux -type f | head -20 - exit 1 - fi - cd "$BUNDLE_DIR" - tar czf selene-linux.tar.gz selene + cd build/linux/x64/release/bundle + tar czf selene-linux.tar.gz Selene - name: 上传构建产物 uses: actions/upload-artifact@v4 From 9500d9472d0861cd2c36c74c22ed6e8276c4c90c Mon Sep 17 00:00:00 2001 From: taskmemz <1450594750@qq.com> Date: Fri, 19 Jun 2026 07:54:42 +0000 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=20Linux=20artifac?= =?UTF-8?q?t=20=E4=B8=8A=E4=BC=A0=E8=B7=AF=E5=BE=84=20bundle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3eb81a32..a11a1b2d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,7 +38,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: selene-linux - path: build/linux/x64/release/bundles/selene-linux.tar.gz + path: build/linux/x64/release/bundle/selene-linux.tar.gz build-windows: name: Windows 构建 From e8c5cff9a0e2043696e76f13a3692bc0ae3d652e Mon Sep 17 00:00:00 2001 From: taskmemz <1450594750@qq.com> Date: Fri, 19 Jun 2026 08:06:11 +0000 Subject: [PATCH 9/9] =?UTF-8?q?refactor:=20=E6=8A=BD=E5=8F=96=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E8=84=9A=E6=9C=AC=EF=BC=8C=E5=8A=A8=E6=80=81=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=E8=B7=AF=E5=BE=84=E5=92=8C=E7=89=88=E6=9C=AC=EF=BC=8C?= =?UTF-8?q?=E5=87=8F=E5=B0=91=E4=B8=8A=E6=B8=B8=E5=8F=98=E6=9B=B4=E5=BD=B1?= =?UTF-8?q?=E5=93=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 61 ++++++-------------- scripts/ci-build.sh | 115 +++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 44 deletions(-) create mode 100755 scripts/ci-build.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a11a1b2d..f416b741 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,10 +13,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: 安装 Linux 构建依赖 + - name: 安装构建依赖 run: | sudo apt-get update - sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev libasound2-dev libmpv-dev + sudo apt-get install -y clang cmake ninja-build pkg-config \ + libgtk-3-dev liblzma-dev libstdc++-12-dev libasound2-dev libmpv-dev - name: 配置 Flutter uses: subosito/flutter-action@v2 @@ -26,19 +27,14 @@ jobs: - name: 获取依赖 run: flutter pub get - - name: 构建 Linux - run: flutter build linux --release - - - name: 打包 - run: | - cd build/linux/x64/release/bundle - tar czf selene-linux.tar.gz Selene + - name: 构建并打包 + run: bash scripts/ci-build.sh linux - name: 上传构建产物 uses: actions/upload-artifact@v4 with: name: selene-linux - path: build/linux/x64/release/bundle/selene-linux.tar.gz + path: build/**/selene-linux.tar.gz build-windows: name: Windows 构建 @@ -54,12 +50,13 @@ jobs: - name: 获取依赖 run: flutter pub get - - name: 构建 Windows + - name: 构建 run: flutter build windows --release - name: 打包 run: | - Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath selene-windows.zip + $dir = (Get-ChildItem -Path build -Recurse -Filter "runner" -Directory | Select-Object -First 1).FullName + Compress-Archive -Path "$dir\Release\*" -DestinationPath selene-windows.zip - name: 上传构建产物 uses: actions/upload-artifact@v4 @@ -81,25 +78,11 @@ jobs: - name: 获取依赖 run: flutter pub get - - name: 构建 iOS - run: flutter build ios --release --no-codesign + - name: 构建并打包 iOS + run: bash scripts/ci-build.sh ios - - name: 打包 iOS IPA - run: | - cd build/ios/iphoneos - mkdir -p Payload - cp -r Runner.app Payload/ - zip -r ../../../selene-ios.ipa Payload/ - - - name: 构建 macOS - run: flutter build macos --release - - - name: 打包 macOS DMG - run: | - hdiutil create -volname "Selene" \ - -srcfolder build/macos/Build/Products/Release/selene.app \ - -ov -format UDZO \ - selene-macos.dmg + - name: 构建并打包 macOS + run: bash scripts/ci-build.sh macos - name: 上传构建产物 uses: actions/upload-artifact@v4 @@ -121,14 +104,9 @@ jobs: distribution: temurin java-version: '17' - - name: 配置 Android SDK 和 NDK + - name: 配置 Android SDK uses: android-actions/setup-android@v3 - - name: 安装 NDK - run: | - yes | sdkmanager --licenses || true - sdkmanager --install "ndk;29.0.14033849" || true - - name: 配置 Flutter uses: subosito/flutter-action@v2 with: @@ -137,13 +115,8 @@ jobs: - name: 获取依赖 run: flutter pub get - - name: 构建 Android 分架构 APK - run: | - flutter build apk --release \ - --split-per-abi \ - --obfuscate \ - --split-debug-info=build/app/outputs/symbols \ - --target-platform android-arm64,android-arm + - name: 构建并打包 + run: bash scripts/ci-build.sh android - name: 上传构建产物 uses: actions/upload-artifact@v4 @@ -177,7 +150,7 @@ jobs: generate_release_notes: true files: | artifacts/selene-android/*.apk - artifacts/selene-linux/*.tar.gz + artifacts/selene-linux/**/*.tar.gz artifacts/selene-windows/*.zip artifacts/selene-apple/*.ipa artifacts/selene-apple/*.dmg diff --git a/scripts/ci-build.sh b/scripts/ci-build.sh new file mode 100755 index 00000000..e198a25c --- /dev/null +++ b/scripts/ci-build.sh @@ -0,0 +1,115 @@ +#!/bin/bash +# Selene CI 构建脚本 +# 自动检测输出路径、依赖版本,减少上游变更导致的 CI 挂掉 + +set -e + +PLATFORM="$1" +if [ -z "$PLATFORM" ]; then + echo "Usage: $0 " + exit 1 +fi + +log() { echo -e "\033[1;34m[CI]\033[0m $1"; } +err() { echo -e "\033[1;31m[ERROR]\033[0m $1"; exit 1; } + +# 从 pubspec.yaml 读版本 +VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: *//' | cut -d'+' -f1) +log "Version: $VERSION" + +# 通用:查找构建产物(递归搜索,不硬编码路径) +find_artifact() { + local pattern="$1" + local found + found=$(find build -name "$pattern" -type f 2>/dev/null | head -1) + if [ -z "$found" ]; then + # 尝试目录 + found=$(find build -name "$pattern" -type d 2>/dev/null | head -1) + fi + echo "$found" +} + +build_linux() { + log "Building Linux..." + flutter build linux --release + + # 动态查找 bundle 目录和可执行文件 + local bundle_dir + bundle_dir=$(find build/linux -name "bundle" -type d 2>/dev/null | head -1) + [ -z "$bundle_dir" ] && err "Cannot find Linux bundle directory" + + local binary + binary=$(find "$bundle_dir" -maxdepth 1 -type f -executable 2>/dev/null | head -1) + [ -z "$binary" ] && err "Cannot find Linux binary in $bundle_dir" + + log "Found binary: $binary" + cd "$bundle_dir" + tar czf "selene-linux.tar.gz" "$(basename "$binary")" + log "Packed: selene-linux.tar.gz" +} + +build_android() { + log "Building Android..." + + # 动态读取 NDK 版本(从 build.gradle 或 local.properties) + local ndk_version + ndk_version=$(grep -oP 'ndkVersion\s*=\s*"\K[^"]+' android/app/build.gradle 2>/dev/null || \ + grep -oP 'ndk\.version\s*=\s*\K\S+' android/local.properties 2>/dev/null || \ + echo "") + + if [ -n "$ndk_version" ]; then + log "NDK version from project: $ndk_version" + yes | sdkmanager --licenses 2>/dev/null || true + sdkmanager --install "ndk;$ndk_version" 2>/dev/null || true + fi + + flutter build apk --release \ + --split-per-abi \ + --obfuscate \ + --split-debug-info=build/app/outputs/symbols \ + --target-platform android-arm64,android-arm + + log "Android APKs:" + ls -la build/app/outputs/flutter-apk/*.apk 2>/dev/null +} + +build_ios() { + log "Building iOS..." + flutter build ios --release --no-codesign + + local app_dir + app_dir=$(find build/ios -name "Runner.app" -type d 2>/dev/null | head -1) + [ -z "$app_dir" ] && err "Cannot find Runner.app" + + cd "$(dirname "$app_dir")" + mkdir -p Payload + cp -r Runner.app Payload/ + zip -r "../../../selene-ios.ipa" Payload/ + rm -rf Payload + log "Packed: selene-ios.ipa" +} + +build_macos() { + log "Building macOS..." + flutter build macos --release + + local app_dir + app_dir=$(find build/macos -name "selene.app" -o -name "Selene.app" -o -name "Runner.app" 2>/dev/null | head -1) + [ -z "$app_dir" ] && err "Cannot find macOS .app" + + hdiutil create -volname "Selene" \ + -srcfolder "$app_dir" \ + -ov -format UDZO \ + selene-macos.dmg + log "Packed: selene-macos.dmg" +} + +case "$PLATFORM" in + linux) build_linux ;; + android) build_android ;; + ios) build_ios ;; + macos) build_macos ;; + *) err "Unknown platform: $PLATFORM" ;; +esac + +log "Done: $PLATFORM"