설정 A 분야 4: 복사 · 이동 · rsync · 실시간 동기화 (CopySettings) - 키 16 · 도우미 _method 함께
- 부르는 곳: s.copyTasks → s.copy.tasks 꼴 (RsyncRun 의 자기 필드 copySkipNewer 는 그대로 - 같은 이름이라 잘못 바뀐 것을 되돌림) - 문서 주석 [AppSettings.x] → [CopySettings.x] 등 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
ee7524e1c7
commit
059c8cba59
|
|
@ -24,9 +24,9 @@ void main() {
|
|||
final out = Platform.environment['JJ_SHOT_DIR'] ?? Directory.systemTemp.path;
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..copyMethodFolder = 'rsync'
|
||||
..copyBandwidthKBps = 5000
|
||||
..liveSyncPairs = [
|
||||
..copy.methodFolder = 'rsync'
|
||||
..copy.bandwidthKBps = 5000
|
||||
..copy.liveSyncPairs = [
|
||||
LiveSyncPair(r'D:\영상\원본', r'\\nas\backup\영상', method: 'rsync', delete: true),
|
||||
LiveSyncPair(r'C:\Users\me\Documents', r'E:\Documents', enabled: false),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ void main() {
|
|||
}
|
||||
Directory(dst).createSync();
|
||||
final c = AppController(PlatformServices.create());
|
||||
c.settings.liveSyncPairs = [
|
||||
c.settings.copy.liveSyncPairs = [
|
||||
LiveSyncPair(src, p.join(dst, 'mirror'), schedule: const ['0 9-17 * * 1-5', '0 22-23 * * 0,6'], delete: true),
|
||||
];
|
||||
final live = LiveSync(c)..start();
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ void main() {
|
|||
|
||||
final c = AppController(PlatformServices.create());
|
||||
c.settings
|
||||
..rsyncPaths = [tmp.path, tmp.path]
|
||||
..copy.rsyncPaths = [tmp.path, tmp.path]
|
||||
..explorer.style = 'windows';
|
||||
final key = GlobalKey();
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
|
|
@ -101,8 +101,8 @@ void main() {
|
|||
// 모니터링: 실행한 두 방향이 남는다
|
||||
await tap(find.text('모니터링'));
|
||||
await wait(1500);
|
||||
expect(c.settings.copyTasks.length, 2);
|
||||
expect(c.settings.copyTasks.every((t) => t.method == 'rsync' && t.contents && t.lastResult == 'done'), isTrue);
|
||||
expect(c.settings.copy.tasks.length, 2);
|
||||
expect(c.settings.copy.tasks.every((t) => t.method == 'rsync' && t.contents && t.lastResult == 'done'), isTrue);
|
||||
await shot('rsync_monitor');
|
||||
tester.view.reset();
|
||||
tmp.deleteSync(recursive: true);
|
||||
|
|
@ -124,8 +124,8 @@ void main() {
|
|||
await tester.runAsync(findBundledRsync);
|
||||
final c = AppController(PlatformServices.create());
|
||||
c.settings
|
||||
..rsyncPaths = [tmp.path, tmp.path]
|
||||
..rsyncOptions = '-avPog --delete --exclude=*.log --exclude=build/ --exclude=/sub/deep/';
|
||||
..copy.rsyncPaths = [tmp.path, tmp.path]
|
||||
..copy.rsyncOptions = '-avPog --delete --exclude=*.log --exclude=build/ --exclude=/sub/deep/';
|
||||
final key = GlobalKey();
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -194,8 +194,8 @@ void main() {
|
|||
await tester.runAsync(findBundledRsync);
|
||||
final c = AppController(PlatformServices.create());
|
||||
c.settings
|
||||
..rsyncPaths = [tmp.path, tmp.path]
|
||||
..liveSyncPairs = [LiveSyncPair(cDir, d, method: 'rsync', delete: true)];
|
||||
..copy.rsyncPaths = [tmp.path, tmp.path]
|
||||
..copy.liveSyncPairs = [LiveSyncPair(cDir, d, method: 'rsync', delete: true)];
|
||||
final key = GlobalKey();
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -240,7 +240,7 @@ void main() {
|
|||
expect(File(p.join(b, 'sub', 'deep', 'x.txt')).readAsStringSync(), 'x');
|
||||
expect(Directory(p.join(b, 'empty')).existsSync(), isTrue); // 빈 폴더도 대상에는 만들어짐 (rsync -a)
|
||||
expect(Directory(a).existsSync(), isFalse, reason: '원본 폴더까지 지움');
|
||||
final moved = c.settings.copyTasks.single;
|
||||
final moved = c.settings.copy.tasks.single;
|
||||
expect([moved.move, moved.prune, moved.lastResult], [true, 'all', 'done']);
|
||||
expect(moved.lastMessage, '빈 폴더 4개 지움'); // sub/deep · sub · empty · srcA
|
||||
|
||||
|
|
@ -263,8 +263,8 @@ void main() {
|
|||
expect(File(p.join(d, 'sub', 'deep', 'y.txt')).readAsStringSync(), 'y');
|
||||
expect(Directory(cDir).existsSync(), isTrue, reason: '원본 폴더는 남김');
|
||||
expect(Directory(cDir).listSync(), isEmpty, reason: '빈 폴더는 모두 지움');
|
||||
expect(c.settings.liveSyncPairs.single.enabled, isFalse, reason: '원본이 비었으므로 실시간 동기화는 끔');
|
||||
final fin = c.settings.copyTasks.firstWhere((t) => t.sources.single == cDir);
|
||||
expect(c.settings.copy.liveSyncPairs.single.enabled, isFalse, reason: '원본이 비었으므로 실시간 동기화는 끔');
|
||||
final fin = c.settings.copy.tasks.firstWhere((t) => t.sources.single == cDir);
|
||||
expect([fin.options, fin.move, fin.prune, fin.lastResult], ['-avHPOg', true, 'keep', 'done']);
|
||||
expect(fin.lastMessage, '빈 폴더 3개 지움'); // sub/deep · sub · empty (liveC 는 남김)
|
||||
tester.view.reset();
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ void main() {
|
|||
testWidgets('52: 복사 중에 다른 화면에 갔다 와도 진행 막대', (t) async {
|
||||
// 다른 화면에 갔다 와도 아직 복사 중이게 (약 25초)
|
||||
File(p.join(left, 'big.bin')).writeAsBytesSync(List.filled(2 * 1024 * 1024, 7));
|
||||
c.settings.copyBandwidthKBps = 80;
|
||||
c.settings.copy.bandwidthKBps = 80;
|
||||
await show(t, ExplorerPage(device: device, c: c));
|
||||
await selectAndPress(t, 'big.bin', '복사');
|
||||
await t.tap(find.widgetWithText(FilledButton, '복사'));
|
||||
|
|
|
|||
|
|
@ -60,9 +60,9 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorer.paths = [tmp.path, tmp.path]
|
||||
..rsyncPaths = [tmp.path, tmp.path]
|
||||
..copyTasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, 'sub'), method: 'rsync', options: '-av')]
|
||||
..liveSyncPairs = [LiveSyncPair(tmp.path, p.join(tmp.path, 'x'))];
|
||||
..copy.rsyncPaths = [tmp.path, tmp.path]
|
||||
..copy.tasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, 'sub'), method: 'rsync', options: '-av')]
|
||||
..copy.liveSyncPairs = [LiveSyncPair(tmp.path, p.join(tmp.path, 'x'))];
|
||||
c.videos.add(VideoItem(video));
|
||||
// 256: 폰 가로에서 작업 기록을 켜도 목록이 여러 줄 보이는지 - 동영상 5개 · 기록 몇 줄
|
||||
for (var i = 2; i <= 5; i++) {
|
||||
|
|
|
|||
|
|
@ -51,12 +51,12 @@ void main() {
|
|||
}
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..rsyncSource = 'custom'
|
||||
..rsyncPath = rsync
|
||||
..rsyncOptions = '-av --exclude=*.tmp --delete-excluded';
|
||||
..copy.rsyncSource = 'custom'
|
||||
..copy.rsyncPath = rsync
|
||||
..copy.rsyncOptions = '-av --exclude=*.tmp --delete-excluded';
|
||||
// 이미 --exclude 만으로 확인한 쌍에 --delete-excluded 를 더함 → 다시 묻는 창 (주황 이유 줄 · 거른 것 묶음)
|
||||
final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true, deleteConfirmed: true, confirmedOptions: '-av --exclude=*.tmp');
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
await t.runAsync(() => live.syncNow(pair));
|
||||
|
|
|
|||
|
|
@ -47,13 +47,13 @@ void main() {
|
|||
File(p.join(tmp.path, 'rules.txt')).writeAsStringSync('H a.txt\n');
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..rsyncSource = 'custom'
|
||||
..rsyncPath = rsync
|
||||
..rsyncOptions = '-a --filter="merge ${toCygwinPath(p.join(tmp.path, 'rules.txt'))}"'
|
||||
..liveSyncPairs = [LiveSyncPair(src, dst, method: 'rsync', delete: true, deleteConfirmed: true)];
|
||||
..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();
|
||||
addTearDown(live.dispose);
|
||||
final pair = c.settings.liveSyncPairs.single;
|
||||
final pair = c.settings.copy.liveSyncPairs.single;
|
||||
await t.runAsync(() => live.syncNow(pair));
|
||||
expect(live.noAutoDelete[LiveSync.keyOf(pair)], isNotEmpty);
|
||||
final shotKey = GlobalKey();
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorer.paths = [left, right]
|
||||
..copyConflict = 'overwrite';
|
||||
..copy.conflict = 'overwrite';
|
||||
final root = p.rootPrefix(tmp.path);
|
||||
device.volumes = () => [(root, root.replaceAll(RegExp(r'[\\/]+$'), ''))];
|
||||
final shotKey = GlobalKey();
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorer.orientation = 'side'
|
||||
..rsyncPaths = [l, r];
|
||||
..copy.rsyncPaths = [l, r];
|
||||
final shotKey = GlobalKey();
|
||||
await t.pumpWidget(MaterialApp(
|
||||
theme: buildTheme(),
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class CopyCenter extends ChangeNotifier {
|
|||
/// 작업 id → 지금 (또는 마지막) 실행
|
||||
final jobs = <String, TransferJob>{};
|
||||
|
||||
List<CopyTask> get tasks => c.settings.copyTasks;
|
||||
List<CopyTask> get tasks => c.settings.copy.tasks;
|
||||
|
||||
bool isRunning(String id) => jobs[id] != null && !jobs[id]!.finished;
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ class CopyCenter extends ChangeNotifier {
|
|||
CopyTask fresh(List<String> sources, String dest, {bool move = false, bool contents = false, String? method}) {
|
||||
final s = c.settings;
|
||||
final hasDir = sources.any((s) => isDav(s) || FileSystemEntity.isDirectorySync(s));
|
||||
var m = CopyMethod.of(method ?? (hasDir ? s.copyMethodFolder : s.copyMethodFile));
|
||||
var m = CopyMethod.of(method ?? (hasDir ? s.copy.methodFolder : s.copy.methodFile));
|
||||
if (!copyMethodAvailable(m, s)) m = CopyMethod.builtin;
|
||||
// WebDAV 가 끼면 실제로는 앱이 직접 복사한다 (확인 창에 robocopy 로 잘못 보이지 않게). Rsync 화면 ([method] 지정) 은 그대로
|
||||
if (method == null && [...sources, dest].any(isDav)) m = CopyMethod.builtin;
|
||||
|
|
@ -61,9 +61,9 @@ class CopyCenter extends ChangeNotifier {
|
|||
move: move,
|
||||
contents: contents,
|
||||
method: m.name,
|
||||
options: m == CopyMethod.robocopy ? s.robocopyOptions : s.rsyncOptions,
|
||||
once: s.copyRunMode == 'once',
|
||||
bandwidthKBps: s.copyBandwidthKBps,
|
||||
options: m == CopyMethod.robocopy ? s.copy.robocopyOptions : s.copy.rsyncOptions,
|
||||
once: s.copy.runMode == 'once',
|
||||
bandwidthKBps: s.copy.bandwidthKBps,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -79,20 +79,20 @@ class CopyCenter extends ChangeNotifier {
|
|||
final t = fresh(sources, dest, move: move, contents: contents, method: method);
|
||||
final old = tasks.where((x) => x.key == t.key).firstOrNull;
|
||||
if (old != null) return old;
|
||||
await c.updateSettings((x) => x.copyTasks = [t, ...x.copyTasks]);
|
||||
await c.updateSettings((x) => x.copy.tasks = [t, ...x.copy.tasks]);
|
||||
notifyListeners();
|
||||
return t;
|
||||
}
|
||||
|
||||
Future<void> update(CopyTask t) async {
|
||||
await c.updateSettings((x) => x.copyTasks = [for (final y in x.copyTasks) y.id == t.id ? t : y]);
|
||||
await c.updateSettings((x) => x.copy.tasks = [for (final y in x.copy.tasks) y.id == t.id ? t : y]);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> remove(String id) async {
|
||||
cancel(id);
|
||||
jobs.remove(id);
|
||||
await c.updateSettings((x) => x.copyTasks = [for (final y in x.copyTasks) if (y.id != id) y]);
|
||||
await c.updateSettings((x) => x.copy.tasks = [for (final y in x.copy.tasks) if (y.id != id) y]);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +126,7 @@ class CopyCenter extends ChangeNotifier {
|
|||
contents: t.contents,
|
||||
prune: t.move ? t.prune : '',
|
||||
conflict: conflict,
|
||||
skipNewer: c.settings.copySkipNewer,
|
||||
skipNewer: c.settings.copy.skipNewer,
|
||||
);
|
||||
jobs[t.id] = job;
|
||||
job.addListener(notifyListeners);
|
||||
|
|
@ -170,13 +170,13 @@ class CopyCenter extends ChangeNotifier {
|
|||
];
|
||||
final rest = t.sources.where((s) => !dirs.contains(s)).toList();
|
||||
await c.updateSettings((x) => x
|
||||
..liveSyncPairs = [
|
||||
...x.liveSyncPairs,
|
||||
..copy.liveSyncPairs = [
|
||||
...x.copy.liveSyncPairs,
|
||||
for (final n in pairs)
|
||||
if (!x.liveSyncPairs.any((o) => o.source == n.source && o.target == n.target)) n,
|
||||
if (!x.copy.liveSyncPairs.any((o) => o.source == n.source && o.target == n.target)) n,
|
||||
]
|
||||
..copyTasks = [
|
||||
for (final y in x.copyTasks)
|
||||
..copy.tasks = [
|
||||
for (final y in x.copy.tasks)
|
||||
if (y.id != t.id)
|
||||
y
|
||||
else if (rest.isNotEmpty)
|
||||
|
|
@ -203,8 +203,8 @@ class CopyCenter extends ChangeNotifier {
|
|||
final m = copyMethodAvailable(CopyMethod.rsync, s) ? 'rsync' : x.method;
|
||||
final t = fresh([x.source], x.target, contents: true, method: m);
|
||||
await c.updateSettings((y) => y
|
||||
..liveSyncPairs = [for (final o in y.liveSyncPairs) if (!(o.source == x.source && o.target == x.target)) o]
|
||||
..copyTasks = [t, ...y.copyTasks]);
|
||||
..copy.liveSyncPairs = [for (final o in y.copy.liveSyncPairs) if (!(o.source == x.source && o.target == x.target)) o]
|
||||
..copy.tasks = [t, ...y.copy.tasks]);
|
||||
notifyListeners();
|
||||
return t;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ Future<void> findBundledRsync() async {
|
|||
/// - 직접 지정: 그 파일
|
||||
/// - 기본: 앱에 들어 있는 것, 없으면 (Windows) 내려받아 둔 것
|
||||
Future<String?> rsyncExecutable(AppSettings s) async {
|
||||
if (s.rsyncSource == 'custom') {
|
||||
return s.rsyncPath.isNotEmpty && File(s.rsyncPath).existsSync() ? s.rsyncPath : null;
|
||||
if (s.copy.rsyncSource == 'custom') {
|
||||
return s.copy.rsyncPath.isNotEmpty && File(s.copy.rsyncPath).existsSync() ? s.copy.rsyncPath : null;
|
||||
}
|
||||
final b = bundledRsync;
|
||||
if (b != null && File(b).existsSync()) return b;
|
||||
|
|
@ -54,11 +54,11 @@ Future<String?> rsyncExecutable(AppSettings s) async {
|
|||
bool copyMethodAvailable(CopyMethod m, AppSettings s) => switch (m) {
|
||||
CopyMethod.builtin => true,
|
||||
CopyMethod.robocopy => Platform.isWindows,
|
||||
CopyMethod.rsync => Platform.isWindows || s.rsyncSource == 'custom' || bundledRsync != null,
|
||||
CopyMethod.rsync => Platform.isWindows || s.copy.rsyncSource == 'custom' || bundledRsync != null,
|
||||
};
|
||||
|
||||
/// 실시간 동기화 (lsyncd 처럼): 설정의 폴더 쌍마다 원본을 지켜보다가 바뀌면 대상에 맞춘다.
|
||||
/// Windows 는 폴더 감시로 바로 (모아서 3초 뒤), 그 밖은 [AppSettings.liveSyncIntervalSec] 마다 살핀다.
|
||||
/// Windows 는 폴더 감시로 바로 (모아서 3초 뒤), 그 밖은 [CopySettings.liveSyncIntervalSec] 마다 살핀다.
|
||||
/// 앱이 켜져 있는 동안만 동작하고, 시작할 때 한 번 맞춘다.
|
||||
class LiveSync extends ChangeNotifier {
|
||||
final AppController c;
|
||||
|
|
@ -82,13 +82,13 @@ class LiveSync extends ChangeNotifier {
|
|||
/// 1분마다: 동작 시간 (cron) 이 시작되면 밀린 것을 맞춘다
|
||||
Timer? _clock;
|
||||
|
||||
/// 이번 실행 동안 멈춘 쌍 (설정의 켜기 · 끄기와 따로, 앱을 다시 켜면 [AppSettings.liveSyncOnStart] 를 따른다)
|
||||
/// 이번 실행 동안 멈춘 쌍 (설정의 켜기 · 끄기와 따로, 앱을 다시 켜면 [CopySettings.liveSyncOnStart] 를 따른다)
|
||||
final paused = <String>{};
|
||||
|
||||
/// [hold] 면 켜진 쌍을 모두 멈춘 채로 시작 (모니터링 · [resume] 으로 시작)
|
||||
void start({bool hold = false}) {
|
||||
instance = this;
|
||||
if (hold) paused.addAll(c.settings.liveSyncPairs.where((x) => x.enabled).map(keyOf));
|
||||
if (hold) paused.addAll(c.settings.copy.liveSyncPairs.where((x) => x.enabled).map(keyOf));
|
||||
c.addListener(_apply);
|
||||
_apply();
|
||||
_clock ??= Timer.periodic(const Duration(minutes: 1), (_) => _onClock());
|
||||
|
|
@ -120,19 +120,19 @@ class LiveSync extends ChangeNotifier {
|
|||
|
||||
/// 지금 지켜보는 쌍 (켜져 있고 멈추지 않은 것)
|
||||
List<LiveSyncPair> get watching => [
|
||||
for (final x in c.settings.liveSyncPairs)
|
||||
for (final x in c.settings.copy.liveSyncPairs)
|
||||
if (x.enabled && !paused.contains(keyOf(x))) x,
|
||||
];
|
||||
|
||||
void pause(LiveSyncPair x) => _setPaused({...paused, keyOf(x)});
|
||||
void resume(LiveSyncPair x) => _setPaused({...paused}..remove(keyOf(x)));
|
||||
void pauseAll() => _setPaused({...paused, ...c.settings.liveSyncPairs.map(keyOf)});
|
||||
void pauseAll() => _setPaused({...paused, ...c.settings.copy.liveSyncPairs.map(keyOf)});
|
||||
void resumeAll() => _setPaused({});
|
||||
|
||||
/// 시작할 때 고른 쌍만 (나머지는 멈춤)
|
||||
void runOnly(Iterable<LiveSyncPair> xs) {
|
||||
final keep = xs.map(keyOf).toSet();
|
||||
_setPaused({for (final x in c.settings.liveSyncPairs) if (!keep.contains(keyOf(x))) keyOf(x)});
|
||||
_setPaused({for (final x in c.settings.copy.liveSyncPairs) if (!keep.contains(keyOf(x))) keyOf(x)});
|
||||
}
|
||||
|
||||
void _setPaused(Set<String> v) {
|
||||
|
|
@ -153,10 +153,10 @@ class LiveSync extends ChangeNotifier {
|
|||
/// 원본과 대상의 다른 점 (크기 · 바뀐 시각). [x.delete] 면 원본에 없는 대상 항목도 "− 이름" 으로.
|
||||
/// 관리자 지시 (10/19): rsync 쌍이 원격이라 앱이 맞출 때 (그리고 그 대기 목록) 도 환경 설정 rsync 옵션의 거르기를 따른다.
|
||||
/// 로컬 rsync 쌍은 rsync 가 직접 따르므로 대기 목록 (diff) 에도 같이
|
||||
RsyncFilter _filterOf(LiveSyncPair x) => x.method == 'rsync' ? RsyncFilter.parse(c.settings.rsyncOptions) : RsyncFilter.none;
|
||||
RsyncFilter _filterOf(LiveSyncPair x) => x.method == 'rsync' ? RsyncFilter.parse(c.settings.copy.rsyncOptions) : RsyncFilter.none;
|
||||
|
||||
/// 지울 것에 영향을 주는 옵션 (rsync 쌍만 - 거르기 · --delete-excluded). 확인할 때 쌍에 기억한다 ([LiveSyncPair.confirmedOptions])
|
||||
String _optionsOf(LiveSyncPair x) => x.method == 'rsync' ? c.settings.rsyncOptions : '';
|
||||
String _optionsOf(LiveSyncPair x) => x.method == 'rsync' ? c.settings.copy.rsyncOptions : '';
|
||||
|
||||
/// 관리자 3a 판단 (10/20): 확인한 쌍이라도 옵션이 바뀌어 지울 것이 늘었으면 다시 묻는다 (로컬 · 원격 쌍 모두).
|
||||
/// 늘지 않았으면 (줄거나 같음) 묻지 않고 지금 옵션을 기억한다. 예전에 확인한 쌍 (기억한 옵션 없음) 은 예전 동작
|
||||
|
|
@ -419,8 +419,8 @@ class LiveSync extends ChangeNotifier {
|
|||
}
|
||||
|
||||
/// 설정의 쌍을 바꾼다 (같은 원본 → 대상)
|
||||
Future<void> _setPair(LiveSyncPair old, LiveSyncPair now) => c.updateSettings((s) => s.liveSyncPairs = [
|
||||
for (final p in s.liveSyncPairs) keyOf(p) == keyOf(old) ? now : p,
|
||||
Future<void> _setPair(LiveSyncPair old, LiveSyncPair now) => c.updateSettings((s) => s.copy.liveSyncPairs = [
|
||||
for (final p in s.copy.liveSyncPairs) keyOf(p) == keyOf(old) ? now : p,
|
||||
]);
|
||||
|
||||
/// 지울 목록을 보고 결정: [delete] true = 지우기 포함으로 맞추기 (확인함), false = 지우기 끄기. 그 뒤 바로 맞춘다
|
||||
|
|
@ -449,7 +449,7 @@ class LiveSync extends ChangeNotifier {
|
|||
void _apply() {
|
||||
final s = c.settings;
|
||||
final pairs = watching;
|
||||
final sig = jsonEncode([for (final x in pairs) x.toJson(), s.liveSyncIntervalSec]);
|
||||
final sig = jsonEncode([for (final x in pairs) x.toJson(), s.copy.liveSyncIntervalSec]);
|
||||
if (sig == _sig) return;
|
||||
_sig = sig;
|
||||
_stopAll();
|
||||
|
|
@ -475,11 +475,11 @@ class LiveSync extends ChangeNotifier {
|
|||
_schedule(x);
|
||||
});
|
||||
} catch (_) {
|
||||
_timers[k] = Timer.periodic(Duration(seconds: s.liveSyncIntervalSec), (_) => _schedule(x));
|
||||
_timers[k] = Timer.periodic(Duration(seconds: s.copy.liveSyncIntervalSec), (_) => _schedule(x));
|
||||
}
|
||||
} else {
|
||||
// Android 등 · 원본이 WebDAV: 폴더 안쪽까지 감시가 안 되므로 정해진 간격으로 살핀다
|
||||
_timers[k] = Timer.periodic(Duration(seconds: s.liveSyncIntervalSec), (_) => _schedule(x));
|
||||
_timers[k] = Timer.periodic(Duration(seconds: s.copy.liveSyncIntervalSec), (_) => _schedule(x));
|
||||
}
|
||||
_schedule(x); // 시작할 때 한 번
|
||||
}
|
||||
|
|
@ -581,7 +581,7 @@ class LiveSync extends ChangeNotifier {
|
|||
// 한쪽이라도 WebDAV 면 rsync · robocopy 대신 앱이 맞춘다 (크기 · 시각 비교)
|
||||
final method = isDav(x.source) || isDav(x.target) ? CopyMethod.builtin : CopyMethod.of(x.method);
|
||||
// M1 (10/19): 원격 쌍에서 받는 쪽의 같은 이름 폴더는 지우기 포함 (--delete) 이 아니면 지우지 않고 건너뛴다 - 카드에 알린다
|
||||
final ops = FileOps(bandwidthKBps: s.copyBandwidthKBps);
|
||||
final ops = FileOps(bandwidthKBps: s.copy.bandwidthKBps);
|
||||
final n = switch (method) {
|
||||
CopyMethod.builtin => await ops.mirror(x.source, x.target, delete: x.delete, allowEmptySource: allowEmpty, filter: _filterOf(x)),
|
||||
CopyMethod.rsync => await _rsync(x, s),
|
||||
|
|
@ -650,15 +650,15 @@ class LiveSync extends ChangeNotifier {
|
|||
// 107: 원본을 지우는 옵션 (--remove-source-files) 도 실시간 동기화에서는 쓰지 않는다
|
||||
// 관리자 판단 (10/20, 28 확인): 목록 · 기록 · 배치 파일의 C:\… 는 rsync 가 읽는 꼴로 (rsyncFileOptionArgs)
|
||||
...rsyncFileOptionArgs([
|
||||
for (final o in splitOptions(s.rsyncOptions))
|
||||
for (final o in splitOptions(s.copy.rsyncOptions))
|
||||
if (o != '-P' && o != '--progress' && !isRsyncDeleteOption(o) && !isSourceRemovingOption(o)) o,
|
||||
], windows: windows),
|
||||
if (!windows) '-8', // 한글 등 이름을 \#355… 로 바꾸지 않고 그대로 (Android 빌드는 iconv 없음)
|
||||
if (s.copyBandwidthKBps > 0) '--bwlimit=${s.copyBandwidthKBps}',
|
||||
if (s.copy.bandwidthKBps > 0) '--bwlimit=${s.copy.bandwidthKBps}',
|
||||
if (x.delete) '--delete',
|
||||
// 10/20 (15, 28 의 장면으로 찾음): --delete-excluded 는 쌍이 지우기 포함일 때만 남긴다 - 예전에는 늘 빼서, 지울지 묻는 창
|
||||
// (diff 는 옵션의 거르기를 따름) 에 보인 거른 것을 실제로는 지우지 않고 맞출 것에 계속 남았다 (원격 쌍 · Rsync 화면은 따름)
|
||||
if (x.delete && splitOptions(s.rsyncOptions).contains('--delete-excluded')) '--delete-excluded',
|
||||
if (x.delete && splitOptions(s.copy.rsyncOptions).contains('--delete-excluded')) '--delete-excluded',
|
||||
...appTempRsyncExcludes, // 78 보충: 이름 바꾸는 중의 임시 이름은 보내지도 지우지도 않는다
|
||||
'${toCygwinPath(x.source, windows: windows)}/',
|
||||
'${rsyncDestPath(x.target, windows: windows)}/', // 받는 쪽 UNC 는 \\서버\… (28 · 15 확인 10/20)
|
||||
|
|
@ -667,7 +667,7 @@ class LiveSync extends ChangeNotifier {
|
|||
/// robocopy 인수 (실시간 동기화). 지우기 (/MIR · /PURGE) 는 쌍의 "지우기 포함" 으로만 (/MIR 는 /E 로) - 97
|
||||
static List<String> robocopyArgsFor(LiveSyncPair x, AppSettings s) {
|
||||
final opts = [
|
||||
for (final o in splitOptions(s.robocopyOptions))
|
||||
for (final o in splitOptions(s.copy.robocopyOptions))
|
||||
if (o.toUpperCase() == '/MIR') '/E' else if (o.toUpperCase() != '/PURGE' && !isSourceRemovingOption(o)) o,
|
||||
];
|
||||
return [
|
||||
|
|
@ -679,7 +679,7 @@ class LiveSync extends ChangeNotifier {
|
|||
if (!robocopyLinkOption(opts)) '/XJ',
|
||||
if (x.delete) '/PURGE',
|
||||
...appTempRobocopyExcludes, // 78 보충
|
||||
if (s.copyBandwidthKBps > 0) '/IPG:${robocopyIpg(s.copyBandwidthKBps)}',
|
||||
if (s.copy.bandwidthKBps > 0) '/IPG:${robocopyIpg(s.copy.bandwidthKBps)}',
|
||||
'/BYTES', '/NJH', '/NJS', '/NDL', '/NP',
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import '../services/app_shell.dart' show appIconOf;
|
|||
import '../core/remote_idle.dart';
|
||||
import 'settings/records.dart';
|
||||
import 'settings/settings_group.dart';
|
||||
import 'settings/copy_settings.dart';
|
||||
import 'settings/tor_settings.dart';
|
||||
import 'settings/network_settings.dart';
|
||||
import 'settings/explorer_settings.dart';
|
||||
|
|
@ -23,6 +24,7 @@ import 'settings/explorer_settings.dart';
|
|||
export 'settings/records.dart';
|
||||
export 'settings/settings_group.dart';
|
||||
export 'settings/settings_store.dart';
|
||||
export 'settings/copy_settings.dart';
|
||||
export 'settings/tor_settings.dart';
|
||||
export 'settings/network_settings.dart';
|
||||
export 'settings/explorer_settings.dart';
|
||||
|
|
@ -226,42 +228,18 @@ class AppSettings {
|
|||
|
||||
|
||||
// ── 복사 · 이동 (파일 탐색기) · rsync · lsync (Rsync 화면) ──
|
||||
/// 파일 탐색기의 복사 · 이동 방법: 'builtin' 현재 방식 (기본) · 'robocopy' (Windows). 파일만 고를 때 / 폴더가 들어 있을 때.
|
||||
/// rsync 는 Rsync 화면에서 따로 (파일 탐색기에서는 쓰지 않음)
|
||||
String copyMethodFile = 'builtin';
|
||||
|
||||
/// 48: 복사 · 이동할 때 받는 폴더에 같은 이름이 있으면 'ask' (확인 창에서 고름, 처음 값) · 'rename' · 'overwrite' · 'skip'
|
||||
String copyConflict = 'ask';
|
||||
|
||||
/// P0: MKV 를 만들 때 결과 MKV 가 이미 있으면 - 'ask' 묻기 (처음 값) · 'overwrite' 덮어쓰기 · 'skip' 건너뛰기 · 'rename' 다른 이름.
|
||||
/// 덮어쓰기여도 다 만든 뒤에만 바꿔 넣는다 (도중에 취소 · 실패하면 예전 것 그대로)
|
||||
String mkvExisting = 'ask';
|
||||
|
||||
/// 229: 앱이 직접 하는 복사 · 이동 (현재 방식 · WebDAV 의 안의 것 맞추기) 에서 받는 쪽이 더 새것이면 건너뛴다 (rsync -u 와 같음).
|
||||
/// 처음 값 켜짐 - 예전 원본으로 새 파일을 덮어쓰고 (이동이면 원본까지 지워) 새 내용을 잃었다 (관리자 판단)
|
||||
bool copySkipNewer = true;
|
||||
String copyMethodFolder = 'builtin';
|
||||
String rsyncOptions = '-avPog';
|
||||
String robocopyOptions = '/E /COPY:DAT /DCOPY:T /R:2 /W:2';
|
||||
|
||||
/// 여러 개를 고르면: 'each' 항목마다 따로 실행 (기본) · 'once' 한 번에 (rsync)
|
||||
String copyRunMode = 'each';
|
||||
|
||||
/// 속도 제한 KB/s (0 = 제한 없음). 모든 방법에 (rsync --bwlimit, 현재 방식은 앱이 조절, robocopy 는 /IPG 로 비슷하게)
|
||||
int copyBandwidthKBps = 0;
|
||||
|
||||
/// rsync 가져오기: 'download' 앱에 들어 있는 것 (기본, Windows 는 없으면 내려받기) · 'custom' 직접 지정한 실행 파일 ([rsyncPath])
|
||||
String rsyncSource = 'download';
|
||||
String rsyncPath = '';
|
||||
|
||||
/// 실시간 동기화 (lsyncd 처럼)
|
||||
List<LiveSyncPair> liveSyncPairs = [];
|
||||
|
||||
/// 모니터링 (Rsync 화면 가운데 [모니터링]): 실행한 rsync 를 기억해 다시 실행 · 옵션 고치기 · lsync 로 옮기기
|
||||
List<CopyTask> copyTasks = [];
|
||||
|
||||
/// Rsync 화면에서 마지막으로 연 폴더 (왼쪽 · 오른쪽)
|
||||
List<String> rsyncPaths = [];
|
||||
|
||||
/// 설치한 (켠) 컴포넌트 id (app/components.dart). 처음엔 기본으로 들어 있는 것 모두
|
||||
List<String> components = AppComponent.defaultInstalled;
|
||||
|
|
@ -303,8 +281,6 @@ class AppSettings {
|
|||
/// 99 (Windows): 시작 메뉴에 등록 - 알림이 JJ_MKVMaker 로 보이고 눌러서 열린다
|
||||
bool startMenuShortcut = true;
|
||||
|
||||
/// 92 (Windows): 실시간 동기화가 멈췄을 때 Windows 알림
|
||||
bool syncStopToast = true;
|
||||
|
||||
// ── 102: 사용자에게 물어 정한 것 · 관리자 1차 판단 (기본값 = 그 답) ──
|
||||
|
||||
|
|
@ -432,15 +408,11 @@ class AppSettings {
|
|||
/// 저장한 뒤 'upscaled' 올린 것으로 계속 보기 · 'original' 원본으로
|
||||
String aiUpAfter = 'upscaled';
|
||||
|
||||
/// 실시간 동기화 확인 간격 (초). Windows 는 바뀌면 바로, 그 밖은 이 간격으로 살핀다.
|
||||
int liveSyncIntervalSec = 30;
|
||||
|
||||
/// 백그라운드로 실행 (Android): 켜면 ← 로 닫거나 최근 앱에서 밀어도 동기화 · MKV 만들기 · 다운로드를 계속한다 (알림에 표시).
|
||||
/// 끄면 지금처럼 닫을 때 끝난다. Windows 는 [closeAction] 'background' 가 같은 뜻 ([backgroundRun]).
|
||||
bool runInBackground = true;
|
||||
|
||||
/// 앱을 다시 켤 때 실시간 동기화: 'auto' 바로 시작 / 'ask' 골라서 시작 / 'off' 시작 안 함 (모니터링에서 시작)
|
||||
String liveSyncOnStart = 'auto';
|
||||
|
||||
/// 백그라운드로 실행 (설정 화면의 체크 하나). Windows: 창 ✕ 를 누르면 트레이로 (끄면 종료), Android: [runInBackground]
|
||||
bool get backgroundRun => Platform.isWindows ? closeAction == 'background' : runInBackground;
|
||||
|
|
@ -499,8 +471,11 @@ class AppSettings {
|
|||
/// Tor 釉뚮씪?곗? (?ъ슜???붿껌 10/10)
|
||||
final tor = TorBrowserSettings();
|
||||
|
||||
/// 복사 · 이동 (파일 탐색기) · rsync · 실시간 동기화 (Rsync 화면)
|
||||
final copy = CopySettings();
|
||||
|
||||
/// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다
|
||||
List<SettingsGroup> get groups => [explorer, network, tor];
|
||||
List<SettingsGroup> get groups => [explorer, network, tor, copy];
|
||||
|
||||
/// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다
|
||||
Map<String, Object?> toJson({bool plainSecrets = false}) {
|
||||
|
|
@ -571,12 +546,7 @@ class AppSettings {
|
|||
'rootEnabled': rootEnabled,
|
||||
'vaultMasterCopy': vaultMasterCopy,
|
||||
'vaultAutoLockSeconds': vaultAutoLockSeconds,
|
||||
'copyMethodFile': copyMethodFile,
|
||||
'copyConflict': copyConflict,
|
||||
'mkvExisting': mkvExisting,
|
||||
'copySkipNewer': copySkipNewer,
|
||||
'copyMethodFolder': copyMethodFolder,
|
||||
'rsyncPaths': rsyncPaths,
|
||||
'components': components,
|
||||
'aiImageV1': true,
|
||||
'robocopyLinksV1': true,
|
||||
|
|
@ -592,7 +562,6 @@ class AppSettings {
|
|||
'readerVolumeKeys': readerVolumeKeys,
|
||||
'readerSplit': readerSplit,
|
||||
'startMenuShortcut': startMenuShortcut,
|
||||
'syncStopToast': syncStopToast,
|
||||
'swipeWrap': swipeWrap,
|
||||
'browserSwipeHinted': browserSwipeHinted,
|
||||
'cleanupPrechecked': cleanupPrechecked,
|
||||
|
|
@ -647,18 +616,8 @@ class AppSettings {
|
|||
'smbRequireSecure': smbRequireSecure,
|
||||
'remoteIdleSeconds': remoteIdleSeconds,
|
||||
'ftpServers': [for (final x in ftpServers) x.toJson()],
|
||||
'rsyncOptions': rsyncOptions,
|
||||
'robocopyOptions': robocopyOptions,
|
||||
'copyRunMode': copyRunMode,
|
||||
'copyBandwidthKBps': copyBandwidthKBps,
|
||||
'rsyncSource': rsyncSource,
|
||||
'rsyncPath': rsyncPath,
|
||||
'liveSyncPairs': [for (final x in liveSyncPairs) x.toJson()],
|
||||
'liveSyncIntervalSec': liveSyncIntervalSec,
|
||||
'runInBackground': runInBackground,
|
||||
'backgroundV2': true,
|
||||
'liveSyncOnStart': liveSyncOnStart,
|
||||
'copyTasks': [for (final x in copyTasks) x.toJson()],
|
||||
'autoCheckUpdates': autoCheckUpdates,
|
||||
'lastUpdateCheck': lastUpdateCheck,
|
||||
'lastUpdateCheckVersion': lastUpdateCheckVersion,
|
||||
|
|
@ -689,8 +648,6 @@ class AppSettings {
|
|||
return j;
|
||||
}
|
||||
|
||||
/// 파일 탐색기의 복사 방법 (rsync 는 Rsync 화면으로 옮겨 예전 값 rsync 도 현재 방식으로)
|
||||
static String _method(Object? v) => v == 'robocopy' ? 'robocopy' : 'builtin';
|
||||
|
||||
/// 색 보정 값 (-100 ~ 100)
|
||||
static int _adj(Object? v) => ((v as num?)?.round() ?? 0).clamp(-100, 100);
|
||||
|
|
@ -762,12 +719,7 @@ class AppSettings {
|
|||
..wifiShareMinutes = const [0, 10, 30, 60, 120].contains(j['wifiShareMinutes']) ? j['wifiShareMinutes'] as int : 30
|
||||
..wifiShareTailscale = j['wifiShareTailscale'] is bool ? j['wifiShareTailscale'] as bool : true
|
||||
..wifiShareFolder = j['wifiShareFolder'] is String ? j['wifiShareFolder'] as String : ''
|
||||
..copyMethodFile = _method(j['copyMethodFile'])
|
||||
..copyConflict = const ['rename', 'overwrite', 'skip'].contains(j['copyConflict']) ? j['copyConflict'] as String : 'ask'
|
||||
..mkvExisting = const ['rename', 'overwrite', 'skip'].contains(j['mkvExisting']) ? j['mkvExisting'] as String : 'ask'
|
||||
..copySkipNewer = j['copySkipNewer'] != false
|
||||
..copyMethodFolder = _method(j['copyMethodFolder'])
|
||||
..rsyncPaths = [for (final x in (j['rsyncPaths'] as List?) ?? const []) '$x']
|
||||
..components = j['components'] is List
|
||||
? [
|
||||
// 225: 이 판이 모르는 구성 요소 (새 판의 Tor 등) 도 지우지 않고 그대로 둔다 - 예전 판을 거쳐 다시 올려도 켠 것이 남게 (40 과 같은 원칙).
|
||||
|
|
@ -794,7 +746,6 @@ class AppSettings {
|
|||
..readerVolumeKeys = j['readerVolumeKeys'] as bool? ?? false
|
||||
..readerSplit = const ['auto', 'on', 'off'].contains(j['readerSplit']) ? j['readerSplit'] as String : 'auto'
|
||||
..startMenuShortcut = j['startMenuShortcut'] != false
|
||||
..syncStopToast = j['syncStopToast'] != false
|
||||
..swipeWrap = j['swipeWrap'] != false
|
||||
..browserSwipeHinted = j['browserSwipeHinted'] == true
|
||||
..cleanupPrechecked = j['cleanupPrechecked'] is List
|
||||
|
|
@ -863,24 +814,8 @@ class AppSettings {
|
|||
for (final x in (j['ftpServers'] as List?) ?? const [])
|
||||
if (x is Map) FtpServer.fromJson(x),
|
||||
]
|
||||
..rsyncOptions = j['rsyncOptions'] as String? ?? '-avPog'
|
||||
..robocopyOptions = j['robocopyOptions'] as String? ?? '/E /COPY:DAT /DCOPY:T /R:2 /W:2'
|
||||
..copyRunMode = j['copyRunMode'] == 'once' ? 'once' : 'each'
|
||||
..copyBandwidthKBps = ((j['copyBandwidthKBps'] as num?)?.toInt() ?? 0).clamp(0, 10000000)
|
||||
..rsyncSource = j['rsyncSource'] == 'custom' ? 'custom' : 'download'
|
||||
..rsyncPath = j['rsyncPath'] as String? ?? ''
|
||||
..liveSyncPairs = [
|
||||
for (final x in (j['liveSyncPairs'] as List?) ?? const [])
|
||||
if (x is Map && x['source'] is String && x['target'] is String) LiveSyncPair.fromJson(x),
|
||||
]
|
||||
..liveSyncIntervalSec = ((j['liveSyncIntervalSec'] as num?)?.toInt() ?? 30).clamp(5, 3600)
|
||||
// 사용자 결정 (10/8): 기본 켜짐. 예전 기본 (꺼짐) 은 한 번 켜짐으로
|
||||
..runInBackground = j['backgroundV2'] == true ? j['runInBackground'] != false : true
|
||||
..liveSyncOnStart = const ['auto', 'ask', 'off'].contains(j['liveSyncOnStart']) ? j['liveSyncOnStart'] as String : 'auto'
|
||||
..copyTasks = [
|
||||
for (final x in (j['copyTasks'] as List?) ?? const [])
|
||||
if (x is Map && x['dest'] is String) CopyTask.fromJson(x),
|
||||
]
|
||||
..autoCheckUpdates = j['autoCheckUpdates'] as bool? ?? true
|
||||
..lastUpdateCheck = j['lastUpdateCheck'] as String? ?? ''
|
||||
..lastUpdateCheckVersion = j['lastUpdateCheckVersion'] as String? ?? ''
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
|
||||
import 'records.dart';
|
||||
import 'settings_group.dart';
|
||||
|
||||
/// 약속 (설정 A): 환경 설정 > 복사 · 이동 (파일 탐색기) · rsync · 실시간 동기화 (Rsync 화면) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다.
|
||||
/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키)
|
||||
class CopySettings implements SettingsGroup {
|
||||
/// 파일 탐색기의 복사 · 이동 방법: 'builtin' 현재 방식 (기본) · 'robocopy' (Windows). 파일만 고를 때 / 폴더가 들어 있을 때.
|
||||
/// rsync 는 Rsync 화면에서 따로 (파일 탐색기에서는 쓰지 않음)
|
||||
String methodFile = 'builtin';
|
||||
|
||||
/// 48: 복사 · 이동할 때 받는 폴더에 같은 이름이 있으면 'ask' (확인 창에서 고름, 처음 값) · 'rename' · 'overwrite' · 'skip'
|
||||
String conflict = 'ask';
|
||||
|
||||
/// 229: 앱이 직접 하는 복사 · 이동 (현재 방식 · WebDAV 의 안의 것 맞추기) 에서 받는 쪽이 더 새것이면 건너뛴다 (rsync -u 와 같음).
|
||||
/// 처음 값 켜짐 - 예전 원본으로 새 파일을 덮어쓰고 (이동이면 원본까지 지워) 새 내용을 잃었다 (관리자 판단)
|
||||
bool skipNewer = true;
|
||||
|
||||
String methodFolder = 'builtin';
|
||||
|
||||
String rsyncOptions = '-avPog';
|
||||
|
||||
String robocopyOptions = '/E /COPY:DAT /DCOPY:T /R:2 /W:2';
|
||||
|
||||
/// 여러 개를 고르면: 'each' 항목마다 따로 실행 (기본) · 'once' 한 번에 (rsync)
|
||||
String runMode = 'each';
|
||||
|
||||
/// 속도 제한 KB/s (0 = 제한 없음). 모든 방법에 (rsync --bwlimit, 현재 방식은 앱이 조절, robocopy 는 /IPG 로 비슷하게)
|
||||
int bandwidthKBps = 0;
|
||||
|
||||
/// rsync 가져오기: 'download' 앱에 들어 있는 것 (기본, Windows 는 없으면 내려받기) · 'custom' 직접 지정한 실행 파일 ([rsyncPath])
|
||||
String rsyncSource = 'download';
|
||||
|
||||
String rsyncPath = '';
|
||||
|
||||
/// 실시간 동기화 (lsyncd 처럼)
|
||||
List<LiveSyncPair> liveSyncPairs = [];
|
||||
|
||||
/// 모니터링 (Rsync 화면 가운데 [모니터링]): 실행한 rsync 를 기억해 다시 실행 · 옵션 고치기 · lsync 로 옮기기
|
||||
List<CopyTask> tasks = [];
|
||||
|
||||
/// Rsync 화면에서 마지막으로 연 폴더 (왼쪽 · 오른쪽)
|
||||
List<String> rsyncPaths = [];
|
||||
|
||||
/// 92 (Windows): 실시간 동기화가 멈췄을 때 Windows 알림
|
||||
bool syncStopToast = true;
|
||||
|
||||
/// 실시간 동기화 확인 간격 (초). Windows 는 바뀌면 바로, 그 밖은 이 간격으로 살핀다.
|
||||
int liveSyncIntervalSec = 30;
|
||||
|
||||
/// 앱을 다시 켤 때 실시간 동기화: 'auto' 바로 시작 / 'ask' 골라서 시작 / 'off' 시작 안 함 (모니터링에서 시작)
|
||||
String liveSyncOnStart = 'auto';
|
||||
|
||||
/// 파일 탐색기의 복사 방법 (rsync 는 Rsync 화면으로 옮겨 예전 값 rsync 도 현재 방식으로)
|
||||
static String _method(Object? v) => v == 'robocopy' ? 'robocopy' : 'builtin';
|
||||
|
||||
@override
|
||||
void write(Map<String, Object?> j, {required bool plainSecrets}) {
|
||||
j['copyMethodFile'] = methodFile;
|
||||
j['copyConflict'] = conflict;
|
||||
j['copySkipNewer'] = skipNewer;
|
||||
j['copyMethodFolder'] = methodFolder;
|
||||
j['rsyncOptions'] = rsyncOptions;
|
||||
j['robocopyOptions'] = robocopyOptions;
|
||||
j['copyRunMode'] = runMode;
|
||||
j['copyBandwidthKBps'] = bandwidthKBps;
|
||||
j['rsyncSource'] = rsyncSource;
|
||||
j['rsyncPath'] = rsyncPath;
|
||||
j['liveSyncPairs'] = [for (final x in liveSyncPairs) x.toJson()];
|
||||
j['liveSyncIntervalSec'] = liveSyncIntervalSec;
|
||||
j['liveSyncOnStart'] = liveSyncOnStart;
|
||||
j['copyTasks'] = [for (final x in tasks) x.toJson()];
|
||||
j['rsyncPaths'] = rsyncPaths;
|
||||
j['syncStopToast'] = syncStopToast;
|
||||
}
|
||||
|
||||
@override
|
||||
void read(Map<String, dynamic> j) {
|
||||
methodFile = _method(j['copyMethodFile']);
|
||||
conflict = const ['rename', 'overwrite', 'skip'].contains(j['copyConflict']) ? j['copyConflict'] as String : 'ask';
|
||||
skipNewer = j['copySkipNewer'] != false;
|
||||
methodFolder = _method(j['copyMethodFolder']);
|
||||
rsyncPaths = [for (final x in (j['rsyncPaths'] as List?) ?? const []) '$x'];
|
||||
syncStopToast = j['syncStopToast'] != false;
|
||||
rsyncOptions = j['rsyncOptions'] as String? ?? '-avPog';
|
||||
robocopyOptions = j['robocopyOptions'] as String? ?? '/E /COPY:DAT /DCOPY:T /R:2 /W:2';
|
||||
runMode = j['copyRunMode'] == 'once' ? 'once' : 'each';
|
||||
bandwidthKBps = ((j['copyBandwidthKBps'] as num?)?.toInt() ?? 0).clamp(0, 10000000);
|
||||
rsyncSource = j['rsyncSource'] == 'custom' ? 'custom' : 'download';
|
||||
rsyncPath = j['rsyncPath'] as String? ?? '';
|
||||
liveSyncPairs = [
|
||||
for (final x in (j['liveSyncPairs'] as List?) ?? const [])
|
||||
if (x is Map && x['source'] is String && x['target'] is String) LiveSyncPair.fromJson(x),
|
||||
];
|
||||
liveSyncIntervalSec = ((j['liveSyncIntervalSec'] as num?)?.toInt() ?? 30).clamp(5, 3600);
|
||||
liveSyncOnStart = const ['auto', 'ask', 'off'].contains(j['liveSyncOnStart']) ? j['liveSyncOnStart'] as String : 'auto';
|
||||
tasks = [
|
||||
for (final x in (j['copyTasks'] as List?) ?? const [])
|
||||
if (x is Map && x['dest'] is String) CopyTask.fromJson(x),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -148,7 +148,7 @@ Future<void> main(List<String> args) async {
|
|||
await setupMasterLock(controller); // 124: 저장된 비밀번호를 쓰기 전에 (실시간 동기화보다 먼저)
|
||||
// 실시간 동기화 (환경 설정 > 파일 탐색기): 앱이 켜져 있는 동안. 다시 켤 때 바로 / 골라서 / 시작 안 함
|
||||
await findBundledRsync(); // 앱에 들어 있는 rsync
|
||||
final live = LiveSync(controller)..start(hold: controller.settings.liveSyncOnStart != 'auto');
|
||||
final live = LiveSync(controller)..start(hold: controller.settings.copy.liveSyncOnStart != 'auto');
|
||||
// 화면 언어 (환경 설정 > 화면 언어)
|
||||
i18n.init(controller, dataDir);
|
||||
await i18n.apply(controller.settings.uiLanguage, save: false);
|
||||
|
|
@ -286,7 +286,7 @@ Future<void> main(List<String> args) async {
|
|||
?.showSnackBar(SnackBar(content: Text(trf('다운로드 추가: {0}', [t.source])))));
|
||||
// 92: 트레이에 둔 채 실시간 동기화가 멈추면 Windows 알림 · 트레이 툴팁, 누르면 모니터링 > lsync 카드로
|
||||
final syncAlert = SyncAlert(live, services.shell,
|
||||
toastEnabled: () => controller.settings.syncStopToast,
|
||||
toastEnabled: () => controller.settings.copy.syncStopToast,
|
||||
openLsync: () {
|
||||
final nav = navigatorKey.currentState;
|
||||
if (nav != null) unawaited(nav.push(MaterialPageRoute<void>(builder: (_) => MonitorPage(c: controller, initialTab: 1))));
|
||||
|
|
@ -399,7 +399,7 @@ Future<void> main(List<String> args) async {
|
|||
if (home != 'mkv' && nav0 != null) {
|
||||
unawaited(AppNavButtons.openPage(nav0, home, c: controller, downloads: downloads, bookmarks: bookmarks));
|
||||
}
|
||||
if (controller.settings.liveSyncOnStart == 'ask' && ctx.mounted) await askLiveSyncStart(ctx, live);
|
||||
if (controller.settings.copy.liveSyncOnStart == 'ask' && ctx.mounted) await askLiveSyncStart(ctx, live);
|
||||
// 설정 파일을 읽지 못했으면 (백업으로 되살렸거나 처음 설정으로 켰으면) 알린다
|
||||
if (ctx.mounted) await showSettingsProblem(ctx, controller);
|
||||
watchSecretIssue(messengerKey, controller);
|
||||
|
|
@ -595,7 +595,7 @@ Future<void> runAndroid(String dataDir) async {
|
|||
await setupMasterLock(controller); // 124: 저장된 비밀번호를 쓰기 전에 (실시간 동기화보다 먼저)
|
||||
// 실시간 동기화 (환경 설정 > 파일 탐색기): 앱이 켜져 있는 동안. 다시 켤 때 바로 / 골라서 / 시작 안 함
|
||||
await findBundledRsync(); // 앱에 들어 있는 rsync
|
||||
final live = LiveSync(controller)..start(hold: controller.settings.liveSyncOnStart != 'auto');
|
||||
final live = LiveSync(controller)..start(hold: controller.settings.copy.liveSyncOnStart != 'auto');
|
||||
// 화면 언어 (환경 설정 > 화면 언어)
|
||||
i18n.init(controller, dataDir);
|
||||
await i18n.apply(controller.settings.uiLanguage, save: false);
|
||||
|
|
@ -703,7 +703,7 @@ Future<void> runAndroid(String dataDir) async {
|
|||
}
|
||||
// 다시 켤 때 실시간 동기화를 골라서 시작
|
||||
final ctx0 = navigatorKey.currentContext;
|
||||
if (controller.settings.liveSyncOnStart == 'ask' && ctx0 != null && ctx0.mounted) await askLiveSyncStart(ctx0, live);
|
||||
if (controller.settings.copy.liveSyncOnStart == 'ask' && ctx0 != null && ctx0.mounted) await askLiveSyncStart(ctx0, live);
|
||||
// 다른 버전을 쓰다가 돌아왔거나 앱을 다시 설치했으면 보관해 둔 설정을 되살릴지 (되살리면 앱을 끝내고 다시 켜 달라고)
|
||||
final current = await _currentVersion(services);
|
||||
final ctxV = navigatorKey.currentContext;
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
|
|||
return null;
|
||||
}
|
||||
var dest = choices.first.$1;
|
||||
var conflict = switch (c.settings.copyConflict) { 'overwrite' => NameConflict.overwrite, 'skip' => NameConflict.skip, _ => NameConflict.rename };
|
||||
var conflict = switch (c.settings.copy.conflict) { 'overwrite' => NameConflict.overwrite, 'skip' => NameConflict.skip, _ => NameConflict.rename };
|
||||
Future<int?> free(String d) async {
|
||||
var x = d;
|
||||
while (!Directory(x).existsSync() && p.dirname(x) != x) {
|
||||
|
|
@ -220,7 +220,7 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
|
|||
// 관리자 판단: 덮어쓴 파일은 휴지통에 남지 않는다 - 되돌릴 수 없다고 알린다 (받는 쪽이 더 새것이면 설정대로 그대로 둠)
|
||||
if (conflict == NameConflict.overwrite)
|
||||
Text(
|
||||
c.settings.copySkipNewer
|
||||
c.settings.copy.skipNewer
|
||||
? tr('덮어쓴 파일은 되돌릴 수 없습니다 (받는 쪽이 더 새 파일은 그대로 둡니다).')
|
||||
: tr('덮어쓴 파일은 되돌릴 수 없습니다.'),
|
||||
style: TextStyle(color: JjColors.warning, fontSize: 12)),
|
||||
|
|
@ -253,13 +253,13 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
|
|||
if (kind == ArchiveKind.zip) {
|
||||
return extractZipPlan(local, dest, plan,
|
||||
conflict: conflict,
|
||||
skipNewer: c.settings.copySkipNewer,
|
||||
skipNewer: c.settings.copy.skipNewer,
|
||||
password: pw,
|
||||
cancelled: cancelled,
|
||||
onProgress: (d, t) => set(t == 0 ? null : d / t));
|
||||
}
|
||||
return extract7zPlan(exe!, local, dest, plan,
|
||||
conflict: conflict, skipNewer: c.settings.copySkipNewer, password: pw, cancelled: cancelled, onProgress: set);
|
||||
conflict: conflict, skipNewer: c.settings.copy.skipNewer, password: pw, cancelled: cancelled, onProgress: set);
|
||||
});
|
||||
say([
|
||||
trf('{0}개를 풀었습니다: {1}', [r.done, vDisplay(dest)]),
|
||||
|
|
|
|||
|
|
@ -125,36 +125,36 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
|||
title: Text(tr('파일 복사 · 이동 방법')),
|
||||
subtitle: Text(tr('파일만 골랐을 때')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: _valid(s.copyMethodFile),
|
||||
value: _valid(s.copy.methodFile),
|
||||
items: _methods(),
|
||||
onChanged: (v) => c.updateSettings((x) => x.copyMethodFile = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.copy.methodFile = v!),
|
||||
),
|
||||
),
|
||||
SettingTile(
|
||||
title: Text(tr('폴더 복사 · 이동 방법')),
|
||||
subtitle: Text(tr('폴더가 들어 있을 때')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: _valid(s.copyMethodFolder),
|
||||
value: _valid(s.copy.methodFolder),
|
||||
items: _methods(),
|
||||
onChanged: (v) => c.updateSettings((x) => x.copyMethodFolder = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.copy.methodFolder = v!),
|
||||
),
|
||||
),
|
||||
_options(tr('robocopy 옵션'), tr('예: /E /COPY:DAT /DCOPY:T /R:2 /W:2. 이동은 /MOVE 를 자동으로 붙임'), s.robocopyOptions,
|
||||
defaultRobocopyOptions, (x, v) => x.robocopyOptions = v),
|
||||
_options(tr('robocopy 옵션'), tr('예: /E /COPY:DAT /DCOPY:T /R:2 /W:2. 이동은 /MOVE 를 자동으로 붙임'), s.copy.robocopyOptions,
|
||||
defaultRobocopyOptions, (x, v) => x.copy.robocopyOptions = v),
|
||||
],
|
||||
// 48: 같은 이름이 있을 때 (앱이 직접 복사 · 이동할 때, robocopy · rsync 는 그 프로그램의 규칙)
|
||||
SettingTile(
|
||||
title: Text(tr('같은 이름이 있을 때')),
|
||||
subtitle: Text(tr('복사 · 이동할 폴더에 같은 이름이 있으면 (현재 방식 · WebDAV)')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: const ['ask', 'rename', 'overwrite', 'skip'].contains(s.copyConflict) ? s.copyConflict : 'ask',
|
||||
value: const ['ask', 'rename', 'overwrite', 'skip'].contains(s.copy.conflict) ? s.copy.conflict : 'ask',
|
||||
items: [
|
||||
DropdownMenuItem(value: 'ask', child: Text(tr('묻기'))),
|
||||
DropdownMenuItem(value: 'rename', child: Text(tr('늘 이름 바꾸기'))),
|
||||
DropdownMenuItem(value: 'overwrite', child: Text(tr('늘 덮어쓰기'))),
|
||||
DropdownMenuItem(value: 'skip', child: Text(tr('늘 건너뛰기'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.copyConflict = v ?? 'ask'),
|
||||
onChanged: (v) => c.updateSettings((x) => x.copy.conflict = v ?? 'ask'),
|
||||
),
|
||||
),
|
||||
_skipNewerTile(),
|
||||
|
|
@ -165,12 +165,12 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
|||
trailing: SizedBox(
|
||||
width: 140,
|
||||
child: TextFormField(
|
||||
initialValue: '${s.copyBandwidthKBps}',
|
||||
initialValue: '${s.copy.bandwidthKBps}',
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: const InputDecoration(isDense: true, border: OutlineInputBorder(), suffixText: 'KB/s'),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onChanged: (v) => c.updateSettings((x) => x.copyBandwidthKBps = int.tryParse(v) ?? 0),
|
||||
onChanged: (v) => c.updateSettings((x) => x.copy.bandwidthKBps = int.tryParse(v) ?? 0),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -180,8 +180,8 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
|||
/// Rsync 화면: rsync 옵션 · rsync 가져오기 · 실시간 동기화 (lsync)
|
||||
/// 229: 받는 쪽이 더 새것이면 건너뛰기 - 파일 탐색기 · Rsync 탭 두 곳에 같은 설정 (Rsync 화면 · 모니터링에서도 쓰여 찾기 쉽게)
|
||||
Widget _skipNewerTile() => SwitchListTile(
|
||||
value: c.settings.copySkipNewer,
|
||||
onChanged: (v) => c.updateSettings((x) => x.copySkipNewer = v),
|
||||
value: c.settings.copy.skipNewer,
|
||||
onChanged: (v) => c.updateSettings((x) => x.copy.skipNewer = v),
|
||||
title: Text(tr('받는 쪽이 더 새것이면 건너뛰기')),
|
||||
subtitle: Text(keepOptionsTogether(tr('현재 방식 · robocopy · WebDAV 로 복사 · 이동할 때 (rsync 의 -u, robocopy 의 /XO 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다'))),
|
||||
);
|
||||
|
|
@ -193,7 +193,7 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
|||
// 111: 이미 실행한 쌍은 기억한 옵션을 쓴다는 것을 알린다
|
||||
_options(tr('rsync 옵션'), tr('예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 속도 제한은 파일 탐색기의 속도 제한을 함께 씀. '
|
||||
'새로 맞추는 폴더 쌍에 씁니다 - 이미 실행한 쌍은 기억한 옵션을 쓰며 모니터링 > 복사 · rsync 에서 따로 고칩니다'),
|
||||
s.rsyncOptions, defaultRsyncOptions, (x, v) => x.rsyncOptions = v, rsync: true),
|
||||
s.copy.rsyncOptions, defaultRsyncOptions, (x, v) => x.copy.rsyncOptions = v, rsync: true),
|
||||
_skipNewerTile(),
|
||||
SettingTile(
|
||||
title: Text(tr('rsync 가져오기')),
|
||||
|
|
@ -201,14 +201,14 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
|||
? tr('확인 중…')
|
||||
: _rsync != null
|
||||
? trf('사용: {0}', [_rsync!])
|
||||
: s.rsyncSource == 'download'
|
||||
: s.copy.rsyncSource == 'download'
|
||||
? (desk
|
||||
? tr('이 설치본에는 rsync 가 들어 있지 않습니다 (rsync 로 복사할 때 · 오른쪽 [내려받기] 로 받습니다)')
|
||||
: tr('이 설치본에는 rsync 가 들어 있지 않습니다. 직접 지정하세요.'))
|
||||
: tr('지정한 파일이 없습니다')),
|
||||
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
// 내려받기 · 내려받은 것 지우기 (앱에 들어 있는 것을 쓰면 보이지 않음)
|
||||
if (desk && s.rsyncSource == 'download' && !_checking && (_rsync == null || _rsync != bundledRsync))
|
||||
if (desk && s.copy.rsyncSource == 'download' && !_checking && (_rsync == null || _rsync != bundledRsync))
|
||||
_rsync == null
|
||||
? TextButton(
|
||||
onPressed: () async {
|
||||
|
|
@ -227,28 +227,28 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
DropdownButton<String>(
|
||||
value: s.rsyncSource,
|
||||
value: s.copy.rsyncSource,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'download', child: Text(tr('앱에 들어 있는 것 (기본)'))),
|
||||
DropdownMenuItem(value: 'custom', child: Text(tr('직접 지정'))),
|
||||
],
|
||||
onChanged: (v) async {
|
||||
await c.updateSettings((x) => x.rsyncSource = v!);
|
||||
await c.updateSettings((x) => x.copy.rsyncSource = v!);
|
||||
await _check();
|
||||
},
|
||||
),
|
||||
]),
|
||||
),
|
||||
if (s.rsyncSource == 'custom')
|
||||
if (s.copy.rsyncSource == 'custom')
|
||||
SettingTile(
|
||||
title: Text(tr('rsync 실행 파일')),
|
||||
subtitle: Text(s.rsyncPath.isEmpty ? tr('고르지 않음') : s.rsyncPath),
|
||||
subtitle: Text(s.copy.rsyncPath.isEmpty ? tr('고르지 않음') : s.copy.rsyncPath),
|
||||
trailing: TextButton(
|
||||
onPressed: () async {
|
||||
final r = await FilePicker.pickFiles(dialogTitle: tr('rsync 실행 파일'));
|
||||
final path = r.isEmpty ? null : r.single.path;
|
||||
if (path == null) return;
|
||||
await c.updateSettings((x) => x.rsyncPath = path);
|
||||
await c.updateSettings((x) => x.copy.rsyncPath = path);
|
||||
await _check();
|
||||
},
|
||||
child: Text(tr('고르기')),
|
||||
|
|
@ -275,8 +275,8 @@ Future<void> removeLiveSyncPair(BuildContext context, AppController c, LiveSyncP
|
|||
ok: tr('지우기'),
|
||||
);
|
||||
if (!ok) return;
|
||||
await c.updateSettings((y) => y.liveSyncPairs = [
|
||||
for (final o in y.liveSyncPairs) if (!(o.source == x.source && o.target == x.target)) o,
|
||||
await c.updateSettings((y) => y.copy.liveSyncPairs = [
|
||||
for (final o in y.copy.liveSyncPairs) if (!(o.source == x.source && o.target == x.target)) o,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -374,7 +374,7 @@ class _LiveSyncTile extends StatelessWidget {
|
|||
if (ok != true) return;
|
||||
if (old == null) {
|
||||
await c.updateSettings((x) =>
|
||||
x.liveSyncPairs = [...x.liveSyncPairs, LiveSyncPair(src!, dst!, method: method, delete: delete, schedule: schedule)]);
|
||||
x.copy.liveSyncPairs = [...x.copy.liveSyncPairs, LiveSyncPair(src!, dst!, method: method, delete: delete, schedule: schedule)]);
|
||||
return;
|
||||
}
|
||||
// 원본 · 대상이 바뀌면 새 쌍 (지우기 확인도 새로 - 42), 아니면 방법 · 지우기 · 일정만
|
||||
|
|
@ -382,21 +382,21 @@ class _LiveSyncTile extends StatelessWidget {
|
|||
final now = same
|
||||
? old.copyWith(method: method, delete: delete, schedule: schedule)
|
||||
: LiveSyncPair(src!, dst!, method: method, delete: delete, schedule: schedule, enabled: old.enabled);
|
||||
await c.updateSettings((x) => x.liveSyncPairs = [
|
||||
for (final o in x.liveSyncPairs) o.source == old.source && o.target == old.target ? now : o,
|
||||
await c.updateSettings((x) => x.copy.liveSyncPairs = [
|
||||
for (final o in x.copy.liveSyncPairs) o.source == old.source && o.target == old.target ? now : o,
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final live = LiveSync.instance;
|
||||
final pairs = c.settings.liveSyncPairs;
|
||||
final pairs = c.settings.copy.liveSyncPairs;
|
||||
Widget body() => Column(children: [
|
||||
SettingTile(
|
||||
title: Text(tr('실시간 동기화 (lsyncd 처럼)')),
|
||||
subtitle: Text(Platform.isWindows
|
||||
? tr('원본 폴더가 바뀌면 곧바로 대상 폴더에 맞춥니다 (앱이 켜져 있는 동안). 시작할 때 한 번 맞춥니다.')
|
||||
: trf('{0}초마다 원본 폴더를 살펴 대상 폴더에 맞춥니다 (앱이 켜져 있는 동안).', [c.settings.liveSyncIntervalSec])),
|
||||
: trf('{0}초마다 원본 폴더를 살펴 대상 폴더에 맞춥니다 (앱이 켜져 있는 동안).', [c.settings.copy.liveSyncIntervalSec])),
|
||||
trailing: TextButton.icon(
|
||||
onPressed: () => _add(context, c),
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
|
|
@ -414,13 +414,13 @@ class _LiveSyncTile extends StatelessWidget {
|
|||
? tr('WebDAV 원본에 씁니다 (PC 의 폴더는 바뀌면 바로 맞춤). 짧을수록 빨리 맞추지만 서버 · 배터리를 더 씁니다.')
|
||||
: tr('짧을수록 빨리 맞추지만 배터리를 더 씁니다.')),
|
||||
trailing: DropdownButton<int>(
|
||||
value: c.settings.liveSyncIntervalSec,
|
||||
value: c.settings.copy.liveSyncIntervalSec,
|
||||
items: [
|
||||
for (final v in {5, 10, 15, 30, 60, 120, 300, 600, 1800, 3600, c.settings.liveSyncIntervalSec}.toList()..sort())
|
||||
for (final v in {5, 10, 15, 30, 60, 120, 300, 600, 1800, 3600, c.settings.copy.liveSyncIntervalSec}.toList()..sort())
|
||||
DropdownMenuItem(value: v, child: Text(v < 60 ? trf('{0}초', [v]) : trf('{0}분', [v ~/ 60]))),
|
||||
],
|
||||
onChanged: (v) {
|
||||
if (v != null) c.updateSettings((x) => x.liveSyncIntervalSec = v);
|
||||
if (v != null) c.updateSettings((x) => x.copy.liveSyncIntervalSec = v);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
@ -452,7 +452,7 @@ class _LiveSyncTile extends StatelessWidget {
|
|||
onPressed: () async {
|
||||
final r = await editSchedule(context, pairs[i].schedule);
|
||||
if (r == null) return;
|
||||
await c.updateSettings((x) => x.liveSyncPairs = [
|
||||
await c.updateSettings((x) => x.copy.liveSyncPairs = [
|
||||
for (var k = 0; k < pairs.length; k++) k == i ? pairs[k].copyWith(schedule: r) : pairs[k],
|
||||
]);
|
||||
},
|
||||
|
|
@ -465,7 +465,7 @@ class _LiveSyncTile extends StatelessWidget {
|
|||
Switch(
|
||||
value: pairs[i].enabled,
|
||||
onChanged: (v) => c.updateSettings(
|
||||
(x) => x.liveSyncPairs = [for (var k = 0; k < pairs.length; k++) k == i ? pairs[k].copyWith(enabled: v) : pairs[k]]),
|
||||
(x) => x.copy.liveSyncPairs = [for (var k = 0; k < pairs.length; k++) k == i ? pairs[k].copyWith(enabled: v) : pairs[k]]),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: tr('고치기'),
|
||||
|
|
@ -501,14 +501,14 @@ class LiveSyncRunOptions extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final s = c.settings;
|
||||
final onStart = DropdownButton<String>(
|
||||
value: s.liveSyncOnStart,
|
||||
value: s.copy.liveSyncOnStart,
|
||||
isDense: dense,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'auto', child: Text(tr('바로 시작 (기본)'))),
|
||||
DropdownMenuItem(value: 'ask', child: Text(tr('골라서 시작'))),
|
||||
DropdownMenuItem(value: 'off', child: Text(tr('시작 안 함 (모니터링에서 시작)'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.liveSyncOnStart = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.copy.liveSyncOnStart = v!),
|
||||
);
|
||||
void setBackground(bool? v) => c.updateSettings((x) => x.backgroundRun = v ?? false);
|
||||
if (dense) {
|
||||
|
|
|
|||
|
|
@ -407,7 +407,7 @@ class ExplorerController extends ChangeNotifier {
|
|||
final i = panes.indexOf(q);
|
||||
final paths = [...panes.map((x) => x.current)];
|
||||
if (rsync) {
|
||||
unawaited(c.updateSettings((s) => s.rsyncPaths = paths));
|
||||
unawaited(c.updateSettings((s) => s.copy.rsyncPaths = paths));
|
||||
} else {
|
||||
final hist = [dir, ...c.settings.explorer.history.where((h) => !samePath(h, dir))].take(30).toList();
|
||||
unawaited(c.updateSettings((s) => s
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class ExplorerRsyncFlow {
|
|||
allowInnerToOuter: ctl.c.settings.allowInnerToOuter,
|
||||
jobs: this.jobs.list,
|
||||
viaDav: l != null && r != null && ExplorerTransfers.viaDav([l, r]),
|
||||
copySkipNewer: ctl.c.settings.copySkipNewer);
|
||||
copySkipNewer: ctl.c.settings.copy.skipNewer);
|
||||
if (run == null) {
|
||||
ctl.host.snack(problem!);
|
||||
if (missing) {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class ExplorerTransfers extends ChangeNotifier {
|
|||
final have = await rsyncExecutable(s);
|
||||
if (have != null) return have;
|
||||
if (!context.mounted) return null;
|
||||
if (s.rsyncSource == 'custom' || !Platform.isWindows) {
|
||||
if (s.copy.rsyncSource == 'custom' || !Platform.isWindows) {
|
||||
ctl.host.snack(tr('rsync 실행 파일을 찾을 수 없습니다. 환경 설정 > 파일 탐색기에서 경로를 확인하세요.'));
|
||||
return null;
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@ class ExplorerTransfers extends ChangeNotifier {
|
|||
// 다 찾기 전에 누르면 이름 바꾸기 (원래 파일을 건드리지 않는 쪽)
|
||||
final clashesFuture = builtin ? findClashes(sources, dest) : Future.value(const <NameClash>[]);
|
||||
// 48: 환경 설정 "같은 이름이 있을 때" 가 늘 하는 것이면 묻지 않고 그대로 (확인 창에 알림만)
|
||||
final always = switch (ctl.c.settings.copyConflict) {
|
||||
final always = switch (ctl.c.settings.copy.conflict) {
|
||||
'rename' => NameConflict.rename,
|
||||
'overwrite' => NameConflict.overwrite,
|
||||
'skip' => NameConflict.skip,
|
||||
|
|
@ -240,7 +240,7 @@ class ExplorerTransfers extends ChangeNotifier {
|
|||
? tr('안쪽 파일을 확인하는 중 …')
|
||||
: (n.data ?? 0) == 0
|
||||
? ''
|
||||
: ctl.c.settings.copySkipNewer
|
||||
: ctl.c.settings.copy.skipNewer
|
||||
? trf('안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)', [n.data])
|
||||
: trf('안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다', [n.data]),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.warning),
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ class _ExplorerPageState extends State<ExplorerPage> implements ExplorerHost, Ex
|
|||
// X-plore 비교 e: 보관 일이 지난 휴지통 항목을 지운다 (뒤에서)
|
||||
if (_flows.deletes.appTrash case final t?) unawaited(t.purgeOlderThan(c.settings.explorer.androidTrashDays).catchError((_) => 0));
|
||||
if (Platform.isAndroid) unawaited(_access.checkAccess());
|
||||
final saved = widget.rsync ? c.settings.rsyncPaths : c.settings.explorer.paths;
|
||||
final saved = widget.rsync ? c.settings.copy.rsyncPaths : c.settings.explorer.paths;
|
||||
// 창마다 따로 읽는다: 한 창이 WebDAV 서버를 기다려도 다른 창 (이 기기 파일) 은 바로 보이게
|
||||
// 165: 지난 폴더가 느린 네트워크 드라이브여도 화면이 멈추지 않게 (뒤에서 확인, 오래 걸리면 그냥 열어 본다 - 못 읽으면 그 창에 이유)
|
||||
final reach = await Future.wait(
|
||||
|
|
|
|||
|
|
@ -120,12 +120,12 @@ class _CopyTaskCardState extends State<CopyTaskCard> {
|
|||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
// 104: 지우기가 들어 있으면 Rsync 화면과 같은 미리 보기를 거친다 (그사이 대상에 생긴 파일을 묻지 않고 지우지 않게).
|
||||
// 210: 이동 (원본 지우기) 도 - 카드의 [실행] 은 따로 묻는 창이 없어 곧바로 원본을 지웠다. 복사만은 지금처럼 바로
|
||||
if (!await confirmDeletingRun(context, t, includeMove: true, skipNewer: widget.c.settings.copySkipNewer) || !mounted) return;
|
||||
if (!await confirmDeletingRun(context, t, includeMove: true, skipNewer: widget.c.settings.copy.skipNewer) || !mounted) return;
|
||||
widget.c.note(trf('rsync 시작 ({0}): {1} → {2}', [tr('모니터링의 [실행]'), t.sources.join(', '), t.dest]));
|
||||
String? exe;
|
||||
if (t.method == 'rsync') {
|
||||
exe = await rsyncExecutable(widget.c.settings);
|
||||
if (exe == null && Platform.isWindows && widget.c.settings.rsyncSource == 'download' && mounted) {
|
||||
if (exe == null && Platform.isWindows && widget.c.settings.copy.rsyncSource == 'download' && mounted) {
|
||||
exe = await installRsyncWithDialog(context);
|
||||
}
|
||||
if (exe == null) {
|
||||
|
|
@ -162,7 +162,7 @@ class _CopyTaskCardState extends State<CopyTaskCard> {
|
|||
content: Text(trf('칸에 보이는 옵션: {0}\n저장된 옵션: {1}\n\n고친 옵션을 저장하고 실행할까요?', [
|
||||
'${_method == 'builtin' ? tr('현재 방식') : _method} ${_options.text.trim()}',
|
||||
// 178: 실제로 쓰는 옵션 (원본 지우기는 이동일 때만)
|
||||
effectiveRunText(t.method, t.options, move: t.move, skipNewer: widget.c.settings.copySkipNewer),
|
||||
effectiveRunText(t.method, t.options, move: t.move, skipNewer: widget.c.settings.copy.skipNewer),
|
||||
])),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('취소'))),
|
||||
|
|
@ -342,7 +342,7 @@ class _CopyTaskCardState extends State<CopyTaskCard> {
|
|||
],
|
||||
onChanged: (v) => setState(() {
|
||||
if (v != _method && _options.text.trim().isEmpty || _options.text.trim() == defaultRsyncOptions || _options.text.trim() == defaultRobocopyOptions) {
|
||||
_options.text = v == 'robocopy' ? s.robocopyOptions : v == 'rsync' ? s.rsyncOptions : '';
|
||||
_options.text = v == 'robocopy' ? s.copy.robocopyOptions : v == 'rsync' ? s.copy.rsyncOptions : '';
|
||||
}
|
||||
_method = v!;
|
||||
}),
|
||||
|
|
@ -363,7 +363,7 @@ class _CopyTaskCardState extends State<CopyTaskCard> {
|
|||
width: 460,
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(
|
||||
'${t.move ? tr('이동') : tr('복사')} · ${trf('실제 실행: {0}', [effectiveRunText(_method, _options.text.trim(), move: t.move, skipNewer: s.copySkipNewer)])}',
|
||||
'${t.move ? tr('이동') : tr('복사')} · ${trf('실제 실행: {0}', [effectiveRunText(_method, _options.text.trim(), move: t.move, skipNewer: s.copy.skipNewer)])}',
|
||||
style: TextStyle(fontSize: 12, color: t.move ? JjColors.warning : JjColors.textDim),
|
||||
),
|
||||
// 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 옵션 (실행은 작업이 막는다)
|
||||
|
|
|
|||
|
|
@ -147,10 +147,10 @@ Future<void> confirmEmptySourceSync(BuildContext context, LiveSync live, LiveSyn
|
|||
if (ok == true) await live.syncEmptyAnyway(x);
|
||||
}
|
||||
|
||||
/// 앱을 다시 켤 때 ([AppSettings.liveSyncOnStart] 'ask'): 어느 실시간 동기화를 시작할지 고르기.
|
||||
/// 앱을 다시 켤 때 ([CopySettings.liveSyncOnStart] 'ask'): 어느 실시간 동기화를 시작할지 고르기.
|
||||
/// 고르지 않은 것은 이번 실행 동안 멈춤 (모니터링 > lsync 에서 ▶ 로 시작).
|
||||
Future<void> askLiveSyncStart(BuildContext context, LiveSync live) async {
|
||||
final pairs = live.c.settings.liveSyncPairs.where((x) => x.enabled).toList();
|
||||
final pairs = live.c.settings.copy.liveSyncPairs.where((x) => x.enabled).toList();
|
||||
if (pairs.isEmpty) return;
|
||||
final pick = {for (final x in pairs) LiveSync.keyOf(x)};
|
||||
final ok = await showDialog<bool>(
|
||||
|
|
@ -190,8 +190,8 @@ Future<void> askLiveSyncStart(BuildContext context, LiveSync live) async {
|
|||
}
|
||||
|
||||
/// 같은 원본 · 대상 쌍만 [now] 로 바꿔 저장 (나머지 순서 그대로)
|
||||
Future<void> replaceLiveSyncPair(AppController c, LiveSyncPair old, LiveSyncPair now) => c.updateSettings((x) => x.liveSyncPairs = [
|
||||
for (final y in x.liveSyncPairs) y.source == old.source && y.target == old.target ? now : y,
|
||||
Future<void> replaceLiveSyncPair(AppController c, LiveSyncPair old, LiveSyncPair now) => c.updateSettings((x) => x.copy.liveSyncPairs = [
|
||||
for (final y in x.copy.liveSyncPairs) y.source == old.source && y.target == old.target ? now : y,
|
||||
]);
|
||||
|
||||
/// 69: 카드의 길게 누르기 · 오른쪽 클릭 메뉴
|
||||
|
|
@ -257,7 +257,7 @@ Future<bool> finalizeLiveSync(BuildContext context, AppController c, LiveSyncPai
|
|||
);
|
||||
if (go != true || !context.mounted) return false;
|
||||
var exe = await rsyncExecutable(c.settings);
|
||||
if (exe == null && Platform.isWindows && c.settings.rsyncSource == 'download' && context.mounted) {
|
||||
if (exe == null && Platform.isWindows && c.settings.copy.rsyncSource == 'download' && context.mounted) {
|
||||
exe = await installRsyncWithDialog(context);
|
||||
}
|
||||
if (exe == null) {
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class _LiveTabState extends State<_LiveTab> {
|
|||
// 열 때 맞출 것을 새로 센다
|
||||
final live = LiveSync.instance;
|
||||
if (live != null) {
|
||||
for (final x in c.settings.liveSyncPairs) {
|
||||
for (final x in c.settings.copy.liveSyncPairs) {
|
||||
live.refreshPending(x);
|
||||
}
|
||||
}
|
||||
|
|
@ -103,7 +103,7 @@ class _LiveTabState extends State<_LiveTab> {
|
|||
return ListenableBuilder(
|
||||
listenable: Listenable.merge([c, ?live]),
|
||||
builder: (context, _) {
|
||||
final pairs = c.settings.liveSyncPairs;
|
||||
final pairs = c.settings.copy.liveSyncPairs;
|
||||
return ListView(padding: const EdgeInsets.all(12), children: [
|
||||
Row(children: [
|
||||
Expanded(
|
||||
|
|
|
|||
|
|
@ -42,13 +42,13 @@ class _RsyncSettingsSectionState extends State<RsyncSettingsSection> {
|
|||
),
|
||||
if (desk)
|
||||
SwitchListTile(
|
||||
value: s.syncStopToast,
|
||||
onChanged: (v) => c.updateSettings((x) => x.syncStopToast = v),
|
||||
value: s.copy.syncStopToast,
|
||||
onChanged: (v) => c.updateSettings((x) => x.copy.syncStopToast = v),
|
||||
title: Text(tr('동기화가 멈추면 Windows 알림')),
|
||||
subtitle: Text(tr('창을 트레이에 둔 채 실시간 동기화가 원본을 못 읽어 멈추면 알림을 띄웁니다 (트레이 툴팁 경고는 늘)')),
|
||||
),
|
||||
// Windows 에서 알림이 꺼져 있으면 알리고 설정을 열어 준다 (바꾸지는 않음)
|
||||
if (desk && s.syncStopToast) const ToastStatusHint(),
|
||||
if (desk && s.copy.syncStopToast) const ToastStatusHint(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,9 +89,9 @@ bool isPlainHttp(String url) => url.trim().toLowerCase().startsWith('http://');
|
|||
List<String> davServerUsers(AppSettings s, String id) {
|
||||
bool on(String path) => isDav(path) && DavPath.parse(path).server == id;
|
||||
return [
|
||||
for (final x in s.liveSyncPairs)
|
||||
for (final x in s.copy.liveSyncPairs)
|
||||
if (on(x.source) || on(x.target)) trf('실시간 동기화: {0} → {1}', [vDisplay(x.source), vDisplay(x.target)]),
|
||||
for (final t in s.copyTasks)
|
||||
for (final t in s.copy.tasks)
|
||||
if (on(t.dest) || t.sources.any(on)) trf('기억된 작업: {0} → {1}', [t.sources.map(vDisplay).join(', '), vDisplay(t.dest)]),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@ void main() {
|
|||
Directory(p.join(tmp.path, 'sub')).createSync();
|
||||
c.settings
|
||||
..explorer.paths = [tmp.path, tmp.path]
|
||||
..rsyncPaths = [tmp.path, tmp.path]
|
||||
..copyTasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, 'sub'), method: 'rsync', options: '-av')]
|
||||
..liveSyncPairs = [LiveSyncPair(tmp.path, p.join(tmp.path, 'x'))];
|
||||
..copy.rsyncPaths = [tmp.path, tmp.path]
|
||||
..copy.tasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, 'sub'), method: 'rsync', options: '-av')]
|
||||
..copy.liveSyncPairs = [LiveSyncPair(tmp.path, p.join(tmp.path, 'x'))];
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: page(c))));
|
||||
for (var i = 0; i < 20; i++) {
|
||||
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ void main() {
|
|||
final tmp = Directory.systemTemp.createTempSync('jj_card_');
|
||||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
final src = p.join(tmp.path, 'movies', 'src'), dst = p.join(tmp.path, 'backup', 'dst');
|
||||
c.settings.liveSyncPairs = [LiveSyncPair(src, dst)];
|
||||
c.settings.copy.liveSyncPairs = [LiveSyncPair(src, dst)];
|
||||
final live = LiveSync(c);
|
||||
LiveSync.instance = live;
|
||||
addTearDown(() {
|
||||
|
|
@ -61,13 +61,13 @@ void main() {
|
|||
expect(find.text('실시간 동기화를 지울까요?'), findsOneWidget);
|
||||
await tester.tap(find.text('취소'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(c.settings.liveSyncPairs, hasLength(1));
|
||||
expect(c.settings.copy.liveSyncPairs, hasLength(1));
|
||||
// 카드의 지우기 버튼 → 확인 → 지움
|
||||
await tester.tap(find.byTooltip('지우기'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(FilledButton, '지우기'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(c.settings.liveSyncPairs, isEmpty);
|
||||
expect(c.settings.copy.liveSyncPairs, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('46: MKV 목록 "모두 지우기" 는 확인 뒤 · 45: 대기 작업이 있으면 지금 작업만 / 모두 고르기', (tester) async {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ void main() {
|
|||
test('기본값 = 정한 답 · 저장 · 읽기', () {
|
||||
final s = AppSettings();
|
||||
expect([
|
||||
s.startMenuShortcut, s.syncStopToast, s.swipeWrap, s.migrationNotice, s.allowInnerToOuter,
|
||||
s.startMenuShortcut, s.copy.syncStopToast, s.swipeWrap, s.migrationNotice, s.allowInnerToOuter,
|
||||
s.rememberPasswords, s.recycleOnDelete, s.readerSystemBars,
|
||||
], everyElement(isTrue));
|
||||
expect(s.cleanupPrechecked, ['work', 'download']);
|
||||
|
|
@ -37,12 +37,12 @@ void main() {
|
|||
expect([s.webPauseOnLeave, s.autoCheckUpdates, s.youtubeAdSkip, s.youtubeAdHide, s.runInBackground, s.swipeNav],
|
||||
everyElement(isTrue));
|
||||
// 탐색기 처음 배치 = 두 창 (규칙 7 · 관리자 결정 3, 10/22)
|
||||
expect([s.liveSyncOnStart, s.liveSyncIntervalSec, s.explorer.layout, s.zipComic], ['auto', 30, 'dual', false]);
|
||||
expect([s.copy.liveSyncOnStart, s.copy.liveSyncIntervalSec, s.explorer.layout, s.zipComic], ['auto', 30, 'dual', false]);
|
||||
expect([AppSettings.uiScaleMin, AppSettings.uiScaleMax], [0.5, 2.5]);
|
||||
// 바꾼 값은 남는다
|
||||
final b = AppSettings.fromJson((AppSettings()
|
||||
..startMenuShortcut = false
|
||||
..syncStopToast = false
|
||||
..copy.syncStopToast = false
|
||||
..swipeWrap = false
|
||||
..migrationNotice = false
|
||||
..allowInnerToOuter = false
|
||||
|
|
@ -52,7 +52,7 @@ void main() {
|
|||
..tripleTapAction = 'none'
|
||||
..davExternalOpen = 'url')
|
||||
.toJson());
|
||||
expect([b.startMenuShortcut, b.syncStopToast, b.swipeWrap, b.migrationNotice, b.allowInnerToOuter, b.rememberPasswords,
|
||||
expect([b.startMenuShortcut, b.copy.syncStopToast, b.swipeWrap, b.migrationNotice, b.allowInnerToOuter, b.rememberPasswords,
|
||||
b.recycleOnDelete], everyElement(isFalse));
|
||||
expect([b.cleanupPrechecked, b.tripleTapAction, b.davExternalOpen], [['logs'], 'none', 'url']);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -90,17 +90,17 @@ void main() {
|
|||
..createSync(recursive: true)
|
||||
..writeAsStringSync('o');
|
||||
final pair = LiveSyncPair(src, dst, delete: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
await tester.runAsync(() => live.syncNow(pair));
|
||||
expect(live.toDelete[LiveSync.keyOf(pair)], ['only.txt']);
|
||||
await host(tester, (ctx) => confirmLiveDeletes(ctx, live, c.settings.liveSyncPairs.single));
|
||||
await host(tester, (ctx) => confirmLiveDeletes(ctx, live, c.settings.copy.liveSyncPairs.single));
|
||||
await dismiss(tester, how == 'cancel' ? 'back' : how); // 이 창의 "닫기" 는 [나중에]
|
||||
await settle(tester, () => false, rounds: 10);
|
||||
expect(File(p.join(dst, 'only.txt')).existsSync(), isTrue);
|
||||
expect(c.settings.liveSyncPairs.single.delete, isTrue);
|
||||
expect(c.settings.liveSyncPairs.single.deleteConfirmed, isFalse);
|
||||
expect(c.settings.copy.liveSyncPairs.single.delete, isTrue);
|
||||
expect(c.settings.copy.liveSyncPairs.single.deleteConfirmed, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('70 원본이 빈 쌍의 [그래도 맞추기] 창 ($how) → 지우지 않음', (tester) async {
|
||||
|
|
@ -110,7 +110,7 @@ void main() {
|
|||
..createSync(recursive: true)
|
||||
..writeAsStringSync('k');
|
||||
final pair = LiveSyncPair(src, dst, delete: true).copyWith(deleteConfirmed: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
await tester.runAsync(() => live.syncNow(pair));
|
||||
|
|
@ -151,7 +151,7 @@ void main() {
|
|||
final keep = File(p.join(dst, 'keep.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('k');
|
||||
c.settings.copyTasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-av --delete')];
|
||||
c.settings.copy.tasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-av --delete')];
|
||||
tester.view.physicalSize = const Size(1500, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -175,7 +175,7 @@ void main() {
|
|||
..createSync(recursive: true)
|
||||
..writeAsStringSync('a');
|
||||
Directory(dst).createSync();
|
||||
c.settings.copyTasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, move: true, method: 'builtin')];
|
||||
c.settings.copy.tasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, move: true, method: 'builtin')];
|
||||
tester.view.physicalSize = const Size(1500, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -278,7 +278,7 @@ void main() {
|
|||
..createSync(recursive: true)
|
||||
..writeAsStringSync('a');
|
||||
Directory(dst).createSync();
|
||||
c.settings.copyTasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'robocopy', options: '/E')];
|
||||
c.settings.copy.tasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'robocopy', options: '/E')];
|
||||
tester.view.physicalSize = const Size(1500, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -297,11 +297,11 @@ void main() {
|
|||
|
||||
testWidgets('237: 모니터링 카드 실패 글은 예외 이름 없이, 원문 (명령 줄 포함) 은 [자세히] 에서 보고 복사', (tester) async {
|
||||
const raw = 'ProcessException: 원본 파일을 지우지 못했습니다\n Command: rsync -avPog --remove-source-files a/ b/';
|
||||
c.settings.copyTasks = [
|
||||
c.settings.copy.tasks = [
|
||||
const CopyTask(id: '1', sources: ['a'], dest: 'b', method: 'rsync', options: '-avPog', move: true)
|
||||
.copyWith(lastResult: 'failed', lastRun: DateTime(2026, 10, 12, 9).toIso8601String(), lastMessage: '원본 파일을 지우지 못했습니다', lastDetail: raw),
|
||||
];
|
||||
expect(CopyTask.fromJson(c.settings.copyTasks.single.toJson()).lastDetail, raw);
|
||||
expect(CopyTask.fromJson(c.settings.copy.tasks.single.toJson()).lastDetail, raw);
|
||||
tester.view.physicalSize = const Size(1500, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -339,7 +339,7 @@ void main() {
|
|||
final src = p.join(tmp.path, 'mv243'), dst = p.join(tmp.path, 'mv243d');
|
||||
Directory(src).createSync();
|
||||
Directory(dst).createSync();
|
||||
c.settings.copyTasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, move: true, method: 'builtin')];
|
||||
c.settings.copy.tasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, move: true, method: 'builtin')];
|
||||
tester.view.physicalSize = const Size(1500, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -350,12 +350,12 @@ void main() {
|
|||
expect(find.textContaining('실시간 동기화는 원본을 지우지 않습니다 (복사로 맞춤)'), findsOneWidget);
|
||||
await tester.tap(find.text('취소').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(c.settings.liveSyncPairs, isEmpty, reason: '취소하면 옮기지 않음');
|
||||
expect(c.settings.copy.liveSyncPairs, isEmpty, reason: '취소하면 옮기지 않음');
|
||||
await tester.tap(find.text('lsync 로 이동').first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(FilledButton, '옮기기'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(c.settings.liveSyncPairs.map((x) => x.source), [src]);
|
||||
expect(c.settings.copy.liveSyncPairs.map((x) => x.source), [src]);
|
||||
});
|
||||
|
||||
testWidgets('105: 원본이 대상 안 + 창에서 [원본 파일 지우기] 로 지우기 옵션이 생기면 막는다', (tester) async {
|
||||
|
|
@ -365,9 +365,9 @@ void main() {
|
|||
..writeAsStringSync('n');
|
||||
final center = CopyCenter.of(c);
|
||||
c.settings
|
||||
..rsyncOptions = '-avPog'
|
||||
..copy.rsyncOptions = '-avPog'
|
||||
..explorer.layout = 'single'
|
||||
..rsyncPaths = [outer, tmp.path];
|
||||
..copy.rsyncPaths = [outer, tmp.path];
|
||||
// 이동 (원본 파일 지우기) 작업만 --delete 를 기억
|
||||
await center.remember([inner], outer, contents: true, method: 'rsync', move: true).then((t) => center.update(t.copyWith(options: '-avPog --delete')));
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
|
|
@ -404,7 +404,7 @@ void main() {
|
|||
final a = p.join(tmp.path, 'ca'), b = p.join(tmp.path, 'cb');
|
||||
Directory(a).createSync(recursive: true);
|
||||
Directory(b).createSync(recursive: true);
|
||||
c.settings.copyTasks = [
|
||||
c.settings.copy.tasks = [
|
||||
CopyTask(id: '1', sources: [a], dest: b, contents: true, method: 'rsync', options: '-avPog --remove-source-files'),
|
||||
CopyTask(id: '2', sources: [b], dest: a, contents: true, move: true, method: 'rsync', options: '-avPog'),
|
||||
];
|
||||
|
|
@ -427,7 +427,7 @@ void main() {
|
|||
File(p.join(dst, 'keep.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('k');
|
||||
c.settings.copyTasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-av')];
|
||||
c.settings.copy.tasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-av')];
|
||||
tester.view.physicalSize = const Size(1500, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -441,7 +441,7 @@ void main() {
|
|||
expect(find.text('고친 옵션을 저장하지 않았습니다'), findsOneWidget);
|
||||
await tester.tap(find.text('저장하고 실행'));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(c.settings.copyTasks.single.options, '-av --delete');
|
||||
expect(c.settings.copy.tasks.single.options, '-av --delete');
|
||||
// 저장한 옵션에 지우기가 있으므로 미리 보기 창
|
||||
expect(find.text('지우기가 들어 있는 작업입니다'), findsOneWidget);
|
||||
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 200);
|
||||
|
|
|
|||
|
|
@ -158,8 +158,8 @@ void main() {
|
|||
|
||||
testWidgets('178 보충: 설정의 원본 지우기 옵션은 복사 창에서 빠진 까닭을 알린다 (이동 창에는 없음)', (tester) async {
|
||||
c.settings
|
||||
..copyMethodFile = 'robocopy'
|
||||
..robocopyOptions = '/E /MOV';
|
||||
..copy.methodFile = 'robocopy'
|
||||
..copy.robocopyOptions = '/E /MOV';
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||||
await settle(tester, () => false, rounds: 15);
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('48 보충: 환경 설정이 "늘 덮어쓰기" 면 묻지 않고 (확인 창에 알림만) 덮어쓴다', (tester) async {
|
||||
c.settings.copyConflict = 'overwrite';
|
||||
c.settings.copy.conflict = 'overwrite';
|
||||
await startCopy(tester);
|
||||
expect(find.text('덮어쓰기 (원래 파일은 없어집니다)'), findsNothing, reason: '고르는 칸 없음');
|
||||
expect(find.textContaining('환경 설정대로 덮어씁니다'), findsOneWidget);
|
||||
|
|
@ -121,7 +121,7 @@ void main() {
|
|||
File(p.join(right, 'D', 'same.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('NEWER');
|
||||
c.settings.copyConflict = 'overwrite';
|
||||
c.settings.copy.conflict = 'overwrite';
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -167,7 +167,7 @@ void main() {
|
|||
|
||||
testWidgets('52: 복사 중에 화면을 떠났다 돌아와도 진행 막대가 다시 보이고, 끝나면 받는 폴더를 새로 읽는다 (.jjpart 가 남아 보이지 않음)', (tester) async {
|
||||
File(p.join(left, 'big.bin')).writeAsBytesSync(List.filled(150 * 1024, 3));
|
||||
c.settings.copyBandwidthKBps = 60; // 약 2.5초 걸리게
|
||||
c.settings.copy.bandwidthKBps = 60; // 약 2.5초 걸리게
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ void main() {
|
|||
File(p.join(right, 'rsub', 'back.txt')).writeAsStringSync('b'); // ⇄ 비교에서 ← 로 건너갈 것
|
||||
c.settings
|
||||
..explorer.layout = 'single' // 파일 탐색기 배치와 상관없이 두 창
|
||||
..rsyncPaths = [left, right];
|
||||
..copy.rsyncPaths = [left, right];
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -257,7 +257,7 @@ void main() {
|
|||
expect(find.textContaining('-avPog -u'), findsNWidgets(2));
|
||||
await tester.tap(find.text('취소'));
|
||||
await calm(tester);
|
||||
expect(c.settings.copyTasks, isEmpty);
|
||||
expect(c.settings.copy.tasks, isEmpty);
|
||||
expect(find.text(keepOptionsTogether('원본 파일 지우기 (--remove-source-files)')), findsNothing); // 양쪽에는 없음
|
||||
// → 는 한 방향 · -u 없이 · 원본 파일 지우기를 고를 수 있다 (고르면 원본 폴더 남김 / 지움)
|
||||
await act(tester, () => tester.tap(find.text('좌 → 우')));
|
||||
|
|
@ -276,7 +276,7 @@ void main() {
|
|||
await tester.tap(find.text('취소'));
|
||||
await calm(tester);
|
||||
// 고른 폴더는 기억 (다음에 열 때 그 폴더부터)
|
||||
expect(c.settings.rsyncPaths, [p.join(left, 'sub'), p.join(right, 'rsub')]);
|
||||
expect(c.settings.copy.rsyncPaths, [p.join(left, 'sub'), p.join(right, 'rsub')]);
|
||||
|
||||
// 새 폴더: 만든 폴더가 바로 그 창의 원본 · 대상으로 골라진다 (오른쪽 창 rsub 안에)
|
||||
await act(tester, () => tester.tap(find.text('rsub')));
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ void main() {
|
|||
File(p.join(left, 'e.txt')).writeAsStringSync('e');
|
||||
c.settings
|
||||
..explorer.dropAction = 'move'
|
||||
..copyConflict = 'overwrite';
|
||||
..copy.conflict = 'overwrite';
|
||||
await open(tester);
|
||||
// 전체 시험 중 (부하) 에는 목록을 늦게 읽어 e.txt 를 못 찾았다 - 두 파일이 보일 때까지
|
||||
await settle(tester, () => find.text('e.txt').evaluate().isNotEmpty && find.text('doc.txt').evaluate().isNotEmpty);
|
||||
|
|
@ -178,7 +178,7 @@ void main() {
|
|||
..writeAsStringSync('mine');
|
||||
c.settings
|
||||
..explorer.dropAction = 'move'
|
||||
..copyConflict = 'overwrite';
|
||||
..copy.conflict = 'overwrite';
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('선택').first));
|
||||
await act(tester, () => tester.tap(markOf('doc.txt')));
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ void main() {
|
|||
|
||||
test('Rsync 실행: 막는 이유 · 쌍 · ⇄ 의 옵션 (← 는 지우지 않음 · -u) · 원본 지우기 · 미리 보기 (실제 폴더)', () async {
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings.rsyncOptions = '-a --delete';
|
||||
c.settings.copy.rsyncOptions = '-a --delete';
|
||||
final center = CopyCenter.of(c);
|
||||
final l = p.join(tmp.path, 'L'), r = p.join(tmp.path, 'R');
|
||||
Directory(l).createSync();
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorer.paths = [left, right]
|
||||
..copyBandwidthKBps = 512; // 큰 파일이 6초쯤 걸리게
|
||||
..copy.bandwidthKBps = 512; // 큰 파일이 6초쯤 걸리게
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -70,7 +70,7 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorer.orientation = 'side'
|
||||
..rsyncPaths = [l, r];
|
||||
..copy.rsyncPaths = [l, r];
|
||||
tester.view.physicalSize = const Size(400, 860);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -110,7 +110,7 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorer.orientation = 'side'
|
||||
..rsyncPaths = [l, r];
|
||||
..copy.rsyncPaths = [l, r];
|
||||
tester.view.physicalSize = const Size(412, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -146,7 +146,7 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorer.orientation = 'side'
|
||||
..rsyncPaths = [l, r];
|
||||
..copy.rsyncPaths = [l, r];
|
||||
tester.view.physicalSize = const Size(860, 400);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -168,7 +168,7 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorer.orientation = 'side'
|
||||
..rsyncPaths = [p.join(inner, 'L'), p.join(sd, 'R')];
|
||||
..copy.rsyncPaths = [p.join(inner, 'L'), p.join(sd, 'R')];
|
||||
tester.view.physicalSize = const Size(412, 880);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -194,7 +194,7 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorer.layout = 'single'
|
||||
..rsyncPaths = [l, r];
|
||||
..copy.rsyncPaths = [l, r];
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ void main() {
|
|||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
c.settings.rsyncPaths = [left, right];
|
||||
c.settings.copy.rsyncPaths = [left, right];
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c, rsync: true))));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||||
|
|
@ -203,9 +203,9 @@ void main() {
|
|||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('거꾸로')));
|
||||
await settle(tester, () => c.settings.rsyncPaths.length == 2 && c.settings.rsyncPaths[0] == right && c.settings.rsyncPaths[1] == left,
|
||||
await settle(tester, () => c.settings.copy.rsyncPaths.length == 2 && c.settings.copy.rsyncPaths[0] == right && c.settings.copy.rsyncPaths[1] == left,
|
||||
rounds: 100);
|
||||
expect(c.settings.rsyncPaths, [right, left]);
|
||||
expect(c.settings.copy.rsyncPaths, [right, left]);
|
||||
});
|
||||
|
||||
testWidgets('74: 고르기 도구 - 개수 · 합계 크기, 반전, 패턴, 같은 확장자, 범위 (Shift · 길게 누르기)', (tester) async {
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ void main() {
|
|||
c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), shell: NoopShell()));
|
||||
c.settings
|
||||
..explorer.paths = [left, right]
|
||||
..copyMethodFile = 'builtin'
|
||||
..copyMethodFolder = 'builtin';
|
||||
..copy.methodFile = 'builtin'
|
||||
..copy.methodFolder = 'builtin';
|
||||
});
|
||||
tearDown(() async {
|
||||
for (var i = 0;; i++) {
|
||||
|
|
|
|||
|
|
@ -122,8 +122,8 @@ void main() {
|
|||
final s = DavServer(id: 'nas', name: '집 NAS', url: server.url, user: 'user', password: 'pass');
|
||||
c.settings
|
||||
..webdavServers = [s]
|
||||
..rsyncOptions = '-av --remove-source-files'
|
||||
..rsyncPaths = [left, 'dav://nas/'];
|
||||
..copy.rsyncOptions = '-av --remove-source-files'
|
||||
..copy.rsyncPaths = [left, 'dav://nas/'];
|
||||
DavRegistry.configure([s]);
|
||||
// 1280×720 (낮은 창) 에서도 체크 · 경고가 버튼에 가려지지 않게
|
||||
await pump(tester, ExplorerPage(device: device, c: c, rsync: true), size: const Size(1280, 720));
|
||||
|
|
@ -193,7 +193,7 @@ void main() {
|
|||
final s = DavServer(id: 'nas', name: '집 NAS', url: server.url, user: 'user', password: 'pass');
|
||||
c.settings
|
||||
..webdavServers = [s]
|
||||
..rsyncPaths = [left, 'dav://nas/'];
|
||||
..copy.rsyncPaths = [left, 'dav://nas/'];
|
||||
DavRegistry.configure([s]);
|
||||
await pump(tester, ExplorerPage(device: device, c: c, rsync: true));
|
||||
await settle(tester, () => find.text('sub').evaluate().isNotEmpty && find.text('원격').evaluate().isNotEmpty);
|
||||
|
|
@ -216,6 +216,6 @@ void main() {
|
|||
await settle(tester, () => got.existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty, rounds: 200);
|
||||
expect(got.readAsStringSync(), 'inner');
|
||||
expect(File(p.join(remote.path, '원격', 'note.txt')).existsSync(), isTrue); // 지우기 없이
|
||||
expect(c.settings.copyTasks.single.dest, 'dav://nas/원격'); // 모니터링에 남음
|
||||
expect(c.settings.copy.tasks.single.dest, 'dav://nas/원격'); // 모니터링에 남음
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,9 +53,9 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorer.paths = [tmp.path, tmp.path]
|
||||
..rsyncPaths = [tmp.path, tmp.path]
|
||||
..copyTasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, 'sub'), method: 'rsync', options: '-av')]
|
||||
..liveSyncPairs = [LiveSyncPair(tmp.path, p.join(tmp.path, 'x'))];
|
||||
..copy.rsyncPaths = [tmp.path, tmp.path]
|
||||
..copy.tasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, 'sub'), method: 'rsync', options: '-av')]
|
||||
..copy.liveSyncPairs = [LiveSyncPair(tmp.path, p.join(tmp.path, 'x'))];
|
||||
c.videos.add(VideoItem(p.join(tmp.path, '아주 긴 이름의 동영상 파일 - 시험용 2026.mp4')));
|
||||
c.selected = c.videos.first;
|
||||
final d = DownloadManager(backends: const [], settings: () => c.settings, readClipboard: () async => null);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:jj_mkvmaker/app/settings.dart';
|
|||
/// 97: 실시간 동기화의 지우기는 쌍의 "지우기 포함" 으로만 (설정 옵션의 --delete · --del · /MIR · /PURGE 는 빼고)
|
||||
void main() {
|
||||
test('rsync: 설정 옵션의 지우기는 빼고, 쌍이 지우기 포함일 때만 --delete 하나', () {
|
||||
final s = AppSettings()..rsyncOptions = '-avPog --delete --del --delete-after';
|
||||
final s = AppSettings()..copy.rsyncOptions = '-avPog --delete --del --delete-after';
|
||||
final off = LiveSync.rsyncArgsFor(LiveSyncPair('/a', '/b'), s, windows: false);
|
||||
expect(off.where((o) => o.startsWith('--del')), isEmpty);
|
||||
final on = LiveSync.rsyncArgsFor(LiveSyncPair('/a', '/b', delete: true), s, windows: false);
|
||||
|
|
@ -13,7 +13,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('10/20: --delete-excluded 는 쌍이 지우기 포함일 때만 (지울지 묻는 목록과 같게)', () {
|
||||
final s = AppSettings()..rsyncOptions = '-av --exclude=*.tmp --delete-excluded';
|
||||
final s = AppSettings()..copy.rsyncOptions = '-av --exclude=*.tmp --delete-excluded';
|
||||
final off = LiveSync.rsyncArgsFor(LiveSyncPair('/a', '/b'), s, windows: false);
|
||||
expect(off.where((o) => o.startsWith('--del')), isEmpty);
|
||||
final on = LiveSync.rsyncArgsFor(LiveSyncPair('/a', '/b', delete: true), s, windows: false);
|
||||
|
|
@ -22,7 +22,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('robocopy: /MIR 는 /E 로, /PURGE 는 쌍이 지우기 포함일 때만', () {
|
||||
final s = AppSettings()..robocopyOptions = '/MIR /PURGE /R:2';
|
||||
final s = AppSettings()..copy.robocopyOptions = '/MIR /PURGE /R:2';
|
||||
final off = LiveSync.robocopyArgsFor(LiveSyncPair(r'C:\a', r'D:\b'), s).map((o) => o.toUpperCase()).toList();
|
||||
expect(off, isNot(contains('/MIR')));
|
||||
expect(off, isNot(contains('/PURGE')));
|
||||
|
|
@ -33,8 +33,8 @@ void main() {
|
|||
|
||||
test('107: 원본을 지우는 옵션 (--remove-source-files · /MOV · /MOVE) 도 실시간 동기화에서는 쓰지 않는다', () {
|
||||
final s = AppSettings()
|
||||
..rsyncOptions = '-avPog --remove-source-files --remove-sent-files'
|
||||
..robocopyOptions = '/E /MOV /MOVE';
|
||||
..copy.rsyncOptions = '-avPog --remove-source-files --remove-sent-files'
|
||||
..copy.robocopyOptions = '/E /MOV /MOVE';
|
||||
final r = LiveSync.rsyncArgsFor(LiveSyncPair('/a', '/b', delete: true), s, windows: false);
|
||||
expect(r.where((o) => o.startsWith('--remove')), isEmpty);
|
||||
final w = LiveSync.robocopyArgsFor(LiveSyncPair(r'C:', r'D:', delete: true), s).map((o) => o.toUpperCase()).toList();
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
// 앱에 든 rsync 를 그대로 (시험 환경에는 앱 폴더 (path_provider) 가 없어 '직접 지정' 으로)
|
||||
c.settings
|
||||
..rsyncSource = 'custom'
|
||||
..rsyncPath = rsync;
|
||||
..copy.rsyncSource = 'custom'
|
||||
..copy.rsyncPath = rsync;
|
||||
final bad = <String>[];
|
||||
for (final opts in sceneOptions) {
|
||||
writeScene(src, want);
|
||||
|
|
@ -40,9 +40,9 @@ void main() {
|
|||
writeScene(src, got);
|
||||
final before = treeOf(got);
|
||||
final gone = [for (final k in before.keys) if (!w.containsKey(k)) k];
|
||||
c.settings.rsyncOptions = opts;
|
||||
c.settings.copy.rsyncOptions = opts;
|
||||
final pair = LiveSyncPair(src, got, method: 'rsync', delete: optionsDelete(opts));
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
final k = LiveSync.keyOf(pair);
|
||||
final why = <String>[];
|
||||
|
|
@ -64,7 +64,7 @@ void main() {
|
|||
if (!RsyncFilter.parse(opts).deleteExcluded && excl.isNotEmpty) why.add('--delete-excluded 가 없는데 따로 묶음: $excl');
|
||||
if (excl.toSet().difference(asked.toSet()).isNotEmpty) why.add('따로 묶은 것이 지울 목록에 없음: $excl');
|
||||
if (excl.toSet().length != wantExcl.length || !excl.toSet().containsAll(wantExcl)) why.add('따로 묶은 것 $excl ≠ $wantExcl');
|
||||
if (asked.isNotEmpty) await live.decideDelete(c.settings.liveSyncPairs.single, delete: true);
|
||||
if (asked.isNotEmpty) await live.decideDelete(c.settings.copy.liveSyncPairs.single, delete: true);
|
||||
why.addAll(treeDiff(w, treeOf(got)));
|
||||
if ((live.pending[k] ?? const []).isNotEmpty) why.add('맞춘 뒤에도 맞출 것: ${live.pending[k]}');
|
||||
live.dispose();
|
||||
|
|
@ -86,11 +86,11 @@ void main() {
|
|||
}
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..rsyncSource = 'custom'
|
||||
..rsyncPath = rsync
|
||||
..rsyncOptions = '-av --exclude=*.tmp --delete-excluded';
|
||||
..copy.rsyncSource = 'custom'
|
||||
..copy.rsyncPath = rsync
|
||||
..copy.rsyncOptions = '-av --exclude=*.tmp --delete-excluded';
|
||||
final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
await tester.runAsync(() => live.syncNow(pair));
|
||||
|
|
@ -130,29 +130,29 @@ void main() {
|
|||
}
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..rsyncSource = 'custom'
|
||||
..rsyncPath = rsync
|
||||
..rsyncOptions = '-av --exclude=*.tmp';
|
||||
..copy.rsyncSource = 'custom'
|
||||
..copy.rsyncPath = rsync
|
||||
..copy.rsyncOptions = '-av --exclude=*.tmp';
|
||||
// 이미 이 옵션으로 확인한 쌍
|
||||
c.settings.liveSyncPairs = [
|
||||
c.settings.copy.liveSyncPairs = [
|
||||
LiveSyncPair(src, dst, method: 'rsync', delete: true, deleteConfirmed: true, confirmedOptions: '-av --exclude=*.tmp'),
|
||||
];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
LiveSyncPair pair() => c.settings.liveSyncPairs.single;
|
||||
LiveSyncPair pair() => c.settings.copy.liveSyncPairs.single;
|
||||
final k = LiveSync.keyOf(pair());
|
||||
await live.syncNow(pair());
|
||||
expect(live.toDelete[k], isNull);
|
||||
expect(File(p.join(dst, 'a.txt')).existsSync(), isTrue);
|
||||
|
||||
// ① 지울 것이 늘지 않는 바꿈 (-h 더하기) → 묻지 않고 지금 옵션을 기억
|
||||
c.settings.rsyncOptions = '-avh --exclude=*.tmp';
|
||||
c.settings.copy.rsyncOptions = '-avh --exclude=*.tmp';
|
||||
await live.syncNow(pair());
|
||||
expect(live.toDelete[k], isNull);
|
||||
expect(pair().confirmedOptions, '-avh --exclude=*.tmp');
|
||||
|
||||
// ② --delete-excluded 를 더함 → 거른 것이 새로 지워지므로 다시 묻고, 확인 전에는 지우지 않음
|
||||
c.settings.rsyncOptions = '-avh --exclude=*.tmp --delete-excluded';
|
||||
c.settings.copy.rsyncOptions = '-avh --exclude=*.tmp --delete-excluded';
|
||||
await live.syncNow(pair());
|
||||
expect(live.toDelete[k], unorderedEquals(['only.tmp', 'keep']));
|
||||
expect(live.reasked, contains(k));
|
||||
|
|
@ -167,7 +167,7 @@ void main() {
|
|||
expect(live.reasked, isNot(contains(k)));
|
||||
|
||||
// ③ 거르기를 빼면 (--exclude 를 지움) 지울 것은 줄거나 같다 → 묻지 않음
|
||||
c.settings.rsyncOptions = '-avh';
|
||||
c.settings.copy.rsyncOptions = '-avh';
|
||||
await live.syncNow(pair());
|
||||
expect(live.toDelete[k], isNull);
|
||||
expect(pair().confirmedOptions, '-avh');
|
||||
|
|
@ -175,15 +175,15 @@ void main() {
|
|||
// ④ 예전에 확인한 쌍 (기억한 옵션 없음): --delete-excluded 로 새로 지워질 것이 있으면 묻는다 · 없으면 기억만
|
||||
File(p.join(dst, 'new.tmp')).writeAsStringSync('n');
|
||||
c.settings
|
||||
..rsyncOptions = '-av --exclude=*.tmp --delete-excluded'
|
||||
..liveSyncPairs = [LiveSyncPair.fromJson({'source': src, 'target': dst, 'method': 'rsync', 'delete': true})];
|
||||
..copy.rsyncOptions = '-av --exclude=*.tmp --delete-excluded'
|
||||
..copy.liveSyncPairs = [LiveSyncPair.fromJson({'source': src, 'target': dst, 'method': 'rsync', 'delete': true})];
|
||||
expect(pair().confirmedOptions, isNull);
|
||||
await live.syncNow(pair());
|
||||
expect(live.toDelete[k], ['new.tmp']);
|
||||
expect(File(p.join(dst, 'new.tmp')).existsSync(), isTrue);
|
||||
c.settings
|
||||
..rsyncOptions = '-av --exclude=*.tmp'
|
||||
..liveSyncPairs = [LiveSyncPair.fromJson({'source': src, 'target': dst, 'method': 'rsync', 'delete': true})];
|
||||
..copy.rsyncOptions = '-av --exclude=*.tmp'
|
||||
..copy.liveSyncPairs = [LiveSyncPair.fromJson({'source': src, 'target': dst, 'method': 'rsync', 'delete': true})];
|
||||
await live.syncNow(pair());
|
||||
expect(live.toDelete[k], isNull);
|
||||
expect(pair().confirmedOptions, '-av --exclude=*.tmp');
|
||||
|
|
@ -219,8 +219,8 @@ void main() {
|
|||
}
|
||||
c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..rsyncSource = 'custom'
|
||||
..rsyncPath = sceneRsync ?? '';
|
||||
..copy.rsyncSource = 'custom'
|
||||
..copy.rsyncPath = sceneRsync ?? '';
|
||||
});
|
||||
tearDown(() => tmp.deleteSync(recursive: true));
|
||||
bool there(String rel) => File(p.join(dst, rel)).existsSync();
|
||||
|
|
@ -248,14 +248,14 @@ void main() {
|
|||
expect(r.exitCode, 0, reason: '${r.stderr}');
|
||||
expect(File(p.join(probe, 'a.txt')).existsSync(), isFalse, reason: '실제 rsync 는 지움');
|
||||
|
||||
c.settings.rsyncOptions = opts;
|
||||
c.settings.copy.rsyncOptions = opts;
|
||||
final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true, deleteConfirmed: confirmed, confirmedOptions: confirmed ? opts : null);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
final k = LiveSync.keyOf(pair);
|
||||
for (var i = 0; i < 2; i++) {
|
||||
await live.syncNow(c.settings.liveSyncPairs.single);
|
||||
await live.syncNow(c.settings.copy.liveSyncPairs.single);
|
||||
expect(live.status[k]!.$2, isNot(contains('실패')));
|
||||
expect([there('a.txt'), there('old.log'), there('extra.txt')], [true, true, true], reason: '자동 맞추기는 지우지 않음 ($i)');
|
||||
expect(there('b.txt') && there('c.log'), isTrue, reason: '보내기는 함');
|
||||
|
|
@ -264,13 +264,13 @@ void main() {
|
|||
expect(live.pending[k]!.where((d) => d.startsWith('− ')), isEmpty, reason: '− 줄이 늘 남지 않음');
|
||||
}
|
||||
// [지금 지우기 (확인)] - 창에 보일 목록은 미리 본 것 (a.txt 는 없음 - 그래서 빨간 경고와 체크)
|
||||
final (list, _) = await live.previewDeletes(c.settings.liveSyncPairs.single);
|
||||
final (list, _) = await live.previewDeletes(c.settings.copy.liveSyncPairs.single);
|
||||
expect(list, unorderedEquals(['old.log', 'extra.txt']));
|
||||
await live.deleteNow(c.settings.liveSyncPairs.single);
|
||||
await live.deleteNow(c.settings.copy.liveSyncPairs.single);
|
||||
expect([there('old.log'), there('extra.txt')], [false, false]);
|
||||
// 다음 자동 맞추기는 다시 보내기만
|
||||
File(p.join(dst, 'extra2.txt')).writeAsStringSync('x');
|
||||
await live.syncNow(c.settings.liveSyncPairs.single);
|
||||
await live.syncNow(c.settings.copy.liveSyncPairs.single);
|
||||
expect(there('extra2.txt'), isTrue);
|
||||
expect(live.noAutoDelete[k], isNotEmpty);
|
||||
});
|
||||
|
|
@ -279,19 +279,19 @@ void main() {
|
|||
|
||||
test('관리자 지시 (10/20): P (지키기) 는 이제 따른다 - 묻는 목록에 지킨 것 없음 · 확인 뒤 남음 · 다시 묻지 않음', () async {
|
||||
if (sceneRsync == null) return markTestSkipped('앱에 든 rsync 가 없음');
|
||||
c.settings.rsyncOptions = '-a --filter="P old.log"';
|
||||
c.settings.copy.rsyncOptions = '-a --filter="P old.log"';
|
||||
final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
final k = LiveSync.keyOf(pair);
|
||||
await live.syncNow(pair);
|
||||
expect(live.noAutoDelete[k], isNull);
|
||||
expect(live.toDelete[k], ['extra.txt'], reason: 'old.log 는 지킴');
|
||||
await live.decideDelete(c.settings.liveSyncPairs.single, delete: true);
|
||||
await live.decideDelete(c.settings.copy.liveSyncPairs.single, delete: true);
|
||||
expect((there('old.log'), there('extra.txt')), (true, false));
|
||||
for (var i = 0; i < 2; i++) {
|
||||
await live.syncNow(c.settings.liveSyncPairs.single);
|
||||
await live.syncNow(c.settings.copy.liveSyncPairs.single);
|
||||
expect(live.toDelete[k], isNull, reason: '다시 묻지 않음 ($i)');
|
||||
expect(live.pending[k], isEmpty);
|
||||
}
|
||||
|
|
@ -299,9 +299,9 @@ void main() {
|
|||
|
||||
test('미리 볼 수 있는 규칙만이면 예전처럼 (자동 지우기 · 묻기)', () async {
|
||||
if (sceneRsync == null) return markTestSkipped('앱에 든 rsync 가 없음');
|
||||
c.settings.rsyncOptions = '-a --exclude=*.log';
|
||||
c.settings.copy.rsyncOptions = '-a --exclude=*.log';
|
||||
final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
await live.syncNow(pair);
|
||||
|
|
@ -312,9 +312,9 @@ void main() {
|
|||
|
||||
testWidgets('[지금 지우기 (확인)] 창: 빨간 "미리 볼 수 없음" · 체크해야 [지우고 맞추기] · [지우지 않기] 는 없음', (tester) async {
|
||||
if (sceneRsync == null) return markTestSkipped('앱에 든 rsync 가 없음');
|
||||
c.settings.rsyncOptions = '-a --filter="merge C:/jj_rules/rules.txt"'; // 아직 따르지 못하는 규칙 (H 는 이제 따름) · 목록 파일은 전체 경로
|
||||
c.settings.copy.rsyncOptions = '-a --filter="merge C:/jj_rules/rules.txt"'; // 아직 따르지 못하는 규칙 (H 는 이제 따름) · 목록 파일은 전체 경로
|
||||
final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true, deleteConfirmed: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
late BuildContext ctx;
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ void main() {
|
|||
() async {
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
await live.syncNow(pair);
|
||||
expect(live.status[LiveSync.keyOf(pair)]!.$2, '원본을 읽을 수 없어 멈춤');
|
||||
|
|
@ -65,7 +65,7 @@ void main() {
|
|||
..writeAsStringSync('a');
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final pair = LiveSyncPair(src, dst, delete: true); // 새 쌍: deleteConfirmed false
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
await live.syncNow(pair);
|
||||
final k = LiveSync.keyOf(pair);
|
||||
|
|
@ -74,8 +74,8 @@ void main() {
|
|||
expect(live.toDelete[k], unorderedEquals(['keep1.txt', 'sub']));
|
||||
expect(live.status[k]!.$2, contains('확인 필요'));
|
||||
// 지우기 포함으로 맞추기 (확인)
|
||||
await live.decideDelete(c.settings.liveSyncPairs.single, delete: true);
|
||||
expect(c.settings.liveSyncPairs.single.deleteConfirmed, isTrue);
|
||||
await live.decideDelete(c.settings.copy.liveSyncPairs.single, delete: true);
|
||||
expect(c.settings.copy.liveSyncPairs.single.deleteConfirmed, isTrue);
|
||||
expect(File(p.join(dst, 'keep1.txt')).existsSync(), isFalse);
|
||||
expect(live.toDelete[k], isNull);
|
||||
live.dispose();
|
||||
|
|
|
|||
|
|
@ -54,10 +54,10 @@ void main() {
|
|||
final a = LiveSyncPair(p.join(tmp.path, 'a'), p.join(tmp.path, 'A'));
|
||||
final b = LiveSyncPair(p.join(tmp.path, 'b'), p.join(tmp.path, 'B'));
|
||||
final d = LiveSyncPair(p.join(tmp.path, 'd'), p.join(tmp.path, 'D'));
|
||||
c.settings.liveSyncPairs = [a, b, d];
|
||||
c.settings.copy.liveSyncPairs = [a, b, d];
|
||||
await replaceLiveSyncPair(c, b, b.copyWith(enabled: false));
|
||||
expect(c.settings.liveSyncPairs.map((x) => x.source), [a.source, b.source, d.source]);
|
||||
expect(c.settings.liveSyncPairs.map((x) => x.enabled), [true, false, true]);
|
||||
expect(c.settings.copy.liveSyncPairs.map((x) => x.source), [a.source, b.source, d.source]);
|
||||
expect(c.settings.copy.liveSyncPairs.map((x) => x.enabled), [true, false, true]);
|
||||
});
|
||||
|
||||
testWidgets('copy_task_card: 작업이 없으면 안내, 있으면 작업마다 카드', (tester) async {
|
||||
|
|
@ -76,7 +76,7 @@ void main() {
|
|||
testWidgets('live_pair_card: 화면 일은 부르는 쪽 콜백으로 (최종 정리 · 복사 목록으로)', (tester) async {
|
||||
final c = _controller();
|
||||
final x = LiveSyncPair(tmp.path, p.join(tmp.path, 'x'), method: 'builtin');
|
||||
c.settings.liveSyncPairs = [x];
|
||||
c.settings.copy.liveSyncPairs = [x];
|
||||
tester.view.physicalSize = const Size(1500, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -95,12 +95,12 @@ void main() {
|
|||
// 스위치는 replaceLiveSyncPair 로 저장
|
||||
await tester.tap(find.byType(Switch));
|
||||
await tester.pump();
|
||||
expect(c.settings.liveSyncPairs.single.enabled, isFalse);
|
||||
expect(c.settings.copy.liveSyncPairs.single.enabled, isFalse);
|
||||
// 복사 목록으로 옮기면 lsync 쌍은 빠지고 복사 작업이 생긴 뒤 부르는 쪽 콜백
|
||||
await tester.tap(find.text('복사 · rsync 로 이동'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(shown, 1);
|
||||
expect(c.settings.liveSyncPairs, isEmpty);
|
||||
expect(c.settings.copyTasks.single.dest, x.target);
|
||||
expect(c.settings.copy.liveSyncPairs, isEmpty);
|
||||
expect(c.settings.copy.tasks.single.dest, x.target);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ void main() {
|
|||
final single = File(p.join(tmp.path, 'single.txt'))..writeAsStringSync('s');
|
||||
final center = CopyCenter(c);
|
||||
final t = CopyTask(id: '9', sources: [src, single.path], dest: dst, move: true, method: 'rsync', prune: 'all', once: true);
|
||||
await c.updateSettings((x) => x.copyTasks = [t]);
|
||||
await c.updateSettings((x) => x.copy.tasks = [t]);
|
||||
expect(await center.toLiveSync(center.tasks.single), 1);
|
||||
final rest = center.tasks.single;
|
||||
expect(rest.sources, [single.path]);
|
||||
|
|
@ -96,7 +96,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('같은 복사는 기억한 옵션 · 실행 결과 기록 · lsync 로 이동 · 다시 복사 목록으로', () async {
|
||||
c.settings.copyMethodFolder = 'builtin'; // rsync 실행은 sync_tools_test (시험 환경에서는 path_provider 없음)
|
||||
c.settings.copy.methodFolder = 'builtin'; // rsync 실행은 sync_tools_test (시험 환경에서는 path_provider 없음)
|
||||
final center = CopyCenter(c);
|
||||
final t = await center.remember([src], dst);
|
||||
await center.update(t.copyWith(options: '-a --custom', bandwidthKBps: 77));
|
||||
|
|
@ -114,12 +114,12 @@ void main() {
|
|||
// lsync 로: 원본 → 대상\이름
|
||||
expect(await center.toLiveSync(center.tasks.single), 1);
|
||||
expect(center.tasks, isEmpty);
|
||||
final pair = c.settings.liveSyncPairs.single;
|
||||
final pair = c.settings.copy.liveSyncPairs.single;
|
||||
expect([pair.source, pair.target], [src, p.join(dst, 'src')]);
|
||||
|
||||
// 다시 복사 목록으로 (안의 것 → 대상)
|
||||
final back = await center.fromLiveSync(pair);
|
||||
expect(c.settings.liveSyncPairs, isEmpty);
|
||||
expect(c.settings.copy.liveSyncPairs, isEmpty);
|
||||
expect([back.contents, back.dest], [true, p.join(dst, 'src')]);
|
||||
expect(back.method, Platform.isWindows ? 'rsync' : 'builtin'); // rsync 로 옮겨 온다 (쓸 수 있으면)
|
||||
File(p.join(src, 'new.txt')).writeAsStringSync('n');
|
||||
|
|
@ -139,7 +139,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('기억한 복사의 원본이 없어졌거나 자기 안으로면 시작하지 않고 실패로 남긴다', () async {
|
||||
c.settings.copyMethodFolder = 'rsync';
|
||||
c.settings.copy.methodFolder = 'rsync';
|
||||
final center = CopyCenter(c);
|
||||
final t = await center.remember([src], src); // 자기 안으로
|
||||
expect(await center.start(t), isNull);
|
||||
|
|
@ -157,7 +157,7 @@ void main() {
|
|||
final pair = LiveSyncPair(src, target, schedule: ['0 $off * * *'], delete: true);
|
||||
expect(await LiveSync.diff(pair), unorderedEquals(['a.txt', 'sub/b.txt']));
|
||||
expect(LiveSync.activeNow(pair), isFalse);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c)..start();
|
||||
// 모아서 3초 뒤 (컴퓨터가 바쁘면 늦을 수 있어 정해진 4초가 아니라 될 때까지 - 최대 20초)
|
||||
for (var i = 0; i < 80 && (live.pending[LiveSync.keyOf(pair)]?.length ?? 0) < 2; i++) {
|
||||
|
|
@ -191,10 +191,10 @@ void main() {
|
|||
test('설정 저장 · 읽기, Windows 는 종료 동작과 같은 값', () {
|
||||
final s = AppSettings()
|
||||
..runInBackground = true
|
||||
..liveSyncOnStart = 'ask';
|
||||
..copy.liveSyncOnStart = 'ask';
|
||||
final r = AppSettings.fromJson(s.toJson());
|
||||
expect([r.runInBackground, r.liveSyncOnStart], [true, 'ask']);
|
||||
expect(AppSettings.fromJson({'liveSyncOnStart': 'x'}).liveSyncOnStart, 'auto');
|
||||
expect([r.runInBackground, r.copy.liveSyncOnStart], [true, 'ask']);
|
||||
expect(AppSettings.fromJson({'liveSyncOnStart': 'x'}).copy.liveSyncOnStart, 'auto');
|
||||
// 사용자 결정 (10/8): Android 백그라운드로 실행은 기본 켜짐. 사용자가 끈 값 (새 형식) 은 그대로
|
||||
expect(AppSettings.fromJson({}).runInBackground, isTrue);
|
||||
expect(AppSettings.fromJson({'runInBackground': false, 'backgroundV2': true}).runInBackground, isFalse);
|
||||
|
|
@ -218,9 +218,9 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final pa = LiveSyncPair(a.path, p.join(tmp.path, 'ta')), pb = LiveSyncPair(b.path, p.join(tmp.path, 'tb'));
|
||||
c.settings
|
||||
..liveSyncPairs = [pa, pb]
|
||||
..liveSyncOnStart = 'off';
|
||||
final live = LiveSync(c)..start(hold: c.settings.liveSyncOnStart != 'auto');
|
||||
..copy.liveSyncPairs = [pa, pb]
|
||||
..copy.liveSyncOnStart = 'off';
|
||||
final live = LiveSync(c)..start(hold: c.settings.copy.liveSyncOnStart != 'auto');
|
||||
// 맞추기 (rsync 프로세스) 가 끝난 뒤 닫고 지운다 - 도는 중에 지우면 "다른 프로세스가 사용 중" (전체 시험이 무거울 때)
|
||||
addTearDown(() async {
|
||||
for (var i = 0; i < 200 && live.anyRunning; i++) {
|
||||
|
|
@ -264,7 +264,7 @@ void main() {
|
|||
Directory(p.join(t.path, 'onlydir')).createSync();
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final pair = LiveSyncPair(a.path, t.path, method: 'builtin');
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
await live.refreshPending(pair);
|
||||
|
|
@ -283,7 +283,7 @@ void main() {
|
|||
File(p.join(a.path, '1.txt')).writeAsStringSync('1');
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final pair = LiveSyncPair(a.path, p.join(tmp.path, 't'), method: 'builtin');
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
final running = live.syncNow(pair);
|
||||
live.dispose();
|
||||
|
|
@ -297,8 +297,8 @@ void main() {
|
|||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..copyTasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, '..'), method: 'builtin')]
|
||||
..liveSyncPairs = [LiveSyncPair(tmp.path, p.join(tmp.path, 'x'), schedule: const ['0 9-17 * * 1-5'])];
|
||||
..copy.tasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, '..'), method: 'builtin')]
|
||||
..copy.liveSyncPairs = [LiveSyncPair(tmp.path, p.join(tmp.path, 'x'), schedule: const ['0 9-17 * * 1-5'])];
|
||||
tester.view.physicalSize = const Size(1500, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -318,7 +318,7 @@ void main() {
|
|||
expect(find.text('설정 반영 후 실행'), findsOneWidget);
|
||||
await tester.tap(find.text('현재 유지'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(c.settings.copyTasks.single.bandwidthKBps, 500);
|
||||
expect(c.settings.copy.tasks.single.bandwidthKBps, 500);
|
||||
expect(CopyCenter.of(c).jobs, isEmpty);
|
||||
|
||||
// lsync 탭: 일정 요약 · 맞추기 버튼
|
||||
|
|
@ -333,7 +333,7 @@ void main() {
|
|||
final tmp = Directory.systemTemp.createTempSync('jj_monbad_');
|
||||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings.copyTasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, '..'), method: 'rsync', options: '-av delete')];
|
||||
c.settings.copy.tasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, '..'), method: 'rsync', options: '-av delete')];
|
||||
tester.view.physicalSize = const Size(1500, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -353,7 +353,7 @@ void main() {
|
|||
final tmp = Directory.systemTemp.createTempSync('jj_mon233_');
|
||||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings.copyTasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, '..'), move: true, method: 'builtin')];
|
||||
c.settings.copy.tasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, '..'), move: true, method: 'builtin')];
|
||||
tester.view.physicalSize = const Size(1500, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -372,6 +372,6 @@ void main() {
|
|||
expect(find.text('받는 쪽이 더 새것이면 건너뛰기'), findsOneWidget);
|
||||
await tester.tap(find.text('받는 쪽이 더 새것이면 건너뛰기'));
|
||||
await tester.pump();
|
||||
expect(c.settings.copySkipNewer, isFalse);
|
||||
expect(c.settings.copy.skipNewer, isFalse);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,8 +182,8 @@ void main() {
|
|||
});
|
||||
|
||||
test('229: 앱 직접 이동 - 받는 쪽이 더 새것이면 덮어쓰지 않고 원본도 남긴다 (환경 설정 처음 값 켜짐)', () async {
|
||||
expect(AppSettings.fromJson({}).copySkipNewer, isTrue);
|
||||
expect(AppSettings.fromJson((AppSettings()..copySkipNewer = false).toJson()).copySkipNewer, isFalse);
|
||||
expect(AppSettings.fromJson({}).copy.skipNewer, isTrue);
|
||||
expect(AppSettings.fromJson((AppSettings()..copy.skipNewer = false).toJson()).copy.skipNewer, isFalse);
|
||||
final old = File(p.join(src, 'older.txt'))..writeAsStringSync('old in src');
|
||||
old.setLastModifiedSync(DateTime.now().subtract(const Duration(days: 2)));
|
||||
File(p.join(dst, 'older.txt')).writeAsStringSync('NEW version in dst');
|
||||
|
|
|
|||
|
|
@ -225,14 +225,14 @@ void main() {
|
|||
final (s, d) = scene('ls', 'ld', ['a.txt'], ['old.txt']);
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..rsyncSource = 'custom'
|
||||
..rsyncPath = rsync
|
||||
..rsyncOptions = '-a'
|
||||
..liveSyncPairs = [LiveSyncPair(unc(s), unc(d), method: 'rsync', delete: true, deleteConfirmed: true, confirmedOptions: '-a')];
|
||||
..copy.rsyncSource = 'custom'
|
||||
..copy.rsyncPath = rsync
|
||||
..copy.rsyncOptions = '-a'
|
||||
..copy.liveSyncPairs = [LiveSyncPair(unc(s), unc(d), method: 'rsync', delete: true, deleteConfirmed: true, confirmedOptions: '-a')];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
await live.syncNow(c.settings.liveSyncPairs.single);
|
||||
expect(live.status[LiveSync.keyOf(c.settings.liveSyncPairs.single)]!.$2, isNot(contains('실패')));
|
||||
await live.syncNow(c.settings.copy.liveSyncPairs.single);
|
||||
expect(live.status[LiveSync.keyOf(c.settings.copy.liveSyncPairs.single)]!.$2, isNot(contains('실패')));
|
||||
expect(treeOf(d).keys.toSet(), {'a.txt'});
|
||||
}, skip: Platform.isWindows ? false : 'Windows 만 (MSYS2 rsync)');
|
||||
|
||||
|
|
@ -241,13 +241,13 @@ void main() {
|
|||
final (s, d) = scene("s'1", "d'2", ['a.txt'], ["it's.txt", "Bob's.txt", 'old.txt']);
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..rsyncSource = 'custom'
|
||||
..rsyncPath = rsync
|
||||
..rsyncOptions = '-a --exclude="it\'s.txt" --exclude="Bob\'s.txt"'
|
||||
..liveSyncPairs = [LiveSyncPair(s, d, method: 'rsync', delete: true, deleteConfirmed: true, confirmedOptions: '-a --exclude="it\'s.txt" --exclude="Bob\'s.txt"')];
|
||||
..copy.rsyncSource = 'custom'
|
||||
..copy.rsyncPath = rsync
|
||||
..copy.rsyncOptions = '-a --exclude="it\'s.txt" --exclude="Bob\'s.txt"'
|
||||
..copy.liveSyncPairs = [LiveSyncPair(s, d, method: 'rsync', delete: true, deleteConfirmed: true, confirmedOptions: '-a --exclude="it\'s.txt" --exclude="Bob\'s.txt"')];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
final pair = c.settings.liveSyncPairs.single;
|
||||
final pair = c.settings.copy.liveSyncPairs.single;
|
||||
await live.syncNow(pair);
|
||||
expect(live.status[LiveSync.keyOf(pair)]!.$2, isNot(contains('실패')));
|
||||
final t = treeOf(d);
|
||||
|
|
|
|||
|
|
@ -159,15 +159,15 @@ void main() {
|
|||
// ③ · ④ 실시간 동기화 (새 쌍 - 지울 것이 있으면 묻고, 확인하면 맞춤)
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..rsyncSource = 'custom'
|
||||
..rsyncPath = rsync
|
||||
..rsyncOptions = opts
|
||||
..copy.rsyncSource = 'custom'
|
||||
..copy.rsyncPath = rsync
|
||||
..copy.rsyncOptions = opts
|
||||
..webdavServers = [dav()];
|
||||
for (final (label, target, local) in [('실시간 (로컬)', p.join(tmp.path, 'live'), true), ('실시간 (원격)', 'dav://$id/l', false)]) {
|
||||
final dir = local ? target : p.join(remote.path, 'l');
|
||||
writeScene(src, dir, srcFiles: _src, dstFiles: _dst);
|
||||
final pair = LiveSyncPair(src, target, method: 'rsync', delete: delete);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
final k = LiveSync.keyOf(pair);
|
||||
await live.syncNow(pair);
|
||||
|
|
@ -180,7 +180,7 @@ void main() {
|
|||
for (final a in asked) {
|
||||
if (!gone.contains(a)) why.add('$label: rsync 는 지우지 않는데 물음: $a');
|
||||
}
|
||||
if (asked.isNotEmpty) await live.decideDelete(c.settings.liveSyncPairs.single, delete: true);
|
||||
if (asked.isNotEmpty) await live.decideDelete(c.settings.copy.liveSyncPairs.single, delete: true);
|
||||
for (final d in treeDiff(w, treeOf(dir))) {
|
||||
why.add('$label: $d');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ void main() {
|
|||
c.settings
|
||||
..webdavServers = [s]
|
||||
..explorer.layout = 'single'
|
||||
..rsyncPaths = [left, 'dav://nas/'];
|
||||
..copy.rsyncPaths = [left, 'dav://nas/'];
|
||||
DavRegistry.configure([s]);
|
||||
});
|
||||
tearDown(() async {
|
||||
|
|
@ -68,7 +68,7 @@ void main() {
|
|||
|
||||
testWidgets('100 · 95: WebDAV ⇄ + --delete - 미리 보기대로 → (지움) 다음 ← (지우지 않음), 한쪽에만 있던 새 파일이 양쪽에서 사라지지 않는다',
|
||||
(tester) async {
|
||||
c.settings.rsyncOptions = '-avPog --delete';
|
||||
c.settings.copy.rsyncOptions = '-avPog --delete';
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('좌 ⇄ 우')));
|
||||
await waitCompare(tester);
|
||||
|
|
@ -92,7 +92,7 @@ void main() {
|
|||
|
||||
testWidgets('96: 원본 파일 지우기를 켜면 확인 창 · 미리 보기가 실제 실행할 (이동) 작업의 옵션으로 바뀐다', (tester) async {
|
||||
// 기억해 둔 (복사) 작업은 --delete 없음, 새 작업의 기본 옵션은 --delete 있음
|
||||
c.settings.rsyncOptions = '-avPog --delete';
|
||||
c.settings.copy.rsyncOptions = '-avPog --delete';
|
||||
await open(tester);
|
||||
await CopyCenter.of(c)
|
||||
.remember([p.join(left, 'sub')], 'dav://nas/rsub', contents: true, method: 'rsync')
|
||||
|
|
@ -115,7 +115,7 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('97: [다시 비교] 하는 동안은 지우는 실행을 누를 수 없다', (tester) async {
|
||||
c.settings.rsyncOptions = '-avPog --del';
|
||||
c.settings.copy.rsyncOptions = '-avPog --del';
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('좌 → 우')));
|
||||
await waitCompare(tester);
|
||||
|
|
@ -150,13 +150,13 @@ void main() {
|
|||
await settle(tester, () => false, rounds: 60);
|
||||
expect(File(p.join(remote.path, 'rsub', 'a.txt')).existsSync(), isFalse);
|
||||
expect(File(p.join(left, 'sub', 'b.txt')).existsSync(), isFalse);
|
||||
expect(c.settings.copyTasks, isEmpty);
|
||||
expect(c.settings.copy.tasks, isEmpty);
|
||||
await tester.pump(const Duration(seconds: 20));
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('110: 좁은 화면 - 비교가 끝나도 [취소] · [실행] 자리가 그대로, 창을 여러 번 열고 닫아도 실행되지 않음', (tester) async {
|
||||
c.settings.rsyncOptions = '-avPog --delete';
|
||||
c.settings.copy.rsyncOptions = '-avPog --delete';
|
||||
HttpOverrides.global = null;
|
||||
tester.view.physicalSize = const Size(900, 1400); // 세로로 든 태블릿
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
|
|||
|
|
@ -67,12 +67,12 @@ void main() {
|
|||
|
||||
test('124-1: 지우려는 서버를 쓰는 실시간 동기화 · 기억된 작업을 찾는다', () {
|
||||
final s = AppSettings()
|
||||
..liveSyncPairs = [
|
||||
..copy.liveSyncPairs = [
|
||||
LiveSyncPair(r'C:\영상', 'dav://nas/백업'),
|
||||
LiveSyncPair(r'C:\a', r'D:\b'),
|
||||
LiveSyncPair('dav://other/x', r'C:\y'),
|
||||
]
|
||||
..copyTasks = [
|
||||
..copy.tasks = [
|
||||
CopyTask(id: 't', sources: const ['dav://nas/사진'], dest: r'C:\사진', method: 'builtin', options: '', once: false),
|
||||
];
|
||||
final users = davServerUsers(s, 'nas');
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import 'package:jj_mkvmaker/ui/settings_page.dart';
|
|||
void main() {
|
||||
testWidgets('213: 기본값 ("-avPog") 을 지나며 쳐도 칸이 그대로 (포커스 · 입력 이어짐) · ↺ 는 칸 안의 글도 되돌린다', (tester) async {
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings.rsyncOptions = '-avPo';
|
||||
c.settings.copy.rsyncOptions = '-avPo';
|
||||
tester.view.physicalSize = const Size(1400, 1600);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -36,11 +36,11 @@ void main() {
|
|||
expect(tester.state(find.byType(TextFormField).first), same(first), reason: '칸을 새로 만들지 않음 ($t)');
|
||||
expect(tester.testTextInput.hasAnyClients, isTrue, reason: '포커스 그대로 ($t)');
|
||||
}
|
||||
expect(c.settings.rsyncOptions, '-avPog -');
|
||||
expect(c.settings.copy.rsyncOptions, '-avPog -');
|
||||
// ↺: 칸 안의 글도 기본값으로
|
||||
await tester.tap(find.byTooltip('기본값으로 ($defaultRsyncOptions)').first);
|
||||
await tester.pump();
|
||||
expect(c.settings.rsyncOptions, defaultRsyncOptions);
|
||||
expect(c.settings.copy.rsyncOptions, defaultRsyncOptions);
|
||||
expect(find.text(defaultRsyncOptions), findsWidgets);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('WebDAV 가 끼면 탐색기 복사 확인 창에 robocopy 가 아니라 실제 방법 (현재 방식)', () {
|
||||
c.settings.copyMethodFolder = 'robocopy';
|
||||
c.settings.copy.methodFolder = 'robocopy';
|
||||
final t = CopyCenter.of(c).fresh(['dav://nas/a'], r'C:\dst');
|
||||
expect(CopyMethod.of(t.method), CopyMethod.builtin);
|
||||
// Rsync 화면 (방법을 정해서) 은 그대로
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ void main() {
|
|||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final pair = LiveSyncPair(p.join(tmp.path, 'gone'), p.join(tmp.path, 'dst'));
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
final shell = _Shell();
|
||||
var opened = 0;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ void main() {
|
|||
|
||||
test('68: 원본을 읽을 수 없으면 문제로 기억 (짧은 원인 · 원문 예외 없음) · "맞출 것 없음" 이 아님 · 작업 알림 맨 앞', () async {
|
||||
final pair = LiveSyncPair(src, dst, delete: true).copyWith(deleteConfirmed: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
final k = LiveSync.keyOf(pair);
|
||||
await live.refreshPending(pair);
|
||||
|
|
@ -64,12 +64,12 @@ void main() {
|
|||
|
||||
test('258: 멈춘 쌍을 지우면 "동기화 멈춤" 도 바로 사라진다 (앱을 다시 켜지 않아도)', () async {
|
||||
final pair = LiveSyncPair(src, dst, delete: true).copyWith(deleteConfirmed: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c)..start();
|
||||
final k = LiveSync.keyOf(pair);
|
||||
await live.syncNow(pair);
|
||||
expect(live.problems[k], isNotNull);
|
||||
await c.updateSettings((s) => s.liveSyncPairs = []);
|
||||
await c.updateSettings((s) => s.copy.liveSyncPairs = []);
|
||||
expect(live.problems, isEmpty);
|
||||
live.dispose();
|
||||
});
|
||||
|
|
@ -77,7 +77,7 @@ void main() {
|
|||
test('70: 원본이 정말 비었으면 멈추고 지울 목록 → [그래도 맞추기] 로 그 한 번만 지운다', () async {
|
||||
Directory(src).createSync();
|
||||
final pair = LiveSyncPair(src, dst, delete: true).copyWith(deleteConfirmed: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
final k = LiveSync.keyOf(pair);
|
||||
await live.syncNow(pair);
|
||||
|
|
@ -104,7 +104,7 @@ void main() {
|
|||
final live = LiveSync(c);
|
||||
// 안쪽 → 바깥, 지우기 없음: 맞춘다 (다른 파일은 그대로)
|
||||
final ok = LiveSyncPair(inner, outer);
|
||||
c.settings.liveSyncPairs = [ok];
|
||||
c.settings.copy.liveSyncPairs = [ok];
|
||||
await live.syncNow(ok);
|
||||
expect(File(p.join(outer, 'new.txt')).existsSync(), isTrue);
|
||||
expect(File(p.join(outer, 'other.txt')).existsSync(), isTrue);
|
||||
|
|
@ -123,7 +123,7 @@ void main() {
|
|||
(tester) async {
|
||||
Directory(src).createSync();
|
||||
final pair = LiveSyncPair(src, dst, delete: true).copyWith(deleteConfirmed: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
LiveSync.instance = live;
|
||||
addTearDown(() {
|
||||
|
|
@ -155,7 +155,7 @@ void main() {
|
|||
Directory(src).createSync();
|
||||
File(p.join(src, 'x.txt')).writeAsStringSync('x');
|
||||
final pair = LiveSyncPair(src, dst);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
LiveSync.instance = live;
|
||||
addTearDown(() {
|
||||
|
|
@ -188,8 +188,8 @@ void main() {
|
|||
..writeAsStringSync('r');
|
||||
c.settings
|
||||
..explorer.layout = 'single'
|
||||
..rsyncOptions = '-avPog --delete'
|
||||
..rsyncPaths = [l, r];
|
||||
..copy.rsyncOptions = '-avPog --delete'
|
||||
..copy.rsyncPaths = [l, r];
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
|
|||
|
|
@ -37,11 +37,11 @@ void main() {
|
|||
});
|
||||
|
||||
test('파일 탐색기 복사 방법: 현재 방식 (기본) · robocopy, 예전 값 rsync 는 현재 방식으로 (rsync 는 Rsync 화면)', () {
|
||||
expect([AppSettings().copyMethodFile, AppSettings().copyMethodFolder], ['builtin', 'builtin']);
|
||||
expect(AppSettings.fromJson({'copyMethodFolder': 'rsync', 'copyMethodFile': 'rsync'}).copyMethodFolder, 'builtin');
|
||||
expect(AppSettings.fromJson({'copyMethodFolder': 'robocopy'}).copyMethodFolder, 'robocopy');
|
||||
final s = AppSettings()..rsyncPaths = ['/a', '/b'];
|
||||
expect(AppSettings.fromJson(s.toJson()).rsyncPaths, ['/a', '/b']);
|
||||
expect([AppSettings().copy.methodFile, AppSettings().copy.methodFolder], ['builtin', 'builtin']);
|
||||
expect(AppSettings.fromJson({'copyMethodFolder': 'rsync', 'copyMethodFile': 'rsync'}).copy.methodFolder, 'builtin');
|
||||
expect(AppSettings.fromJson({'copyMethodFolder': 'robocopy'}).copy.methodFolder, 'robocopy');
|
||||
final s = AppSettings()..copy.rsyncPaths = ['/a', '/b'];
|
||||
expect(AppSettings.fromJson(s.toJson()).copy.rsyncPaths, ['/a', '/b']);
|
||||
});
|
||||
|
||||
test('양쪽 (⇄) rsync 는 -u 를 더한다 (이미 있으면 그대로)', () {
|
||||
|
|
@ -117,7 +117,7 @@ void main() {
|
|||
expect(rc('/E /SJ'), isNot(contains('/XJ')));
|
||||
expect(rc('/E /xjd').where((o) => o.toUpperCase().startsWith('/XJ')), ['/xjd']);
|
||||
expect([robocopySkipsLinks(['/E']), robocopySkipsLinks(['/E', '/SJ'])], [true, false]);
|
||||
final live = LiveSync.robocopyArgsFor(LiveSyncPair(r'C:\a', r'D:\b'), AppSettings()..robocopyOptions = '/E');
|
||||
final live = LiveSync.robocopyArgsFor(LiveSyncPair(r'C:\a', r'D:\b'), AppSettings()..copy.robocopyOptions = '/E');
|
||||
expect(live, contains('/XJ'));
|
||||
});
|
||||
|
||||
|
|
@ -277,20 +277,20 @@ void main() {
|
|||
test('실시간 동기화: 지금 맞추기 (현재 방식) · 상태', () async {
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final pair = LiveSyncPair(src, p.join(tmp.path, 'mirror'));
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
await live.syncNow(pair);
|
||||
expect(File(p.join(tmp.path, 'mirror', 'sub', 'b.txt')).readAsStringSync(), 'bbb');
|
||||
expect(live.status[LiveSync.keyOf(pair)]!.$2, contains('3'));
|
||||
// 대상이 원본 안이면 거부
|
||||
c.settings.liveSyncPairs = [LiveSyncPair(src, p.join(src, 'in'))];
|
||||
c.settings.copy.liveSyncPairs = [LiveSyncPair(src, p.join(src, 'in'))];
|
||||
live.start();
|
||||
expect(live.status[LiveSync.keyOf(c.settings.liveSyncPairs.first)]!.$2, contains('안에'));
|
||||
expect(live.status[LiveSync.keyOf(c.settings.copy.liveSyncPairs.first)]!.$2, contains('안에'));
|
||||
live.dispose();
|
||||
// 설정 저장 · 읽기
|
||||
final back = AppSettings.fromJson(c.settings.toJson());
|
||||
expect(back.liveSyncPairs.single.target, p.join(src, 'in'));
|
||||
expect([back.copyMethodFolder, back.rsyncOptions, back.copyRunMode, back.rsyncSource],
|
||||
expect(back.copy.liveSyncPairs.single.target, p.join(src, 'in'));
|
||||
expect([back.copy.methodFolder, back.copy.rsyncOptions, back.copy.runMode, back.copy.rsyncSource],
|
||||
['builtin', '-avPog', 'each', 'download']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ void main() {
|
|||
// 예전에 확인한 쌍은 처음 맞출 때 지금 옵션을 기억하며 설정을 저장한다 (서버 목록도 설정에서 다시 읽음) - 시험 서버를 설정에도
|
||||
c.settings.webdavServers = [DavServer(id: id, name: '집 NAS', url: server.url, user: 'user', password: 'pass')];
|
||||
final pair = LiveSyncPair(p.join(local.path, 'live'), 'dav://$id/mirror', method: 'rsync', delete: true, deleteConfirmed: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
expect(await LiveSync.diff(pair), unorderedEquals(['a.txt', 'sub/b.txt', '− old.txt']));
|
||||
await live.syncNow(pair);
|
||||
|
|
@ -336,10 +336,10 @@ void main() {
|
|||
final before = treeOf(got);
|
||||
// rsync 가 지운 것 (받는 쪽에 있다가 없어진 것)
|
||||
final gone = [for (final k in before.keys) if (!w.containsKey(k)) k];
|
||||
c.settings.rsyncOptions = opts;
|
||||
c.settings.copy.rsyncOptions = opts;
|
||||
// 새 쌍 (지우기 확인 전) - 지울 것이 있으면 지우지 않고 맞춘 뒤 [toDelete] 로 묻는다
|
||||
final pair = LiveSyncPair(src, 'dav://$id/live', method: 'rsync', delete: optionsDelete(opts));
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
final k = LiveSync.keyOf(pair);
|
||||
final why = <String>[];
|
||||
|
|
@ -353,7 +353,7 @@ void main() {
|
|||
for (final a in asked) {
|
||||
if (!gone.contains(a)) why.add('rsync 는 지우지 않는데 물음: $a');
|
||||
}
|
||||
if (asked.isNotEmpty) await live.decideDelete(c.settings.liveSyncPairs.single, delete: true);
|
||||
if (asked.isNotEmpty) await live.decideDelete(c.settings.copy.liveSyncPairs.single, delete: true);
|
||||
why.addAll(treeDiff(w, treeOf(got)));
|
||||
if ((live.pending[k] ?? const []).isNotEmpty) why.add('맞춘 뒤에도 맞출 것: ${live.pending[k]}');
|
||||
live.dispose();
|
||||
|
|
@ -368,18 +368,18 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..webdavServers = [DavServer(id: id, name: '집 NAS', url: server.url, user: 'user', password: 'pass')]
|
||||
..rsyncOptions = '-av --exclude=*.tmp'
|
||||
..liveSyncPairs = [
|
||||
..copy.rsyncOptions = '-av --exclude=*.tmp'
|
||||
..copy.liveSyncPairs = [
|
||||
LiveSyncPair(p.join(local.path, 'rq'), 'dav://$id/rqd',
|
||||
method: 'rsync', delete: true, deleteConfirmed: true, confirmedOptions: '-av --exclude=*.tmp'),
|
||||
];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
LiveSyncPair pair() => c.settings.liveSyncPairs.single;
|
||||
LiveSyncPair pair() => c.settings.copy.liveSyncPairs.single;
|
||||
final k = LiveSync.keyOf(pair());
|
||||
await live.syncNow(pair());
|
||||
expect(live.toDelete[k], isNull);
|
||||
c.settings.rsyncOptions = '-av --exclude=*.tmp --delete-excluded';
|
||||
c.settings.copy.rsyncOptions = '-av --exclude=*.tmp --delete-excluded';
|
||||
await live.syncNow(pair());
|
||||
expect(live.toDelete[k], ['only.tmp']);
|
||||
expect(live.reasked, contains(k));
|
||||
|
|
|
|||
Loading…
Reference in New Issue