WIP 시험 · 앱이 %TEMP% 에 고정 이름을 남기지 않게 (관리자 지시 10/20): 시험의 앱 임시 폴더는 파일마다 jj_store_ (flutter_test_config) · 업데이트 시험은 시험 폴더 (GitHubUpdater.tempRoot, 기록은 스크립트 옆) · check.py 는 임시 폴더 · 앱이 켜질 때 지난 업데이트가 남긴 새 판 폴더 · .ps1 · .log 를 지움 (2분 지난 것) · 검사는 jj 로 시작하는 것 모두 실패 · 마그넷 멈춘 막대 "Stalled" 따로 열쇠 (28 지적) · explorer_test 82 부하 때 목록 기다리기 (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>review_d2dc267
parent
2f4ad6c146
commit
e228e84adb
|
|
@ -1196,6 +1196,7 @@
|
||||||
"먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "First asks via mDNS (NAS devices that announce their names), then tries an SMB (445) connection to devices on the network this device is on (Wi-Fi · wired) and closes it right away. No files or account details are sent.",
|
"먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "First asks via mDNS (NAS devices that announce their names), then tries an SMB (445) connection to devices on the network this device is on (Wi-Fi · wired) and closes it right away. No files or account details are sent.",
|
||||||
"멈추고 옮기기": "Stop and move",
|
"멈추고 옮기기": "Stop and move",
|
||||||
"멈춤": "Stopped",
|
"멈춤": "Stopped",
|
||||||
|
"멈춤 (시작 안 됨)": "Stalled",
|
||||||
"멈춤 (이번 실행 동안)": "Pause (this session)",
|
"멈춤 (이번 실행 동안)": "Pause (this session)",
|
||||||
"멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "Paused (this session) · press the start button to sync again",
|
"멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "Paused (this session) · press the start button to sync again",
|
||||||
"멈춤 · {0}": "Stopped · {0}",
|
"멈춤 · {0}": "Stopped · {0}",
|
||||||
|
|
@ -2426,6 +2427,7 @@
|
||||||
"지금까지 찾은 {0}개 모두": "All {0} found so far",
|
"지금까지 찾은 {0}개 모두": "All {0} found so far",
|
||||||
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "This is the Dual App (cloned app). Android may not grant this permission to dual apps. If you can't turn it on, open the original JJ_MKVMaker icon without the badge.",
|
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "This is the Dual App (cloned app). Android may not grant this permission to dual apps. If you can't turn it on, open the original JJ_MKVMaker icon without the badge.",
|
||||||
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "The last run did not end normally (last check {0}:{1}). It has been noted in the job log.",
|
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "The last run did not end normally (last check {0}:{1}). It has been noted in the job log.",
|
||||||
|
"지난 업데이트가 남긴 임시 파일을 지움: {0}": "Removed temporary files left by the last update: {0}",
|
||||||
"지난 작업 기록": "Old job logs",
|
"지난 작업 기록": "Old job logs",
|
||||||
"지문 확인하고 연결": "Check fingerprint and connect",
|
"지문 확인하고 연결": "Check fingerprint and connect",
|
||||||
"지우고 초기화": "Delete and reset",
|
"지우고 초기화": "Delete and reset",
|
||||||
|
|
|
||||||
|
|
@ -1196,6 +1196,7 @@
|
||||||
"먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "まず mDNS で問い合わせ (名前を知らせる NAS)、続いてこの端末が接続しているネットワーク (Wi-Fi · 有線) の機器に SMB (445) で接続だけ試し、すぐに閉じます。ファイルやアカウント情報は送りません。",
|
"먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "まず mDNS で問い合わせ (名前を知らせる NAS)、続いてこの端末が接続しているネットワーク (Wi-Fi · 有線) の機器に SMB (445) で接続だけ試し、すぐに閉じます。ファイルやアカウント情報は送りません。",
|
||||||
"멈추고 옮기기": "止めて移す",
|
"멈추고 옮기기": "止めて移す",
|
||||||
"멈춤": "停止",
|
"멈춤": "停止",
|
||||||
|
"멈춤 (시작 안 됨)": "停滞 (開始されません)",
|
||||||
"멈춤 (이번 실행 동안)": "停止 (今回の実行中)",
|
"멈춤 (이번 실행 동안)": "停止 (今回の実行中)",
|
||||||
"멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "停止中 (今回の実行中) · 開始ボタンで再び同期します",
|
"멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "停止中 (今回の実行中) · 開始ボタンで再び同期します",
|
||||||
"멈춤 · {0}": "停止 · {0}",
|
"멈춤 · {0}": "停止 · {0}",
|
||||||
|
|
@ -2426,6 +2427,7 @@
|
||||||
"지금까지 찾은 {0}개 모두": "これまでに見つかった {0} 件すべて",
|
"지금까지 찾은 {0}개 모두": "これまでに見つかった {0} 件すべて",
|
||||||
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "これはデュアルアプリ (複製したアプリ) です。Android はデュアルアプリにこの権限を与えない場合があります。許可画面でオンにできない場合は、バッジのない元の JJ_MKVMaker アイコンから開いてください。",
|
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "これはデュアルアプリ (複製したアプリ) です。Android はデュアルアプリにこの権限を与えない場合があります。許可画面でオンにできない場合は、バッジのない元の JJ_MKVMaker アイコンから開いてください。",
|
||||||
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "前回の実行が正常に終了しませんでした(最終確認 {0}:{1})。作業記録に残しました。",
|
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "前回の実行が正常に終了しませんでした(最終確認 {0}:{1})。作業記録に残しました。",
|
||||||
|
"지난 업데이트가 남긴 임시 파일을 지움: {0}": "前回のアップデートが残した一時ファイルを削除: {0}",
|
||||||
"지난 작업 기록": "過去の作業記録",
|
"지난 작업 기록": "過去の作業記録",
|
||||||
"지문 확인하고 연결": "フィンガープリントを確認して接続",
|
"지문 확인하고 연결": "フィンガープリントを確認して接続",
|
||||||
"지우고 초기화": "削除して初期化",
|
"지우고 초기화": "削除して初期化",
|
||||||
|
|
|
||||||
|
|
@ -1196,6 +1196,7 @@
|
||||||
"먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "先通过 mDNS 询问 (会公布名称的 NAS),再仅尝试与本设备所在网络 (Wi-Fi · 有线) 中的设备建立 SMB (445) 连接并立即关闭。不会发送文件或账户信息。",
|
"먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "先通过 mDNS 询问 (会公布名称的 NAS),再仅尝试与本设备所在网络 (Wi-Fi · 有线) 中的设备建立 SMB (445) 连接并立即关闭。不会发送文件或账户信息。",
|
||||||
"멈추고 옮기기": "停止并移动",
|
"멈추고 옮기기": "停止并移动",
|
||||||
"멈춤": "已停止",
|
"멈춤": "已停止",
|
||||||
|
"멈춤 (시작 안 됨)": "停滞 (未开始)",
|
||||||
"멈춤 (이번 실행 동안)": "暂停 (本次运行期间)",
|
"멈춤 (이번 실행 동안)": "暂停 (本次运行期间)",
|
||||||
"멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "已暂停 (本次运行期间) · 按开始按钮重新同步",
|
"멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "已暂停 (本次运行期间) · 按开始按钮重新同步",
|
||||||
"멈춤 · {0}": "已停止 · {0}",
|
"멈춤 · {0}": "已停止 · {0}",
|
||||||
|
|
@ -2426,6 +2427,7 @@
|
||||||
"지금까지 찾은 {0}개 모두": "目前找到的全部 {0} 个",
|
"지금까지 찾은 {0}개 모두": "目前找到的全部 {0} 个",
|
||||||
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "当前是双开应用 (复制的应用)。Android 可能不会给双开应用此权限。如果无法开启,请从没有徽标的原 JJ_MKVMaker 图标打开。",
|
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "当前是双开应用 (复制的应用)。Android 可能不会给双开应用此权限。如果无法开启,请从没有徽标的原 JJ_MKVMaker 图标打开。",
|
||||||
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "上次运行未正常结束(最后检查 {0}:{1})。已记录在任务记录中。",
|
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "上次运行未正常结束(最后检查 {0}:{1})。已记录在任务记录中。",
|
||||||
|
"지난 업데이트가 남긴 임시 파일을 지움: {0}": "已删除上次更新留下的临时文件: {0}",
|
||||||
"지난 작업 기록": "旧任务记录",
|
"지난 작업 기록": "旧任务记录",
|
||||||
"지문 확인하고 연결": "确认指纹并连接",
|
"지문 확인하고 연결": "确认指纹并连接",
|
||||||
"지우고 초기화": "删除并重置",
|
"지우고 초기화": "删除并重置",
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ void main() {
|
||||||
expect(t.takeException(), isNull, reason: '넘침');
|
expect(t.takeException(), isNull, reason: '넘침');
|
||||||
expect(find.textContaining('[시험] 아주 긴 이름'), findsOneWidget);
|
expect(find.textContaining('[시험] 아주 긴 이름'), findsOneWidget);
|
||||||
expect(find.textContaining('Ubuntu 24.04.1 Desktop amd64'), findsOneWidget);
|
expect(find.textContaining('Ubuntu 24.04.1 Desktop amd64'), findsOneWidget);
|
||||||
expect(find.text(tr('멈춤')), findsOneWidget);
|
expect(find.text(tr('멈춤 (시작 안 됨)')), findsOneWidget);
|
||||||
final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
||||||
final img = await t.runAsync(() => b.toImage());
|
final img = await t.runAsync(() => b.toImage());
|
||||||
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
|
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import '../services/model_store.dart';
|
||||||
import '../services/platform_services.dart';
|
import '../services/platform_services.dart';
|
||||||
import '../services/system_usage.dart';
|
import '../services/system_usage.dart';
|
||||||
import 'settings.dart';
|
import 'settings.dart';
|
||||||
|
import 'cleanup.dart' show removeUpdateLeftovers;
|
||||||
import '../l10n/tr.dart';
|
import '../l10n/tr.dart';
|
||||||
|
|
||||||
extension<T> on Set<T> {
|
extension<T> on Set<T> {
|
||||||
|
|
@ -205,6 +206,10 @@ class AppController extends ChangeNotifier {
|
||||||
);
|
);
|
||||||
_applySettings();
|
_applySettings();
|
||||||
}
|
}
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
final gone = removeUpdateLeftovers(Directory.systemTemp.path);
|
||||||
|
if (gone.isNotEmpty) _log(trf('지난 업데이트가 남긴 임시 파일을 지움: {0}', [gone.join(', ')]));
|
||||||
|
}
|
||||||
ffmpegVersion = await _tool.version();
|
ffmpegVersion = await _tool.version();
|
||||||
if (ffmpegVersion == null) {
|
if (ffmpegVersion == null) {
|
||||||
_log(tr('⚠ FFmpeg 를 찾을 수 없습니다. MKV 만들기를 사용할 수 없습니다.'));
|
_log(tr('⚠ FFmpeg 를 찾을 수 없습니다. MKV 만들기를 사용할 수 없습니다.'));
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,29 @@ import '../l10n/tr.dart';
|
||||||
import 'app_controller.dart';
|
import 'app_controller.dart';
|
||||||
import 'download_manager.dart';
|
import 'download_manager.dart';
|
||||||
|
|
||||||
|
/// 관리자 지시 (10/20): Windows 업데이트가 %TEMP% 에 남긴 것 - 풀어 둔 새 판 (jj_mkvmaker_update_<판>, 수백 MB) ·
|
||||||
|
/// 교체 스크립트 (jj_mkvmaker_update.ps1) · 기록 (jj_mkvmaker_update.log). 예전에는 [정리] 를 누르기 전까지 남았다.
|
||||||
|
/// 앱이 켜질 때 부른다. 교체 스크립트가 아직 끝나는 중일 수 있어 [minAge] 보다 오래된 것만 지운다. 지운 이름을 돌려준다
|
||||||
|
List<String> removeUpdateLeftovers(String sysTmp, {Duration minAge = const Duration(minutes: 2), DateTime? now}) {
|
||||||
|
final mine = RegExp(r'^jj_mkvmaker_update(_.+|\.ps1|\.log)$');
|
||||||
|
final before = (now ?? DateTime.now()).subtract(minAge);
|
||||||
|
final removed = <String>[];
|
||||||
|
try {
|
||||||
|
for (final e in Directory(sysTmp).listSync(followLinks: false)) {
|
||||||
|
final name = p.basename(e.path);
|
||||||
|
if (!mine.hasMatch(name)) continue;
|
||||||
|
try {
|
||||||
|
if (!e.statSync().modified.isBefore(before)) continue;
|
||||||
|
e.deleteSync(recursive: true);
|
||||||
|
removed.add(name);
|
||||||
|
} catch (_) {
|
||||||
|
// 잡혀 있으면 (기록을 메모장으로 열어 둔 것 등) 다음에
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
/// 정리할 파일 · 폴더 하나
|
/// 정리할 파일 · 폴더 하나
|
||||||
class CleanupItem {
|
class CleanupItem {
|
||||||
final String path;
|
final String path;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ import '../../services/storage_service.dart';
|
||||||
import '../../l10n/tr.dart';
|
import '../../l10n/tr.dart';
|
||||||
|
|
||||||
class DesktopStorageService implements StorageService {
|
class DesktopStorageService implements StorageService {
|
||||||
|
/// [tempDirectory] 의 기준 폴더 (null = 시스템 임시 폴더). 시험이 파일마다 따로 쓰고 끝에 지우게 한다
|
||||||
|
/// (test/flutter_test_config.dart - 예전에는 시험이 %TEMP%\jj_mkvmaker 를 남겼다, 관리자 지시 10/20)
|
||||||
|
static String? tempRoot;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<String>> pickVideos() async {
|
Future<List<String>> pickVideos() async {
|
||||||
final files = await FilePicker.pickFiles(
|
final files = await FilePicker.pickFiles(
|
||||||
|
|
@ -108,7 +112,7 @@ class DesktopStorageService implements StorageService {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> tempDirectory() async {
|
Future<String> tempDirectory() async {
|
||||||
final dir = Directory(p.join(Directory.systemTemp.path, 'jj_mkvmaker'));
|
final dir = Directory(p.join(tempRoot ?? Directory.systemTemp.path, 'jj_mkvmaker'));
|
||||||
await dir.create(recursive: true);
|
await dir.create(recursive: true);
|
||||||
return dir.path;
|
return dir.path;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,19 @@ class GitHubUpdater implements Updater {
|
||||||
..userAgent = 'JJ_MKVMaker-updater'
|
..userAgent = 'JJ_MKVMaker-updater'
|
||||||
..connectionTimeout = const Duration(seconds: 20);
|
..connectionTimeout = const Duration(seconds: 20);
|
||||||
|
|
||||||
|
/// 받은 업데이트 · 교체 스크립트 · 기록을 둘 곳 (null = 시스템 임시 폴더). 시험은 시험 폴더를 준다
|
||||||
|
/// (예전에는 시험이 %TEMP% 에 jj_mkvmaker_update_1.9.0 · .ps1 · .log 를 남겼다, 관리자 지시 10/20)
|
||||||
|
final String? tempRoot;
|
||||||
|
|
||||||
GitHubUpdater({
|
GitHubUpdater({
|
||||||
this.apiBase = 'https://api.github.com',
|
this.apiBase = 'https://api.github.com',
|
||||||
String? appDir,
|
String? appDir,
|
||||||
this.versionOverride,
|
this.versionOverride,
|
||||||
|
this.tempRoot,
|
||||||
}) : appDir = appDir ?? AppPaths.root; // Lib 구조면 배포 폴더 맨 위
|
}) : appDir = appDir ?? AppPaths.root; // Lib 구조면 배포 폴더 맨 위
|
||||||
|
|
||||||
|
String get _temp => tempRoot ?? Directory.systemTemp.path;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get installsInPlace => false;
|
bool get installsInPlace => false;
|
||||||
|
|
||||||
|
|
@ -81,7 +88,7 @@ class GitHubUpdater implements Updater {
|
||||||
Future<String> download(ReleaseInfo r, void Function(double progress) onProgress) async {
|
Future<String> download(ReleaseInfo r, void Function(double progress) onProgress) async {
|
||||||
final url = r.zipUrl;
|
final url = r.zipUrl;
|
||||||
if (url == null) throw UpdateException(tr('이 버전에는 Windows 용 zip 이 없습니다.'));
|
if (url == null) throw UpdateException(tr('이 버전에는 Windows 용 zip 이 없습니다.'));
|
||||||
final work = Directory(p.join(Directory.systemTemp.path, 'jj_mkvmaker_update_${r.version}'));
|
final work = Directory(p.join(_temp, 'jj_mkvmaker_update_${r.version}'));
|
||||||
if (await work.exists()) await work.delete(recursive: true);
|
if (await work.exists()) await work.delete(recursive: true);
|
||||||
await work.create(recursive: true);
|
await work.create(recursive: true);
|
||||||
final zip = File(p.join(work.path, r.zipName ?? 'update.zip'));
|
final zip = File(p.join(work.path, r.zipName ?? 'update.zip'));
|
||||||
|
|
@ -134,7 +141,7 @@ param([int]$ParentPid, [string]$Source, [string]$Target, [string]$Exe, [switch]$
|
||||||
$ErrorActionPreference = 'Continue'
|
$ErrorActionPreference = 'Continue'
|
||||||
try { Wait-Process -Id $ParentPid -Timeout 90 -ErrorAction Stop } catch {}
|
try { Wait-Process -Id $ParentPid -Timeout 90 -ErrorAction Stop } catch {}
|
||||||
Start-Sleep -Milliseconds 800
|
Start-Sleep -Milliseconds 800
|
||||||
$log = Join-Path $env:TEMP 'jj_mkvmaker_update.log'
|
$log = Join-Path (Split-Path -Parent $PSCommandPath) 'jj_mkvmaker_update.log'
|
||||||
# keep downloads, models and user data
|
# keep downloads, models and user data
|
||||||
$lib = Join-Path $Source 'Lib'
|
$lib = Join-Path $Source 'Lib'
|
||||||
if (Test-Path $lib) {
|
if (Test-Path $lib) {
|
||||||
|
|
@ -154,7 +161,7 @@ exit 0
|
||||||
|
|
||||||
/// 스크립트 실행. [waitPid] 가 끝나기를 기다렸다가 교체한다.
|
/// 스크립트 실행. [waitPid] 가 끝나기를 기다렸다가 교체한다.
|
||||||
Future<Process> runInstallScript(String source, {required int waitPid, bool restart = true}) async {
|
Future<Process> runInstallScript(String source, {required int waitPid, bool restart = true}) async {
|
||||||
final ps1 = File(p.join(Directory.systemTemp.path, 'jj_mkvmaker_update.ps1'));
|
final ps1 = File(p.join(_temp, 'jj_mkvmaker_update.ps1'));
|
||||||
await ps1.writeAsString(script);
|
await ps1.writeAsString(script);
|
||||||
return Process.start(
|
return Process.start(
|
||||||
'powershell.exe',
|
'powershell.exe',
|
||||||
|
|
|
||||||
|
|
@ -419,7 +419,8 @@ class DownloadProgressBar extends StatelessWidget {
|
||||||
: t.state == DownloadState.queued || t.extra['waiting'] == true
|
: t.state == DownloadState.queued || t.extra['waiting'] == true
|
||||||
? tr('대기 중')
|
? tr('대기 중')
|
||||||
: stalled
|
: stalled
|
||||||
? tr('멈춤')
|
// 관리자 지시 (10/20, 28 지적): 영어 "Stopped" 는 일시 정지 · 취소로 읽혀 따로 열쇠 ("Stalled")
|
||||||
|
? tr('멈춤 (시작 안 됨)')
|
||||||
: tr('준비 중…'))
|
: tr('준비 중…'))
|
||||||
: '${(value * 100).toStringAsFixed(1)}%${size.isEmpty ? '' : ' · $size'}';
|
: '${(value * 100).toStringAsFixed(1)}%${size.isEmpty ? '' : ' · $size'}';
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
|
|
|
||||||
|
|
@ -143,4 +143,30 @@ void main() {
|
||||||
expect(Cleaner.sizeText(5 * 1024 * 1024), '5.0 MB');
|
expect(Cleaner.sizeText(5 * 1024 * 1024), '5.0 MB');
|
||||||
expect(Cleaner.sizeText(3 * 1024 * 1024 * 1024), '3.0 GB');
|
expect(Cleaner.sizeText(3 * 1024 * 1024 * 1024), '3.0 GB');
|
||||||
});
|
});
|
||||||
|
test('관리자 지시 (10/20): 앱이 켜질 때 지난 업데이트가 남긴 것 (풀어 둔 새 판 · .ps1 · .log) 을 지운다 - 막 생긴 것 · 다른 것은 남김', () {
|
||||||
|
final tmp = Directory.systemTemp.createTempSync('jj_upd_left_');
|
||||||
|
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||||
|
final old = DateTime.now().subtract(const Duration(hours: 1));
|
||||||
|
void make(String rel, {bool fresh = false}) {
|
||||||
|
final f = File(p.join(tmp.path, rel))..createSync(recursive: true);
|
||||||
|
if (!fresh) f.setLastModifiedSync(old);
|
||||||
|
}
|
||||||
|
|
||||||
|
make(p.join('jj_mkvmaker_update_2026.10.18_979', 'extract', 'jj_mkvmaker.exe'));
|
||||||
|
make('jj_mkvmaker_update.ps1');
|
||||||
|
make('jj_mkvmaker_update.log', fresh: true); // 교체 스크립트가 막 쓴 것
|
||||||
|
make('jj_mkvmaker_updater.txt');
|
||||||
|
make(p.join('jj_mkvmaker', 'a.wav'));
|
||||||
|
// 폴더는 안에 파일을 만들어 바뀐 때가 지금이다 - 5분 뒤로 보고 지우는지 본다
|
||||||
|
final dir = Directory(p.join(tmp.path, 'jj_mkvmaker_update_2026.10.18_979'));
|
||||||
|
final gone = removeUpdateLeftovers(tmp.path, now: DateTime.now().add(const Duration(minutes: 5)));
|
||||||
|
expect(gone..sort(), ['jj_mkvmaker_update.log', 'jj_mkvmaker_update.ps1', 'jj_mkvmaker_update_2026.10.18_979'],
|
||||||
|
reason: '5분 뒤로 보면 막 생긴 기록도 2분이 지남');
|
||||||
|
expect(dir.existsSync(), isFalse);
|
||||||
|
expect(File(p.join(tmp.path, 'jj_mkvmaker_updater.txt')).existsSync(), isTrue);
|
||||||
|
expect(Directory(p.join(tmp.path, 'jj_mkvmaker')).existsSync(), isTrue);
|
||||||
|
|
||||||
|
make('jj_mkvmaker_update.log', fresh: true);
|
||||||
|
expect(removeUpdateLeftovers(tmp.path), isEmpty, reason: '2분이 안 된 것은 남김 (스크립트가 끝나는 중일 수 있음)');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@ void main() {
|
||||||
d.selectNone();
|
d.selectNone();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.textContaining('트래커 · 피어에 닿지 않습니다'), findsOneWidget);
|
expect(find.textContaining('트래커 · 피어에 닿지 않습니다'), findsOneWidget);
|
||||||
expect(find.text('멈춤'), findsOneWidget);
|
expect(find.text('멈춤 (시작 안 됨)'), findsOneWidget);
|
||||||
expect(find.text('준비 중…'), findsNothing);
|
expect(find.text('준비 중…'), findsNothing);
|
||||||
expect(bar().value, 0, reason: '움직이지 않는 막대');
|
expect(bar().value, 0, reason: '움직이지 않는 막대');
|
||||||
expect(bar().color, Colors.orangeAccent.withValues(alpha: 0.85));
|
expect(bar().color, Colors.orangeAccent.withValues(alpha: 0.85));
|
||||||
|
|
|
||||||
|
|
@ -1429,6 +1429,8 @@ void main() {
|
||||||
..explorerDropAction = 'move'
|
..explorerDropAction = 'move'
|
||||||
..copyConflict = 'overwrite';
|
..copyConflict = 'overwrite';
|
||||||
await open(tester);
|
await open(tester);
|
||||||
|
// 전체 시험 중 (부하) 에는 목록을 늦게 읽어 e.txt 를 못 찾았다 - 두 파일이 보일 때까지
|
||||||
|
await settle(tester, () => find.text('e.txt').evaluate().isNotEmpty && find.text('doc.txt').evaluate().isNotEmpty);
|
||||||
await act(tester, () => tester.tap(find.text('선택').first));
|
await act(tester, () => tester.tap(find.text('선택').first));
|
||||||
await act(tester, () => tester.tap(markOf('doc.txt')));
|
await act(tester, () => tester.tap(markOf('doc.txt')));
|
||||||
await act(tester, () => tester.tap(markOf('e.txt')));
|
await act(tester, () => tester.tap(markOf('e.txt')));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||||
|
|
||||||
|
/// 모든 단위 시험 파일 앞에 돈다 (flutter_test 의 flutter_test_config).
|
||||||
|
/// 관리자 지시 (10/20): 앱 임시 폴더 ([DesktopStorageService.tempDirectory] = %TEMP%\jj_mkvmaker) 를 시험이 남겼다.
|
||||||
|
/// 시험 파일들은 나란히 돌아 같은 폴더를 끝에 지우면 서로 부딪치므로, 파일마다 따로 jj_store_… 를 주고 그 파일이 끝나면 지운다
|
||||||
|
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
|
||||||
|
final root = Directory.systemTemp.createTempSync('jj_store_');
|
||||||
|
DesktopStorageService.tempRoot = root.path;
|
||||||
|
tearDownAll(() {
|
||||||
|
try {
|
||||||
|
root.deleteSync(recursive: true);
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
|
await testMain();
|
||||||
|
}
|
||||||
|
|
@ -150,7 +150,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('최신 확인 → 받기 · SHA256 확인 · 압축 풀기', () async {
|
test('최신 확인 → 받기 · SHA256 확인 · 압축 풀기', () async {
|
||||||
final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: tmp.path, versionOverride: '1.0.1');
|
final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: tmp.path, versionOverride: '1.0.1', tempRoot: tmp.path);
|
||||||
final r = (await up.latest())!;
|
final r = (await up.latest())!;
|
||||||
expect(r.isNewerThan(await up.currentVersion()), isTrue);
|
expect(r.isNewerThan(await up.currentVersion()), isTrue);
|
||||||
final progress = <double>[];
|
final progress = <double>[];
|
||||||
|
|
@ -160,7 +160,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SHA256 이 다르면 설치 거부', () async {
|
test('SHA256 이 다르면 설치 거부', () async {
|
||||||
final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: tmp.path);
|
final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: tmp.path, tempRoot: tmp.path);
|
||||||
final good = (await up.latest())!;
|
final good = (await up.latest())!;
|
||||||
final bad = ReleaseInfo(
|
final bad = ReleaseInfo(
|
||||||
version: good.version, tag: good.tag, name: '', notes: '', pageUrl: '',
|
version: good.version, tag: good.tag, name: '', notes: '', pageUrl: '',
|
||||||
|
|
@ -178,7 +178,7 @@ void main() {
|
||||||
Directory(p.join(app.path, 'models')).createSync();
|
Directory(p.join(app.path, 'models')).createSync();
|
||||||
File(p.join(app.path, 'models', 'm.bin')).writeAsStringSync('model');
|
File(p.join(app.path, 'models', 'm.bin')).writeAsStringSync('model');
|
||||||
|
|
||||||
final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: app.path);
|
final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: app.path, tempRoot: tmp.path);
|
||||||
final dir = await up.download((await up.latest())!, (_) {});
|
final dir = await up.download((await up.latest())!, (_) {});
|
||||||
// 이미 끝난 프로세스를 "앱" 으로 두고 실행 (바로 교체 진행)
|
// 이미 끝난 프로세스를 "앱" 으로 두고 실행 (바로 교체 진행)
|
||||||
final dummy = await Process.start('cmd', ['/c', 'exit']);
|
final dummy = await Process.start('cmd', ['/c', 'exit']);
|
||||||
|
|
@ -192,6 +192,9 @@ void main() {
|
||||||
expect(File(p.join(app.path, 'old_only.txt')).readAsStringSync(), 'keep me');
|
expect(File(p.join(app.path, 'old_only.txt')).readAsStringSync(), 'keep me');
|
||||||
expect(File(p.join(app.path, 'jj_yt-dlp', 'video.mp4')).readAsStringSync(), 'my download');
|
expect(File(p.join(app.path, 'jj_yt-dlp', 'video.mp4')).readAsStringSync(), 'my download');
|
||||||
expect(File(p.join(app.path, 'models', 'm.bin')).readAsStringSync(), 'model');
|
expect(File(p.join(app.path, 'models', 'm.bin')).readAsStringSync(), 'model');
|
||||||
|
// 기록 · 스크립트는 스크립트 옆 (실제로는 %TEMP%, 시험은 시험 폴더 - 시험이 %TEMP% 에 남기지 않음)
|
||||||
|
expect(File(p.join(tmp.path, 'jj_mkvmaker_update.log')).existsSync(), isTrue);
|
||||||
|
expect(File(p.join(tmp.path, 'jj_mkvmaker_update.ps1')).existsSync(), isTrue);
|
||||||
}, timeout: const Timeout(Duration(minutes: 2)));
|
}, timeout: const Timeout(Duration(minutes: 2)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,22 @@
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
here = os.path.dirname(os.path.abspath(__file__))
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
out = os.path.join(tempfile.gettempdir(), 'jj_l10n_keys.json')
|
# 관리자 지시 (10/20): 예전에는 %TEMP% 에 jj_l10n_keys.json · jj_l10n_unwrapped.json 을 남겼다 - 임시 폴더를 만들고 끝에 지운다
|
||||||
out2 = os.path.join(tempfile.gettempdir(), 'jj_l10n_unwrapped.json')
|
work = tempfile.mkdtemp(prefix='jj_l10n_check_')
|
||||||
subprocess.run([sys.executable, os.path.join(here, 'extract_keys.py'), out, out2], check=True)
|
try:
|
||||||
keys = json.load(open(out, encoding='utf-8'))
|
out = os.path.join(work, 'keys.json')
|
||||||
|
out2 = os.path.join(work, 'unwrapped.json')
|
||||||
|
subprocess.run([sys.executable, os.path.join(here, 'extract_keys.py'), out, out2], check=True)
|
||||||
|
keys = json.load(open(out, encoding='utf-8'))
|
||||||
|
unwrapped = json.load(open(out2, encoding='utf-8'))
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(work, ignore_errors=True)
|
||||||
missing = 0
|
missing = 0
|
||||||
for lang in ['en', 'ja', 'zh-Hans']:
|
for lang in ['en', 'ja', 'zh-Hans']:
|
||||||
d = json.load(open(os.path.join('assets', 'l10n', lang + '.json'), encoding='utf-8'))
|
d = json.load(open(os.path.join('assets', 'l10n', lang + '.json'), encoding='utf-8'))
|
||||||
|
|
@ -26,7 +33,7 @@ for lang in ['en', 'ja', 'zh-Hans']:
|
||||||
print('%s 사전에 없음: %r' % (lang, k))
|
print('%s 사전에 없음: %r' % (lang, k))
|
||||||
# 60: tr 로 감싸지 않은 한글 글. 사전에 있으면 tr(변수) 로 번역되는 것으로 본다 - 없으면 번역이 빠진 것
|
# 60: tr 로 감싸지 않은 한글 글. 사전에 있으면 tr(변수) 로 번역되는 것으로 본다 - 없으면 번역이 빠진 것
|
||||||
en = json.load(open(os.path.join('assets', 'l10n', 'en.json'), encoding='utf-8'))
|
en = json.load(open(os.path.join('assets', 'l10n', 'en.json'), encoding='utf-8'))
|
||||||
loose = [(p, n, t) for p, n, t in json.load(open(out2, encoding='utf-8')) if t not in en]
|
loose = [(p, n, t) for p, n, t in unwrapped if t not in en]
|
||||||
for p, n, t in loose:
|
for p, n, t in loose:
|
||||||
print('번역 안 된 글 (tr 로 감싸거나 사전에 넣기, 자료면 // l10n-skip): %s:%d %r' % (p, n, t[:80]))
|
print('번역 안 된 글 (tr 로 감싸거나 사전에 넣기, 자료면 // l10n-skip): %s:%d %r' % (p, n, t[:80]))
|
||||||
missing += len(loose)
|
missing += len(loose)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
# - 앱 코드 (lib) 가 시험 중에 만든 임시 폴더 (jj_up_ · jj_7z_ 등) 도 같은 곳에 생기므로 함께 잡힌다.
|
# - 앱 코드 (lib) 가 시험 중에 만든 임시 폴더 (jj_up_ · jj_7z_ 등) 도 같은 곳에 생기므로 함께 잡힌다.
|
||||||
# - 표는 환경 변수 JJ_TEST_TMP_TAG 로도 시험에 넘긴다: 시험 도우미 (test/support/temp_dirs.dart · integration_test/support/temp_dir.dart)
|
# - 표는 환경 변수 JJ_TEST_TMP_TAG 로도 시험에 넘긴다: 시험 도우미 (test/support/temp_dirs.dart · integration_test/support/temp_dir.dart)
|
||||||
# 가 임시 폴더가 그 안에 생기는지 확인한다 (통합 시험 -d windows 에서 앱까지 환경 변수가 가는지).
|
# 가 임시 폴더가 그 안에 생기는지 확인한다 (통합 시험 -d windows 에서 앱까지 환경 변수가 가는지).
|
||||||
# - 끝나면 남은 것을 보이고 실행 폴더를 통째로 지운다. 실패 (exit 2) 는 시험 소스가 만드는 이름 (createTemp('jj_x_') 등) 의 폴더가 남았을 때만
|
# - 끝나면 남은 것을 보이고 실행 폴더를 통째로 지운다. jj 로 시작하는 것 (폴더 · 파일, 앱 고정 이름 포함) 이 남으면 실패 (exit 2)
|
||||||
# (앱 코드의 고정 이름 jj_mkvmaker · 파일 · 앱이 끝날 때까지 잡힌 jj_held_ 는 참고로만).
|
# (앱이 끝날 때까지 잡힌 jj_held_ 와 jj 가 아닌 것은 참고로만).
|
||||||
# 사용 (app 폴더에서): powershell -ExecutionPolicy Bypass -File tool\test_temp_check.ps1 [flutter test 인자…]
|
# 사용 (app 폴더에서): powershell -ExecutionPolicy Bypass -File tool\test_temp_check.ps1 [flutter test 인자…]
|
||||||
# 통합 시험 · 캡처도 (관리자 지시 10/19, 28 이 찾음): … test_temp_check.ps1 integration_test\windows_b6_shot_test.dart -d windows
|
# 통합 시험 · 캡처도 (관리자 지시 10/19, 28 이 찾음): … test_temp_check.ps1 integration_test\windows_b6_shot_test.dart -d windows
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
@ -38,19 +38,13 @@ Start-Sleep -Seconds 3
|
||||||
$items = @(Get-ChildItem -LiteralPath $run -Force -ErrorAction SilentlyContinue)
|
$items = @(Get-ChildItem -LiteralPath $run -Force -ErrorAction SilentlyContinue)
|
||||||
# jj_held_ 는 앱이 끝날 때까지 놓지 않는 폴더 (integration_test/support/temp_dir.dart 의 heldByApp - Edge 사용자 데이터) - 보이기만 하고 아래에서 함께 지운다
|
# jj_held_ 는 앱이 끝날 때까지 놓지 않는 폴더 (integration_test/support/temp_dir.dart 의 heldByApp - Edge 사용자 데이터) - 보이기만 하고 아래에서 함께 지운다
|
||||||
$held = @($items | Where-Object { $_.Name -like 'jj_held_*' } | ForEach-Object { $_.Name })
|
$held = @($items | Where-Object { $_.Name -like 'jj_held_*' } | ForEach-Object { $_.Name })
|
||||||
# 실패로 세는 것: 시험 소스가 만드는 이름 (createTemp('jj_x_') · testTempDir('jj_x_') · tempDirUntilEnd('jj_x_')) 의 폴더 (예전 검사와 같은 기준)
|
# 실패로 세는 것: jj 로 시작하는 것 모두 (폴더 · 파일 - 시험 소스 이름 · 앱 코드의 고정 이름 jj_mkvmaker · jj_mkvmaker_update_* 등).
|
||||||
$prefixes = @(@('test', 'integration_test') | ForEach-Object { Join-Path $app $_ } | Where-Object { Test-Path $_ } |
|
# 관리자 판단 (10/20): 앱 고정 이름도 사용자 PC 의 실제 %TEMP% 에 쌓이던 것이라 새면 잡는다
|
||||||
ForEach-Object { Get-ChildItem $_ -Recurse -Filter *.dart } |
|
$left = @($items | Where-Object { $_.Name -like 'jj*' -and $_.Name -notlike 'jj_held_*' } | ForEach-Object { $_.Name })
|
||||||
ForEach-Object { [regex]::Matches((Get-Content $_.FullName -Raw -Encoding utf8), '(?:createTemp(?:Sync)?|testTempDir|tempDirUntilEnd)\(''(jj[^''$]*)''') } |
|
# 참고: jj 가 아닌 것 (flutter 도구 · MSBuild 등 - 실패로 세지 않고 보이기만, 실행 폴더와 함께 지움)
|
||||||
ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique)
|
$other = @($items | Where-Object { $_.Name -notlike 'jj*' } | ForEach-Object { $_.Name })
|
||||||
$left = @($items | Where-Object { $_.PSIsContainer -and $_.Name -notlike 'jj_held_*' } | Where-Object {
|
|
||||||
$n = $_.Name
|
|
||||||
@($prefixes | Where-Object { $n.StartsWith($_) }).Count -gt 0
|
|
||||||
} | ForEach-Object { $_.Name })
|
|
||||||
# 참고: 그 밖의 것 (flutter 도구 · 앱 코드의 고정 이름 jj_mkvmaker 등 · 파일 - 실패로 세지 않고 보이기만, 실행 폴더와 함께 지움)
|
|
||||||
$other = @($items | Where-Object { $_.Name -notlike 'jj_held_*' -and $left -notcontains $_.Name } | ForEach-Object { $_.Name })
|
|
||||||
if ($other.Count -gt 0) {
|
if ($other.Count -gt 0) {
|
||||||
Write-Host ("참고 - 시험 이름이 아닌 남은 것 {0}개 (실행 폴더와 함께 지움):" -f $other.Count)
|
Write-Host ("참고 - jj 가 아닌 남은 것 {0}개 (실행 폴더와 함께 지움):" -f $other.Count)
|
||||||
$other | Sort-Object | ForEach-Object { Write-Host " $_" }
|
$other | Sort-Object | ForEach-Object { Write-Host " $_" }
|
||||||
}
|
}
|
||||||
if ($held.Count -gt 0) {
|
if ($held.Count -gt 0) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue