W DangerDialog: 확인 창 두 벌 (confirm.dart · confirm_dialog.dart) 을 confirmDialog 하나로 + 체크해야 켜짐 · 첫 초점 [취소] 를 공용으로
- confirm_dialog.dart 하나: confirmAction (글만 묻는 꼴) 은 confirmDialog 를 부른다 (confirm.dart 지움, 16 파일 import 바꿈). 빨강은 테마 위험 색 (danger) 하나로 - 지우기 · 덮어쓰기 확인의 빨강이 다른 지우기 단추와 같아짐 (FF5252 → FF5C6C) - 새 약속: cancelFirst (빨강이 아니어도 되돌리기 어려운 일은 첫 초점 [취소] - Enter 로 실행 안 됨) · canConfirm (체크 · 두 비밀번호가 같아야 [확인] 켜짐). 시스템 폴더 · 루트 보기 켜기 (설정), 루트 탭 경고 (탐색기), 보관함 비밀번호 바꾸기가 부르기만 한다. 창마다 글 · 단추 이름은 그대로 - confirmAction 도 같은 규칙: danger 면 첫 초점 [취소], 아니면 [확인] (관리자 결정 10/22) - 시험: confirm_dialog_contract_test 에 cancelFirst · canConfirm · confirmAction 3개 (관리자 10/25 조건) - 주입: LiveSync(now:) (처음 값 실제 시계 - 카드의 "마지막: HH:MM"). 시험 쪽 고정: b3 (폴더 · 시각 · CPU · 디스크) · lsync_p0 (폴더 · 시각 · 대상 디스크). b3_52 의 받는 중 파일 (.jjpart) 시각은 실제 복사가 만든 때라 그대로 (흔들림 2장 남음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
d1081f337e
commit
5a65dbf693
|
|
@ -7,13 +7,13 @@ import 'package:flutter/rendering.dart';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/theme.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../test/support/fake_recycle_bin.dart';
|
||||
import '../test/support/recycle_leftovers.dart';
|
||||
import 'support/shot_services.dart';
|
||||
import 'support/temp_dir.dart';
|
||||
import '../test/support/test_explorer_device.dart';
|
||||
|
||||
|
|
@ -25,10 +25,8 @@ void main() {
|
|||
setUp(() => device = TestExplorerDevice());
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_b3');
|
||||
Directory tempDir(String prefix) {
|
||||
final base = Platform.environment['JJ_SHOT_TMP'];
|
||||
return testTempDir(prefix, base: base);
|
||||
}
|
||||
// 관리자 지시 (10/24): 실행마다 다른 값 (폴더 이름 · 시각 · CPU · 메모리 · 디스크) 을 정한 값으로 (캡처가 흔들리지 않게 - 시험 쪽에서만)
|
||||
Directory tempDir(String name) => testFixedTempDir(name, base: Platform.environment['JJ_SHOT_TMP']);
|
||||
|
||||
final key = GlobalKey();
|
||||
|
||||
|
|
@ -69,7 +67,7 @@ void main() {
|
|||
late String left, right;
|
||||
late AppController c;
|
||||
setUp(() {
|
||||
tmp = tempDir('jj_shot_b3_');
|
||||
tmp = tempDir('jj_shot_b3_fixed');
|
||||
left = p.join(tmp.path, 'left');
|
||||
right = p.join(tmp.path, 'right');
|
||||
File(p.join(left, 'doc.txt'))
|
||||
|
|
@ -78,7 +76,9 @@ void main() {
|
|||
File(p.join(right, 'doc.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('old');
|
||||
c = AppController(PlatformServices.create());
|
||||
pinTestTimes(tmp);
|
||||
c = AppController(shotServices());
|
||||
device.space = (_) => (107374182400, 536870912000);
|
||||
c.settings.explorer.paths = [left, right];
|
||||
c.settings.explorer.style = 'xplore'; // 이 캡처는 X-plore 모양 (E6 부터 Windows 처음 값은 Total Commander - 이름 · 확장자가 다른 칸)
|
||||
// 휴지통: 기본은 가짜 (화면 흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통
|
||||
|
|
@ -150,6 +150,7 @@ void main() {
|
|||
testWidgets('52: 복사 중에 다른 화면에 갔다 와도 진행 막대', (t) async {
|
||||
// 다른 화면에 갔다 와도 아직 복사 중이게 (약 25초)
|
||||
File(p.join(left, 'big.bin')).writeAsBytesSync(List.filled(2 * 1024 * 1024, 7));
|
||||
pinTestTimes(tmp); // 오른쪽에 생기는 받는 중 파일 (.jjpart) 의 시각은 실제 복사가 만든 때라 그대로 둔다
|
||||
c.settings.copy.bandwidthKBps = 80;
|
||||
await show(t, ExplorerPage(device: device, c: c));
|
||||
await selectAndPress(t, 'big.bin', '복사');
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ void main() {
|
|||
addTearDown(t.view.reset);
|
||||
await t.runAsync(() => i18n.apply(lang, save: false));
|
||||
addTearDown(() => i18n.apply('ko', save: false));
|
||||
final tmp = testTempDir('jj_lsyncp0_shot_');
|
||||
// 관리자 지시 (10/24): 실행마다 다른 값 (폴더 이름 · 시각 · CPU · 메모리 · 디스크) 을 정한 값으로 (캡처가 흔들리지 않게 - 시험 쪽에서만)
|
||||
final tmp = testFixedTempDir('jj_lsyncp0_shot_fixed');
|
||||
final src = p.join(tmp.path, 's'), dst = p.join(tmp.path, 'd');
|
||||
for (final (dir, rel) in [(src, 'a.txt'), (src, 'b.txt'), (dst, 'a.txt'), (dst, 'old.log'), (dst, 'extra.txt')]) {
|
||||
File(p.join(dir, rel))
|
||||
|
|
@ -45,13 +46,14 @@ void main() {
|
|||
..writeAsStringSync('$dir/$rel');
|
||||
}
|
||||
File(p.join(tmp.path, 'rules.txt')).writeAsStringSync('H a.txt\n');
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final c = AppController(
|
||||
PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), diskSpace: (_) async => (107374182400, 536870912000)));
|
||||
c.settings
|
||||
..copy.rsyncSource = 'custom'
|
||||
..copy.rsyncPath = rsync
|
||||
..copy.rsyncOptions = '-a --filter="merge ${toCygwinPath(p.join(tmp.path, 'rules.txt'))}"'
|
||||
..copy.liveSyncPairs = [LiveSyncPair(src, dst, method: 'rsync', delete: true, deleteConfirmed: true)];
|
||||
final live = LiveSync(c)..start();
|
||||
final live = LiveSync(c, now: () => DateTime(2026, 10, 22, 9))..start();
|
||||
addTearDown(live.dispose);
|
||||
final pair = c.settings.copy.liveSyncPairs.single;
|
||||
await t.runAsync(() => live.syncNow(pair));
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ export 'sync/sync_commands.dart' show bundledRsync, findBundledRsync, rsyncExecu
|
|||
/// 앱이 켜져 있는 동안만 동작하고, 시작할 때 한 번 맞춘다.
|
||||
class LiveSync extends ChangeNotifier {
|
||||
final AppController c;
|
||||
LiveSync(this.c);
|
||||
|
||||
/// 지금 시각 (처음 값 = 실제 시계). 카드의 "마지막: HH:MM" 에 남기는 때 - 캡처 시험이 정한 시각을 준다
|
||||
final DateTime Function() now;
|
||||
LiveSync(this.c, {DateTime Function()? now}) : now = now ?? DateTime.now;
|
||||
|
||||
static LiveSync? instance;
|
||||
|
||||
|
|
@ -271,12 +274,12 @@ class LiveSync extends ChangeNotifier {
|
|||
for (final x in pairs) {
|
||||
final k = keyOf(x);
|
||||
if (!isDav(x.source) && !Directory(x.source).existsSync()) {
|
||||
status[k] = (DateTime.now(), tr('원본 폴더가 없습니다'));
|
||||
status[k] = (now(), tr('원본 폴더가 없습니다'));
|
||||
continue;
|
||||
}
|
||||
// 대상이 원본 안이면 맞출 때마다 원본이 바뀌어 끝없이 돈다. 34: 원본이 대상 안 (안쪽 → 바깥) 은 지우기가 없을 때만
|
||||
if (nestingProblem(x, allowInnerToOuter: s.general.allowInnerToOuter) case final nest?) {
|
||||
status[k] = (DateTime.now(), nest);
|
||||
status[k] = (now(), nest);
|
||||
continue;
|
||||
}
|
||||
if (Platform.isWindows && !isDav(x.source)) {
|
||||
|
|
@ -333,7 +336,7 @@ class LiveSync extends ChangeNotifier {
|
|||
? optionProblemMessage(bad)
|
||||
: nestingProblem(x, allowInnerToOuter: c.settings.general.allowInnerToOuter);
|
||||
if (nest != null) {
|
||||
status[k] = (DateTime.now(), nest);
|
||||
status[k] = (now(), nest);
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
|
@ -400,10 +403,10 @@ class LiveSync extends ChangeNotifier {
|
|||
CopyMethod.robocopy => await _robocopy(x, s),
|
||||
};
|
||||
status[k] = toDelete[k] != null
|
||||
? (DateTime.now(), trf('지우지 않고 맞춤 · 대상에만 있는 {0}개를 지울지 확인 필요', [toDelete[k]!.length]))
|
||||
? (now(), trf('지우지 않고 맞춤 · 대상에만 있는 {0}개를 지울지 확인 필요', [toDelete[k]!.length]))
|
||||
: ops.dirConflicts.isNotEmpty
|
||||
? (DateTime.now(), trf('{0}개 맞춤 · 같은 이름의 폴더가 있어 건너뜀 {1}개', [n < 0 ? 0 : n, ops.dirConflicts.length]))
|
||||
: (DateTime.now(), n < 0 ? tr('맞춤') : trf('{0}개 맞춤', [n]));
|
||||
? (now(), trf('{0}개 맞춤 · 같은 이름의 폴더가 있어 건너뜀 {1}개', [n < 0 ? 0 : n, ops.dirConflicts.length]))
|
||||
: (now(), n < 0 ? tr('맞춤') : trf('{0}개 맞춤', [n]));
|
||||
problems.remove(k);
|
||||
emptyDeletes.remove(k);
|
||||
pending[k] = await SyncDiff.of(_pendingPair(asked), filter: _filterOf(asked));
|
||||
|
|
@ -413,7 +416,7 @@ class LiveSync extends ChangeNotifier {
|
|||
// 68 · 70: 대상은 건드리지 않고 멈춤. 카드에 빨갛게 · 작업 알림에 · 로그에
|
||||
problems[k] = e;
|
||||
status[k] = (
|
||||
DateTime.now(),
|
||||
now(),
|
||||
e.locked
|
||||
? tr('마스터 비밀번호가 필요해 기다리는 중')
|
||||
: e.empty
|
||||
|
|
@ -430,7 +433,7 @@ class LiveSync extends ChangeNotifier {
|
|||
c.note(trf('실시간 동기화 멈춤: {0} → {1}: {2}', [x.source, x.target, e]));
|
||||
} catch (e) {
|
||||
// F18: 화면에는 화면 언어의 짧은 글 (예외 이름 · 영어 파일 오류 없이), 작업 기록에는 원문 그대로 (아래)
|
||||
status[k] = (DateTime.now(), trf('실패: {0}', [userErrorText(e, sources: [x.source])]));
|
||||
status[k] = (now(), trf('실패: {0}', [userErrorText(e, sources: [x.source])]));
|
||||
c.note(trf('실시간 동기화 실패: {0} → {1}: {2}', [x.source, x.target, e]));
|
||||
} finally {
|
||||
_running.remove(k);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import '../core/secret_gate.dart';
|
|||
import '../l10n/tr.dart';
|
||||
import '../services/image_ai.dart';
|
||||
import 'ai_image_page.dart' show aiSaveDir;
|
||||
import 'confirm.dart';
|
||||
import 'confirm_dialog.dart';
|
||||
import 'folder_picker.dart';
|
||||
import 'license_texts.dart' show showLicenseDocument;
|
||||
import 'setting_tile.dart';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import '../app/ai_local.dart' show AiStore;
|
|||
import '../app/app_controller.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import '../services/model_store.dart';
|
||||
import 'confirm.dart';
|
||||
import 'confirm_dialog.dart';
|
||||
import 'setting_tile.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import '../core/reader_sources.dart';
|
|||
import '../core/sd_cli.dart' show ImageGenRequest;
|
||||
import '../l10n/tr.dart';
|
||||
import '../services/image_ai.dart' show ImageAiException;
|
||||
import 'confirm.dart';
|
||||
import 'confirm_dialog.dart';
|
||||
import 'explorer_page.dart' show ExplorerPage;
|
||||
import 'theme.dart';
|
||||
import 'notice.dart';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import '../../app/app_controller.dart';
|
|||
import '../../app/download_manager.dart';
|
||||
import '../../l10n/tr.dart';
|
||||
import '../../services/downloader.dart' show DownloadState;
|
||||
import '../confirm.dart';
|
||||
import '../confirm_dialog.dart';
|
||||
import '../downloads_page.dart';
|
||||
import '../notice.dart';
|
||||
import '../settings_page.dart';
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../l10n/tr.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// 되돌리기 어려운 일 전에 한 번 묻는다 (46 · 47 등). [danger] 면 확인 버튼이 빨강. 확인하면 true.
|
||||
/// [cancel]: 하지 않는 쪽 버튼 이름 (처음 값 "취소" - 확인 버튼이 "… 취소" 면 헷갈리므로 바꾼다, 185)
|
||||
Future<bool> confirmAction(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String body,
|
||||
required String ok,
|
||||
bool danger = true,
|
||||
String? cancel,
|
||||
}) async {
|
||||
final r = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(title),
|
||||
content: Text(body),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(cancel ?? tr('취소'))),
|
||||
FilledButton(
|
||||
style: danger ? FilledButton.styleFrom(backgroundColor: JjColors.danger) : null,
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(ok),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return r == true;
|
||||
}
|
||||
|
|
@ -9,8 +9,11 @@ import 'theme.dart';
|
|||
/// - 돌려주는 값: [확인] 을 누른 때만 true. [취소] · Esc · 바깥 누르기 · 뒤로는 false.
|
||||
/// - Enter = 첫 초점 단추. 되돌릴 수 없는 일 ([danger] 가 창을 열 때 true) 은 첫 초점이 [취소] (실수 막기),
|
||||
/// 그 밖은 [확인] (관리자 결정 10/22).
|
||||
/// - [cancelFirst] 를 주면 첫 초점을 따로 정한다 (빨강이 아니어도 되돌리기 어려운 일 - 루트 · 시스템 폴더 · 보관함 비밀번호 바꾸기는 true).
|
||||
/// - [canConfirm] 이 false 인 동안 [확인] 은 꺼져 있다 (체크해야 켜짐 · 두 비밀번호가 같아야 켜짐 - 창 안에서 [set] 으로 다시 봄).
|
||||
/// - [danger] 가 창 안에서 바뀌면 (예: "영구 삭제" 체크) [확인] 단추만 빨강으로 바뀐다. 첫 초점은 연 때 그대로.
|
||||
/// - [title] · [body] · [confirmLabel] 은 창 안에서 [set] 으로 다시 그려진다 (체크 상자 등).
|
||||
/// - 빨강은 테마의 위험 색 (JjColors.danger - 다른 지우기 단추와 같음). 글만 묻는 곳은 [confirmAction] (같은 창).
|
||||
Future<bool> confirmDialog(
|
||||
BuildContext context, {
|
||||
required String Function() title,
|
||||
|
|
@ -18,9 +21,11 @@ Future<bool> confirmDialog(
|
|||
required String Function() confirmLabel,
|
||||
String? cancelLabel,
|
||||
bool Function()? danger,
|
||||
bool? cancelFirst,
|
||||
bool Function()? canConfirm,
|
||||
Widget? icon,
|
||||
}) async {
|
||||
final dangerAtOpen = danger?.call() ?? false;
|
||||
final cancelAtOpen = cancelFirst ?? danger?.call() ?? false;
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
|
|
@ -33,14 +38,14 @@ Future<bool> confirmDialog(
|
|||
content: body(ctx, set),
|
||||
actions: [
|
||||
TextButton(
|
||||
autofocus: dangerAtOpen,
|
||||
autofocus: cancelAtOpen,
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(cancelLabel ?? tr('취소')),
|
||||
),
|
||||
FilledButton(
|
||||
autofocus: !dangerAtOpen,
|
||||
style: red ? FilledButton.styleFrom(backgroundColor: JjColors.alert) : null,
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
autofocus: !cancelAtOpen,
|
||||
style: red ? FilledButton.styleFrom(backgroundColor: JjColors.danger) : null,
|
||||
onPressed: canConfirm?.call() ?? true ? () => Navigator.pop(ctx, true) : null,
|
||||
child: Text(confirmLabel()),
|
||||
),
|
||||
],
|
||||
|
|
@ -50,3 +55,23 @@ Future<bool> confirmDialog(
|
|||
);
|
||||
return ok == true;
|
||||
}
|
||||
|
||||
/// 글만으로 묻는 확인 창 ([confirmDialog] 와 같은 창 - W DangerDialog 로 하나로 모음, 예전 confirm.dart).
|
||||
/// 되돌리기 어려운 일 전에 한 번 묻는다 (46 · 47 등). [danger] 면 [확인] 이 빨강 · 첫 초점은 [취소]. 확인하면 true.
|
||||
/// [cancel]: 하지 않는 쪽 단추 이름 (처음 값 "취소" - 확인 단추가 "… 취소" 면 헷갈리므로 바꾼다, 185)
|
||||
Future<bool> confirmAction(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String body,
|
||||
required String ok,
|
||||
bool danger = true,
|
||||
String? cancel,
|
||||
}) =>
|
||||
confirmDialog(
|
||||
context,
|
||||
title: () => title,
|
||||
body: (_, _) => Text(body),
|
||||
confirmLabel: () => ok,
|
||||
cancelLabel: cancel,
|
||||
danger: () => danger,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import '../core/sync_tools.dart';
|
|||
import '../core/vfs.dart' show vDisplay;
|
||||
import '../l10n/tr.dart';
|
||||
import '../platform/windows/rsync_installer.dart';
|
||||
import 'confirm.dart';
|
||||
import 'confirm_dialog.dart';
|
||||
import 'folder_picker.dart';
|
||||
import 'path_label.dart';
|
||||
import 'rsync_setup.dart';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import '../app/download_manager.dart';
|
|||
import '../core/download_detect.dart';
|
||||
import '../services/downloader.dart';
|
||||
import 'app_actions.dart';
|
||||
import 'confirm.dart';
|
||||
import 'confirm_dialog.dart';
|
||||
import 'theme.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'jj_header_row.dart';
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import '../../platform/android/android_storage.dart';
|
|||
import '../theme.dart';
|
||||
import 'explorer_controller.dart';
|
||||
import 'explorer_pane.dart';
|
||||
import '../confirm_dialog.dart';
|
||||
|
||||
/// 약속 (E5d-6): 저장 장치에 닿는 일 - Android "모든 파일에 대한 접근" 권한 ([allFiles] · 듀얼 앱, 앱으로 돌아오면 다시 확인),
|
||||
/// 시스템 폴더 (Shizuku) · 루트 (su) 탭 (이번 실행에서 처음이면 경고 → 켜져 있고 허락받았을 때만 연다, 읽기 전용),
|
||||
|
|
@ -84,21 +85,17 @@ class ExplorerAccess extends ChangeNotifier {
|
|||
/// X-plore 비교 5-2: 루트 탭 - 이번 실행에서 처음이면 경고, su 가 있고 허락을 받았을 때만 "/" 로 (관리자 3a 결정)
|
||||
Future<void> openRoot(BuildContext context, ExplorerPane pane) async {
|
||||
if (!rootWarned) {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
scrollable: true,
|
||||
icon: Icon(Icons.security, color: JjColors.warning),
|
||||
title: Text(tr('루트 폴더 보기 (읽기 전용)')),
|
||||
content: Text(tr('루트 (su) 권한으로 시스템 · 다른 앱의 폴더 전체를 봅니다. 다른 앱의 개인 자료 (대화 · 로그인 정보 · 비밀번호 등) 도 '
|
||||
'보일 수 있습니다. 이 앱은 읽기만 하고 (쓰기 · 지우기 · 이름 바꾸기 없음), 꺼낸 파일은 고른 곳에만 둡니다.')),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('알고 봅니다'))),
|
||||
],
|
||||
),
|
||||
// 공용 확인 창: 첫 초점 [취소] (Enter 로 바로 열리지 않음)
|
||||
final ok = await confirmDialog(
|
||||
context,
|
||||
icon: Icon(Icons.security, color: JjColors.warning),
|
||||
title: () => tr('루트 폴더 보기 (읽기 전용)'),
|
||||
body: (_, _) => Text(tr('루트 (su) 권한으로 시스템 · 다른 앱의 폴더 전체를 봅니다. 다른 앱의 개인 자료 (대화 · 로그인 정보 · 비밀번호 등) 도 '
|
||||
'보일 수 있습니다. 이 앱은 읽기만 하고 (쓰기 · 지우기 · 이름 바꾸기 없음), 꺼낸 파일은 고른 곳에만 둡니다.')),
|
||||
confirmLabel: () => tr('알고 봅니다'),
|
||||
cancelFirst: true,
|
||||
);
|
||||
if (ok != true || !context.mounted) return;
|
||||
if (!ok || !context.mounted) return;
|
||||
rootWarned = true;
|
||||
}
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import '../core/ftp.dart';
|
|||
import '../core/secret_gate.dart';
|
||||
import '../core/webdav.dart' show DavLockedException;
|
||||
import '../l10n/tr.dart';
|
||||
import 'confirm.dart';
|
||||
import 'confirm_dialog.dart';
|
||||
import 'setting_tile.dart';
|
||||
import 'theme.dart';
|
||||
import 'webdav_settings.dart' show davServerUsers;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import '../../app/app_controller.dart';
|
|||
import '../../core/models.dart';
|
||||
import '../../app/settings.dart' show MoveTarget;
|
||||
import '../folder_picker.dart';
|
||||
import '../confirm.dart';
|
||||
import '../confirm_dialog.dart';
|
||||
import '../../l10n/tr.dart';
|
||||
|
||||
/// 동영상을 이동 버튼의 폴더로 옮긴다 (세부 정보의 이동 버튼 · 세 번 누르기). [target] 이 없으면 첫 번째 버튼,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import '../../core/models.dart';
|
|||
import '../ai_dialog.dart';
|
||||
import '../player_page.dart';
|
||||
import '../subtitle_search_dialog.dart';
|
||||
import '../confirm.dart';
|
||||
import '../confirm_dialog.dart';
|
||||
import '../theme.dart';
|
||||
import '../../l10n/tr.dart';
|
||||
import 'move_to_target.dart';
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import '../app/settings.dart';
|
|||
import '../core/secret_gate.dart';
|
||||
import '../services/secret_store.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'confirm.dart';
|
||||
import 'confirm_dialog.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// 124: 마스터 비밀번호 입력 창.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import '../app/app_controller.dart';
|
|||
import '../app/master_lock.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'theme.dart';
|
||||
import 'confirm.dart';
|
||||
import 'confirm_dialog.dart';
|
||||
import 'master_prompt.dart';
|
||||
import 'setting_tile.dart';
|
||||
import 'vault_page.dart' show openVault;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import '../component_settings.dart';
|
|||
import '../remote_thumb_view.dart' show remoteThumbsOf;
|
||||
import '../hidden_items_dialog.dart';
|
||||
import '../../core/remote_idle.dart';
|
||||
import '../confirm_dialog.dart';
|
||||
|
||||
// 약속 (설정 B): 환경 설정의 분야 - 파일 탐색기. 바꾸면 바로 설정에 저장 (c.updateSettings)
|
||||
|
||||
|
|
@ -391,35 +392,27 @@ class _RemoteThumbCacheTileState extends State<RemoteThumbCacheTile> {
|
|||
}
|
||||
|
||||
/// X-plore 비교 5-2: 시스템 폴더 · 루트 보기를 켤 때 경고 (체크해야 [켜기]). 켜면 true
|
||||
Future<bool> confirmReadOnlyView(BuildContext context, {required IconData icon, required String title, required String body}) async {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
var sure = false;
|
||||
return StatefulBuilder(
|
||||
builder: (ctx, set) => AlertDialog(
|
||||
scrollable: true,
|
||||
icon: Icon(icon, color: JjColors.warning),
|
||||
title: Text(title),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(body),
|
||||
const SizedBox(height: 6),
|
||||
Text(tr('이 앱은 읽기만 합니다 - 쓰기 · 지우기 · 이름 바꾸기 · 권한 바꾸기는 하지 않습니다. 꺼낸 (복사한) 파일은 고른 곳에만 둡니다.')),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: sure,
|
||||
onChanged: (x) => set(() => sure = x ?? false),
|
||||
title: Text(tr('위 내용을 알고 켭니다')),
|
||||
),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||
FilledButton(onPressed: sure ? () => Navigator.pop(ctx, true) : null, child: Text(tr('켜기'))),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
/// 공용 확인 창 (confirmDialog): 첫 초점 [취소] (Enter 로 바로 켜지지 않음) · 체크 전에는 [켜기] 꺼짐
|
||||
Future<bool> confirmReadOnlyView(BuildContext context, {required IconData icon, required String title, required String body}) {
|
||||
var sure = false;
|
||||
return confirmDialog(
|
||||
context,
|
||||
icon: Icon(icon, color: JjColors.warning),
|
||||
title: () => title,
|
||||
body: (ctx, set) => Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(body),
|
||||
const SizedBox(height: 6),
|
||||
Text(tr('이 앱은 읽기만 합니다 - 쓰기 · 지우기 · 이름 바꾸기 · 권한 바꾸기는 하지 않습니다. 꺼낸 (복사한) 파일은 고른 곳에만 둡니다.')),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: sure,
|
||||
onChanged: (x) => set(() => sure = x ?? false),
|
||||
title: Text(tr('위 내용을 알고 켭니다')),
|
||||
),
|
||||
]),
|
||||
confirmLabel: () => tr('켜기'),
|
||||
cancelFirst: true,
|
||||
canConfirm: () => sure,
|
||||
);
|
||||
return ok == true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import '../core/secret_gate.dart';
|
|||
import '../core/sftp.dart';
|
||||
import '../core/webdav.dart' show DavLockedException;
|
||||
import '../l10n/tr.dart';
|
||||
import 'confirm.dart';
|
||||
import 'confirm_dialog.dart';
|
||||
import 'setting_tile.dart';
|
||||
import 'theme.dart';
|
||||
import 'webdav_settings.dart' show davServerUsers;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import '../core/mdns_smb.dart';
|
|||
import '../core/secret_gate.dart';
|
||||
import '../core/smb.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'confirm.dart';
|
||||
import 'confirm_dialog.dart';
|
||||
import 'setting_tile.dart';
|
||||
import 'theme.dart';
|
||||
import 'webdav_settings.dart' show davServerUsers;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import '../core/app_trash.dart';
|
||||
import '../core/file_ops.dart' show formatSize;
|
||||
import '../l10n/tr.dart';
|
||||
import 'confirm.dart';
|
||||
import 'confirm_dialog.dart';
|
||||
import 'theme.dart';
|
||||
import 'jj_title_bar.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import '../../core/vault_store.dart';
|
|||
import '../../core/vault_view.dart';
|
||||
import '../../core/vfs.dart' show vDisplay;
|
||||
import '../../l10n/tr.dart';
|
||||
import '../confirm.dart' show confirmAction;
|
||||
import '../confirm_dialog.dart' show confirmAction;
|
||||
import '../player_page.dart';
|
||||
import '../reader_page.dart' show openReader;
|
||||
import '../theme.dart';
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import '../../l10n/tr.dart';
|
|||
import '../tail_text.dart';
|
||||
import '../theme.dart';
|
||||
import 'vault_session.dart';
|
||||
import '../confirm_dialog.dart';
|
||||
|
||||
/// 약속 (보관함 나누기): 잠긴 보관함 화면의 두 칸 - 새 보관함 만들기 [VaultCreateForm] · 열기 [VaultUnlockForm].
|
||||
/// 글자 칸 ([TextEditingController]) 과 값은 화면이 갖고, 이 모듈은 그리기만 한다 (비밀번호를 읽거나 남기지 않음).
|
||||
|
|
@ -294,23 +295,19 @@ Future<String?> pickVaultTarget(BuildContext context, List<String> folders, {Str
|
|||
/// 보관함 비밀번호 바꾸기 창. 글자 칸은 창이 닫히는 그림이 끝난 뒤에 버린다 (닫는 중에 버리면 오류)
|
||||
Future<String?> askVaultNewPassword(BuildContext context) async {
|
||||
final a = TextEditingController(), b = TextEditingController();
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, set) => AlertDialog(
|
||||
title: Text(tr('보관함 비밀번호 바꾸기')),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
TextField(controller: a, obscureText: true, decoration: InputDecoration(labelText: tr('새 비밀번호')), onChanged: (_) => set(() {})),
|
||||
TextField(controller: b, obscureText: true, decoration: InputDecoration(labelText: tr('한 번 더')), onChanged: (_) => set(() {})),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||
FilledButton(onPressed: a.text.isNotEmpty && a.text == b.text ? () => Navigator.pop(ctx, true) : null, child: Text(tr('바꾸기'))),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 공용 확인 창: 첫 초점 [취소] (Enter 로 바로 바뀌지 않음) · 두 칸이 같아야 [바꾸기] 켜짐
|
||||
final ok = await confirmDialog(
|
||||
context,
|
||||
title: () => tr('보관함 비밀번호 바꾸기'),
|
||||
body: (ctx, set) => Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
TextField(controller: a, obscureText: true, decoration: InputDecoration(labelText: tr('새 비밀번호')), onChanged: (_) => set(() {})),
|
||||
TextField(controller: b, obscureText: true, decoration: InputDecoration(labelText: tr('한 번 더')), onChanged: (_) => set(() {})),
|
||||
]),
|
||||
confirmLabel: () => tr('바꾸기'),
|
||||
cancelFirst: true,
|
||||
canConfirm: () => a.text.isNotEmpty && a.text == b.text,
|
||||
);
|
||||
final pw = ok == true ? a.text : null;
|
||||
final pw = ok ? a.text : null;
|
||||
Future<void>.delayed(const Duration(seconds: 1), () {
|
||||
a.dispose();
|
||||
b.dispose();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import '../core/vfs.dart';
|
|||
import '../core/secret_gate.dart';
|
||||
import '../core/webdav.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'confirm.dart';
|
||||
import 'confirm_dialog.dart';
|
||||
import 'file_error.dart';
|
||||
import 'theme.dart';
|
||||
import 'setting_tile.dart';
|
||||
|
|
|
|||
|
|
@ -76,4 +76,62 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
expect(got, isTrue);
|
||||
});
|
||||
|
||||
Future<BuildContext> host(WidgetTester tester) async {
|
||||
late BuildContext ctx;
|
||||
await tester.pumpWidget(MaterialApp(home: Builder(builder: (c) {
|
||||
ctx = c;
|
||||
return const Scaffold(body: SizedBox());
|
||||
})));
|
||||
return ctx;
|
||||
}
|
||||
|
||||
testWidgets('W DangerDialog (관리자 10/25): 빨강이 아니어도 되돌리기 어려운 일 (cancelFirst) 은 첫 초점 [취소] - Enter 로 실행되지 않음', (tester) async {
|
||||
final ctx = await host(tester);
|
||||
bool? got;
|
||||
unawaited(confirmDialog(ctx, title: () => '루트 폴더 보기', confirmLabel: () => '알고 봅니다', cancelFirst: true, body: (c, set) => const Text('내용'))
|
||||
.then((v) => got = v));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<FilledButton>(find.byType(FilledButton)).style, isNull, reason: '빨강은 아님');
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
expect(got, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('W DangerDialog: canConfirm 이 false 인 동안 [확인] 꺼짐 (누르고 Enter 해도 실행 안 됨) - 체크하면 켜짐', (tester) async {
|
||||
final ctx = await host(tester);
|
||||
var sure = false;
|
||||
bool? got;
|
||||
unawaited(confirmDialog(ctx,
|
||||
title: () => '시스템 폴더 보기',
|
||||
confirmLabel: () => '켜기',
|
||||
cancelFirst: true,
|
||||
canConfirm: () => sure,
|
||||
body: (c, set) => CheckboxListTile(value: sure, onChanged: (v) => set(() => sure = v ?? false), title: const Text('알고 켭니다')))
|
||||
.then((v) => got = v));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<FilledButton>(find.byType(FilledButton)).onPressed, isNull, reason: '체크 전 꺼짐');
|
||||
await tester.tap(find.text('켜기'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(got, isNull, reason: '꺼진 단추는 아무것도 안 함');
|
||||
await tester.tap(find.text('알고 켭니다'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<FilledButton>(find.byType(FilledButton)).onPressed, isNotNull, reason: '체크하면 켜짐');
|
||||
await tester.tap(find.text('켜기'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(got, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('confirmAction (글만 묻는 꼴) 도 같은 창: danger 면 빨강 · 첫 초점 [취소] (Enter → false), 아니면 Enter → true', (tester) async {
|
||||
for (final (danger, want) in [(true, false), (false, true)]) {
|
||||
final ctx = await host(tester);
|
||||
bool? got;
|
||||
unawaited(confirmAction(ctx, title: '영구 삭제', body: '되돌릴 수 없습니다', ok: '지우기', danger: danger).then((v) => got = v));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<FilledButton>(find.byType(FilledButton)).style != null, danger, reason: '빨강 = danger');
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
expect(got, want, reason: 'danger=$danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import 'package:jj_mkvmaker/app/transfer_job.dart' show userErrorText;
|
|||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/ui/confirm.dart';
|
||||
import 'package:jj_mkvmaker/ui/confirm_dialog.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/monitor_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/sync_preview_view.dart';
|
||||
|
|
|
|||
Loading…
Reference in New Issue