diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e75211..3db6f02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,7 +99,6 @@ jobs: git tag "$TAG_NAME" git push origin "$TAG_NAME" - name: Eextract log - id: extract_log run: python extract_log.py ${{ steps.create_tag.outputs.TAG_NAME }} - name: Download Windows artifact uses: actions/download-artifact@v4 @@ -117,7 +116,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ steps.create_tag.outputs.TAG_NAME }} - body: ${{ steps.extract_log.outputs.result }} + body_path: CHANGELOG_${{ steps.create_tag.outputs.TAG_NAME }}.md draft: false prerelease: false files: | diff --git a/CHANGELOG.md b/CHANGELOG.md index f2c6eeb..65dfff7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.0.3 +### Changelog +* Improve Windows version installation updates +* Fixes an issue where subtitles may not be found + +### 更新日志 +* 改进 Windows 版本安装更新 +* 修复可能无法找到字幕的问题 + ## v1.0.2 ### Changelog * Support for switching built-in audio tracks diff --git a/extract_log.py b/extract_log.py index ef8c5bf..78a4b91 100644 --- a/extract_log.py +++ b/extract_log.py @@ -24,7 +24,12 @@ def extract_log(version): changelog_lines.pop() output = "".join(changelog_lines).strip() - print(output) + + output_file = f"CHANGELOG_{version}.md" + with open(output_file, "w", encoding="utf-8") as file: + file.write(output) + + print(f"Changelog for {version} saved to {output_file}") if __name__ == "__main__": if len(sys.argv) != 2: @@ -32,4 +37,4 @@ def extract_log(version): sys.exit(1) version = sys.argv[1] - extract_log(version) + extract_log(version) \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 216c6e3..e93787e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -18,10 +18,12 @@ "check_update": "Check Update", "checked_new_version": "Checked New Version", "close": "Close", + "confirmUpdate": "Confirm update", "connection_test": "Connection Test", "dark": "Datk", "download": "Download", "download_and_update": "Download and Update", + "download_error": "Download Error", "edit": "Edit", "edit_local_storage": "Edit Local Storage", "edit_webdav_storage": "Edit WebDAV Storage", @@ -49,8 +51,10 @@ "play_queue": "Play Queue", "port": "Port", "previous": "Previous", + "releasePage": "Release Page", "remove": "Remove", "remove_favorite": "Remove Favorite", + "retry": "Retry", "save": "Save", "select_language": "Select Language", "settings": "Settings", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1f305c1..bd29b5c 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -18,10 +18,12 @@ "check_update": "检查更新", "checked_new_version": "检查到新版本", "close": "关闭", + "confirmUpdate": "确认更新", "connection_test": "连接测试", "dark": "暗色", "download": "下载", "download_and_update": "下载并更新", + "download_error": "下载错误", "edit": "编辑", "edit_local_storage": "编辑本地存储", "edit_webdav_storage": "编辑 WebDAV 存储", @@ -49,8 +51,10 @@ "play_queue": "播放队列", "port": "端口", "previous": "上一个", + "releasePage": "发布页面", "remove": "移除", "remove_favorite": "移除收藏", + "retry": "重试", "save": "保存", "select_language": "选择语言", "settings": "设置", diff --git a/lib/main.dart b/lib/main.dart index 3988382..5f9d6cb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_zustand/flutter_zustand.dart'; @@ -7,6 +6,7 @@ import 'package:iris/info.dart'; import 'package:iris/pages/home_page.dart'; import 'package:iris/store/use_app_store.dart'; import 'package:iris/theme.dart'; +import 'package:iris/utils/is_desktop.dart'; import 'package:media_kit/media_kit.dart'; import 'package:window_manager/window_manager.dart'; import 'package:dynamic_color/dynamic_color.dart'; @@ -15,7 +15,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); - if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { + if (isDesktop) { await windowManager.ensureInitialized(); WindowOptions windowOptions = const WindowOptions( diff --git a/lib/models/storages/local_storage.dart b/lib/models/storages/local_storage.dart index 60706de..f6b95a9 100644 --- a/lib/models/storages/local_storage.dart +++ b/lib/models/storages/local_storage.dart @@ -4,7 +4,7 @@ import 'package:iris/store/use_app_store.dart'; import 'package:iris/store/use_play_queue_store.dart'; import 'package:iris/utils/files_filter.dart'; import 'package:iris/utils/files_sort.dart'; -import 'package:iris/utils/find_sub_title.dart'; +import 'package:iris/utils/find_subtitle.dart'; import 'package:iris/utils/path_converter.dart'; import 'package:path/path.dart' as p; import 'package:iris/models/file.dart'; @@ -55,7 +55,7 @@ class LocalStorage implements Storage { type: entity is Directory ? 'dir' : checkFileType(p.basename(entity.path)), - subtitles: findSubTitle( + subtitles: findSubtitle( directory .listSync() .map((entity) => p.basename(entity.path)) diff --git a/lib/models/storages/webdav_storage.dart b/lib/models/storages/webdav_storage.dart index 2d5176f..7b305c2 100644 --- a/lib/models/storages/webdav_storage.dart +++ b/lib/models/storages/webdav_storage.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:developer'; import 'package:iris/utils/check_file_type.dart'; -import 'package:iris/utils/find_sub_title.dart'; +import 'package:iris/utils/find_subtitle.dart'; import 'package:uuid/uuid.dart'; import 'package:webdav_client/webdav_client.dart' as webdav; import 'package:iris/models/file.dart'; @@ -111,7 +111,7 @@ class WebdavStorage implements Storage { size: file.size ?? 0, type: file.isDir ?? false ? 'dir' : checkFileType(file.name!), auth: auth, - subtitles: findSubTitle( + subtitles: findSubtitle( files.map((file) => file.name as String).toList(), file.name as String, baseUri), diff --git a/lib/pages/custom_app_bar.dart b/lib/pages/custom_app_bar.dart index c834999..d2b9ede 100644 --- a/lib/pages/custom_app_bar.dart +++ b/lib/pages/custom_app_bar.dart @@ -1,9 +1,9 @@ -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:iris/hooks/use_player_core.dart'; import 'package:iris/info.dart'; import 'package:iris/utils/get_localizations.dart'; +import 'package:iris/utils/is_desktop.dart'; import 'package:iris/utils/resize_window.dart'; import 'package:window_manager/window_manager.dart'; @@ -22,9 +22,6 @@ class CustomAppBar extends HookWidget { Widget build(BuildContext context) { final t = getLocalizations(context); - bool isDesktop = useMemoized( - (() => Platform.isWindows || Platform.isLinux || Platform.isMacOS)); - return Container( padding: isDesktop ? const EdgeInsets.fromLTRB(12, 4, 4, 8) diff --git a/lib/pages/dialog/show_release_dialog.dart b/lib/pages/dialog/show_release_dialog.dart index f67c01e..af3afa7 100644 --- a/lib/pages/dialog/show_release_dialog.dart +++ b/lib/pages/dialog/show_release_dialog.dart @@ -1,5 +1,10 @@ +import 'dart:async'; import 'dart:io'; +import 'package:iris/utils/file_size_convert.dart'; +import 'package:iris/utils/is_desktop.dart'; +import 'package:iris/utils/path.dart'; import 'package:path/path.dart' as p; +import 'package:http/http.dart' as http; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; @@ -24,50 +29,160 @@ class ReleaseDialog extends HookWidget { @override Widget build(BuildContext context) { final t = getLocalizations(context); + final progress = useState(0.0); + final isError = useState(false); + final isDownload = useState(false); + final downloadingTask = useState(null); + const downloadFileName = 'Iris.zip'; void update() async { if (Platform.isWindows) { - String resolvedExecutablePath = Platform.resolvedExecutable; - String path = p.dirname(resolvedExecutablePath); - String batFilePath = p.join(path, 'iris-updater.bat'); + final String executableDirPath = await getExecutableDirPath(); + final String tempPath = await getTempPath(); + final String filePath = p.join(tempPath, downloadFileName); + final String executablePath = p.join(executableDirPath, 'iris.exe'); + + if (!File(filePath).existsSync()) { + return; + } + + final commands = [ + 'title Iris', + 'timeout /t 2 /nobreak', + 'powerShell Expand-Archive -Path "$filePath" -DestinationPath "$tempPath" -Force', + 'xcopy $tempPath\\Iris\\* $executableDirPath /s /E /I /Y', + 'rd /s /q $tempPath', + 'start $executablePath', + ]; - // 执行 bat 文件 await Process.start( - 'cmd.exe', - ['/c', batFilePath], + 'cmd', + ['/c', commands.join(' && ')], mode: ProcessStartMode.detached, runInShell: true, + workingDirectory: executableDirPath, ); - // 退出应用 exit(0); } } + Future downloadFile(String url, String filePath) async { + final client = http.Client(); + final request = http.Request('GET', Uri.parse(url)); + final response = await client.send(request); + if (response.statusCode == 200) { + final totalBytes = response.contentLength ?? 0; + + var file = File(filePath); + var sink = file.openWrite(); + + final streamSubscription = response.stream.listen( + (List chunk) { + sink.add(chunk); + progress.value += chunk.length / totalBytes; + }, + onDone: () async { + await sink.close(); + client.close(); + }, + onError: (error) { + sink.close(); + client.close(); + isError.value = true; + isDownload.value = false; + }, + cancelOnError: true, + ); + return streamSubscription; + } else { + throw Exception('Download failed with status ${response.statusCode}'); + } + } + + Future download() async { + if (isDesktop && !isDownload.value) { + final String tempPath = await getTempPath(); + final String filePath = p.join(tempPath, downloadFileName); + + try { + isDownload.value = true; + isError.value = false; + final sub = await downloadFile(release.url, filePath); + downloadingTask.value = sub; + } catch (e) { + isError.value = true; + } + } else { + launchURL(release.url); + Navigator.pop(context, 'OK'); + } + } + + Future cancel() async { + if (!(progress.value == 0) && downloadingTask.value != null) { + await downloadingTask.value!.cancel(); + progress.value = 0.0; + } + + if (context.mounted) { + Navigator.pop(context, 'Cancel'); + } + } + return AlertDialog( title: Text('${t.checked_new_version}: ${release.version}'), content: SingleChildScrollView( - child: MarkdownBody(data: release.changeLog, shrinkWrap: true), + child: isError.value + ? SizedBox( + height: 100, + width: 100, + child: Center( + child: Text(t.download_error), + ), + ) + : isDownload.value + ? SizedBox( + height: 100, + width: 100, + child: Center( + child: CircularProgressIndicator(value: progress.value), + ), + ) + : MarkdownBody(data: release.changeLog, shrinkWrap: true), ), actions: [ TextButton( - onPressed: () => Navigator.pop(context, 'Cancel'), + onPressed: cancel, child: Text(t.cancel), ), TextButton( - onPressed: () { - launchURL(release.url); - Navigator.pop(context, 'OK'); - }, - child: Text(t.download), + onPressed: () => launchURL(release.url), + child: Text(t.releasePage), + ), + Visibility( + visible: !isDesktop, + child: TextButton( + onPressed: download, + child: Text(t.download), + ), ), Visibility( - visible: Platform.isWindows || Platform.isLinux || Platform.isMacOS, + visible: isDesktop, child: TextButton( - onPressed: () { - update(); - }, - child: Text(t.download_and_update), + onPressed: progress.value >= 1 + ? update + : isDownload.value + ? null + : download, + child: isError.value + ? Text(t.retry) + : progress.value >= 1 + ? Text(t.confirmUpdate) + : isDownload.value + ? Text('${(progress.value * 100).toStringAsFixed(2)} %') + : Text( + '${t.download} (${fileSizeConvert(release.size)} MB)'), ), ), ], diff --git a/lib/pages/player/control_bar.dart b/lib/pages/player/control_bar.dart index 3f9b2da..512114a 100644 --- a/lib/pages/player/control_bar.dart +++ b/lib/pages/player/control_bar.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_zustand/flutter_zustand.dart'; @@ -11,6 +10,7 @@ import 'package:iris/pages/settings/settings.dart'; import 'package:iris/store/use_play_queue_store.dart'; import 'package:iris/utils/get_localizations.dart'; import 'package:iris/pages/player/play_queue.dart'; +import 'package:iris/utils/is_desktop.dart'; import 'package:iris/utils/resize_window.dart'; import 'package:iris/pages/show_popup.dart'; import 'package:iris/pages/storages/storages.dart'; @@ -33,8 +33,6 @@ class ControlBar extends HookWidget { @override Widget build(BuildContext context) { final t = getLocalizations(context); - bool isDesktop = useMemoized( - (() => Platform.isWindows || Platform.isLinux || Platform.isMacOS)); final playQueueLength = usePlayQueueStore().select(context, (state) => state.playQueue.length); diff --git a/lib/pages/player/iris_player.dart b/lib/pages/player/iris_player.dart index b823f42..1673c3d 100644 --- a/lib/pages/player/iris_player.dart +++ b/lib/pages/player/iris_player.dart @@ -15,6 +15,7 @@ import 'package:iris/pages/settings/settings.dart'; import 'package:iris/pages/show_popup.dart'; import 'package:iris/pages/storages/storages.dart'; import 'package:iris/utils/format_duration_to_minutes.dart'; +import 'package:iris/utils/is_desktop.dart'; import 'package:iris/utils/logger.dart'; import 'package:iris/utils/path.dart'; import 'package:iris/utils/resize_window.dart'; @@ -78,9 +79,6 @@ class IrisPlayer extends HookWidget { return; }, []); - bool isDesktop = useMemoized( - (() => Platform.isWindows || Platform.isLinux || Platform.isMacOS)); - PlayerCore playerCore = usePlayerCore(context, player); PlayerController playerController = usePlayerController(context, playerCore); diff --git a/lib/pages/settings/play.dart b/lib/pages/settings/play.dart index f3d4075..5f79d73 100644 --- a/lib/pages/settings/play.dart +++ b/lib/pages/settings/play.dart @@ -1,10 +1,9 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_zustand/flutter_zustand.dart'; import 'package:iris/store/use_app_store.dart'; import 'package:iris/utils/get_localizations.dart'; +import 'package:iris/utils/is_desktop.dart'; import 'package:window_manager/window_manager.dart'; class Play extends HookWidget { @@ -13,8 +12,6 @@ class Play extends HookWidget { @override Widget build(BuildContext context) { final t = getLocalizations(context); - bool isDesktop = useMemoized( - (() => Platform.isWindows || Platform.isLinux || Platform.isMacOS)); final autoResize = useAppStore().select(context, (state) => state.autoResize); diff --git a/lib/pages/show_popup.dart b/lib/pages/show_popup.dart index b2f702b..d7177c5 100644 --- a/lib/pages/show_popup.dart +++ b/lib/pages/show_popup.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:iris/utils/is_desktop.dart'; import 'package:window_manager/window_manager.dart'; enum PopupDirection { left, right } @@ -63,9 +64,7 @@ class Popup extends PopupRoute { ), ), Positioned( - top: Platform.isWindows || Platform.isLinux || Platform.isMacOS - ? 48 - : 8, + top: isDesktop ? 48 : 8, left: direction == PopupDirection.left ? 8 : null, right: direction == PopupDirection.right ? 8 : null, bottom: 8, diff --git a/lib/utils/find_sub_title.dart b/lib/utils/find_subtitle.dart similarity index 65% rename from lib/utils/find_sub_title.dart rename to lib/utils/find_subtitle.dart index 445f2a0..8554685 100644 --- a/lib/utils/find_sub_title.dart +++ b/lib/utils/find_subtitle.dart @@ -1,7 +1,7 @@ import 'package:iris/models/file.dart'; import 'package:iris/utils/check_file_type.dart'; -List findSubTitle( +List findSubtitle( List files, String name, String baseUri, @@ -12,13 +12,16 @@ List findSubTitle( String baseName = name.split('.').sublist(0, name.split('.').length - 1).join('.'); - List subtitleExtensions = ['.ass', '.srt', '.vtt', '.sub']; + List subtitleExtensions = ['ass', 'srt', 'vtt', 'sub']; for (String file in files) { - if (file.split('.')[0].contains(baseName) && + if (file.startsWith(baseName) && subtitleExtensions.any((ext) => file.endsWith(ext))) { - String subTitleName = - file.split('.').sublist(1, file.split('.').length - 1).join('.'); + String subTitleName = file + .replaceAll(baseName, '') + .split('.') + .where((e) => e.isNotEmpty && !subtitleExtensions.contains(e)) + .join('.'); foundSubTitles.add(Subtitle( name: subTitleName.isEmpty ? file : subTitleName, uri: '$baseUri/$file', diff --git a/lib/utils/get_latest_release.dart b/lib/utils/get_latest_release.dart index 0c67bbb..cd2b7ba 100644 --- a/lib/utils/get_latest_release.dart +++ b/lib/utils/get_latest_release.dart @@ -7,9 +7,17 @@ import 'package:package_info_plus/package_info_plus.dart'; class Release { final String version; final String url; + final String downloadUrl; + final int size; final String changeLog; - Release({required this.version, required this.url, required this.changeLog}); + Release({ + required this.version, + required this.url, + required this.downloadUrl, + required this.size, + required this.changeLog, + }); } Future getLatestRelease() async { @@ -37,16 +45,29 @@ Future getLatestRelease() async { final List assets = data['assets']; final String version = data['tag_name'] ?? 'Unknown version'; - final String url = assets - .where((assets) => - assets['name'].toString().toLowerCase().contains(platform)) - .first['browser_download_url']; + + final String url = + data['html_url'] ?? 'https://github.com/nini22P/Iris/releases/'; + + final filtteredAssets = assets.where((assets) => + assets['name'].toString().toLowerCase().contains(platform)); + + final downloadUrl = filtteredAssets.first['browser_download_url']; + + final size = filtteredAssets.first['size']; + final String changeLog = data['body']; final bool isUpdate = isVersionUpdated(packageInfo.version, version); if (isUpdate) { - return Release(version: version, url: url, changeLog: changeLog); + return Release( + version: version, + url: url, + downloadUrl: downloadUrl, + size: size, + changeLog: changeLog, + ); } else { return null; } diff --git a/lib/utils/is_desktop.dart b/lib/utils/is_desktop.dart new file mode 100644 index 0000000..3c899b4 --- /dev/null +++ b/lib/utils/is_desktop.dart @@ -0,0 +1,3 @@ +import 'dart:io'; + +bool isDesktop = Platform.isWindows || Platform.isLinux || Platform.isMacOS; diff --git a/lib/utils/path.dart b/lib/utils/path.dart index 7e4529b..1182553 100644 --- a/lib/utils/path.dart +++ b/lib/utils/path.dart @@ -1,6 +1,6 @@ import 'dart:io'; import 'package:iris/info.dart'; -import 'package:path/path.dart'; +import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; Future getDataPath() async { @@ -8,7 +8,7 @@ Future getDataPath() async { ? await getExternalStorageDirectory() : await getApplicationDocumentsDirectory(); - final path = join(directory!.path, INFO.title); + final path = p.join(directory!.path, INFO.title); final fileDirectory = Directory(path); if (!await fileDirectory.exists()) { @@ -19,12 +19,26 @@ Future getDataPath() async { } Future getConfigPath() async { - final directory = join(await getDataPath(), 'config'); + final directory = p.join(await getDataPath(), 'config'); final fileDirectory = Directory(directory); if (!await fileDirectory.exists()) { await fileDirectory.create(recursive: true); } - return join(directory, 'config.json'); + return p.join(directory, 'config.json'); +} + +Future getExecutableDirPath() async { + String resolvedExecutablePath = Platform.resolvedExecutable; + return p.dirname(resolvedExecutablePath); +} + +Future getTempPath() async { + final directory = await getTemporaryDirectory(); + final String tempPath = p.join(directory.path, 'Iris'); + if (!Directory(tempPath).existsSync()) { + Directory(tempPath).createSync(recursive: true); + } + return tempPath; } diff --git a/pubspec.yaml b/pubspec.yaml index 19376e9..f533af6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.2+1 +version: 1.0.3+1 environment: sdk: ^3.5.4