// 약속 (설정 A): 설정 안의 기록 - 이동 버튼 · 즐겨찾기 · 실시간 동기화 쌍 · 복사 작업. 저마다 toJson · fromJson /// MKV 세부 정보의 이동 버튼 하나: 표시 이름 · 옮길 폴더 class MoveTarget { final String name; final String dir; const MoveTarget(this.name, this.dir); MoveTarget copyWith({String? name, String? dir}) => MoveTarget(name ?? this.name, dir ?? this.dir); Map toJson() => {'name': name, 'dir': dir}; factory MoveTarget.fromJson(Map j) => MoveTarget((j['name'] as String?)?.trim().isNotEmpty == true ? j['name'] as String : '이동', j['dir'] as String); @override bool operator ==(Object other) => other is MoveTarget && other.name == name && other.dir == dir; @override int get hashCode => Object.hash(name, dir); } /// 73: 파일 탐색기 즐겨찾기 (폴더). [right] 가 있으면 Rsync 화면의 "왼쪽 + 오른쪽" 짝 class FolderBookmark { final String name; final String path; final String? right; const FolderBookmark({required this.name, required this.path, this.right}); bool get isPair => right != null; FolderBookmark copyWith({String? name}) => FolderBookmark(name: name ?? this.name, path: path, right: right); Map toJson() => {'name': name, 'path': path, if (right != null) 'right': right}; static FolderBookmark? fromJson(Object? j) => j is Map && j['path'] is String ? FolderBookmark( name: j['name'] is String && (j['name'] as String).trim().isNotEmpty ? j['name'] as String : j['path'] as String, path: j['path'] as String, right: j['right'] is String ? j['right'] as String : null) : null; } /// 실시간 동기화 한 쌍 (lsyncd 처럼: 원본 폴더를 지켜보다가 바뀌면 대상 폴더에 맞춘다) class LiveSyncPair { final String source; final String target; /// 'builtin' · 'rsync' · 'robocopy' (CopyMethod 이름) final String method; /// 원본에 없는 것을 대상에서 지우기 (rsync --delete, robocopy /PURGE) final bool delete; final bool enabled; /// 동작 시간 (cron 줄들, core/cron_window.dart). 비어 있으면 계속 (앱이 켜져 있는 동안 늘) final List schedule; /// "지우기" 를 사용자가 지울 목록을 보고 확인했는지. 확인 전에는 지울 것이 있으면 지우지 않고 맞추며 확인을 기다린다 (42). /// 이 값이 생기기 전부터 돌던 쌍은 확인한 것으로 본다. final bool deleteConfirmed; /// 지우기를 확인할 때의 rsync 옵션 (rsync 쌍만, 그 밖은 ''). 확인한 뒤 옵션이 바뀌어 지울 것이 늘면 다시 묻는다 /// (관리자 3a 판단 10/20 - --delete-excluded · --exclude · --filter 등). null = 이 값이 생기기 전에 확인한 쌍 final String? confirmedOptions; const LiveSyncPair(this.source, this.target, {this.method = 'builtin', this.delete = false, this.enabled = true, this.schedule = const [], this.deleteConfirmed = false, this.confirmedOptions}); /// [delete] 를 새로 켜면 확인은 다시 받는다 LiveSyncPair copyWith( {String? method, bool? delete, bool? enabled, List? schedule, bool? deleteConfirmed, String? confirmedOptions}) => LiveSyncPair(source, target, method: method ?? this.method, delete: delete ?? this.delete, enabled: enabled ?? this.enabled, schedule: schedule ?? this.schedule, deleteConfirmed: deleteConfirmed ?? (delete == true && !this.delete ? false : this.deleteConfirmed), confirmedOptions: confirmedOptions ?? this.confirmedOptions); Map toJson() => { 'source': source, 'target': target, 'method': method, 'delete': delete, 'enabled': enabled, 'schedule': schedule, 'deleteConfirmed': deleteConfirmed, if (confirmedOptions != null) 'confirmedOptions': confirmedOptions, }; factory LiveSyncPair.fromJson(Map j) => LiveSyncPair( j['source'] as String, j['target'] as String, method: const ['rsync', 'robocopy'].contains(j['method']) ? j['method'] as String : 'builtin', delete: j['delete'] == true, enabled: j['enabled'] != false, schedule: [for (final x in (j['schedule'] as List?) ?? const []) '$x'], // 이 값이 없는 예전 쌍은 이미 지우기 포함으로 돌던 것 → 확인한 것으로 deleteConfirmed: j['deleteConfirmed'] as bool? ?? true, confirmedOptions: j['confirmedOptions'] is String ? j['confirmedOptions'] as String : null, ); } /// 복사 · 이동 기억 (모니터링 > 복사 · rsync): 같은 원본 → 대상을 다시 복사하면 이 옵션을 쓴다. 언제든 [실행] 으로 다시. class CopyTask { final String id; final List sources; final String dest; final bool move; /// 폴더 "안의 것" 을 대상에 맞추기 (lsync 에서 옮겨 온 것: 원본/ → 대상/) final bool contents; /// CopyMethod 이름 · 그 방법의 옵션 · 한 번에 · 속도 제한 (KB/s) final String method; final String options; final bool once; final int bandwidthKBps; /// 마지막 실행: 시각 (ISO) · 결과 ('done' · 'failed' · 'cancelled' · '') · 글 · 파일 수 final String lastRun; final String lastResult; final String lastMessage; final int lastFiles; /// 235: 이동 뒤 원본에 남은 파일 수 (건너뜀 - 216). 카드에 "n개는 원본에 남음" 으로 final int lastLeft; /// 237: 실패 원문 (예외 이름 · 명령 줄 포함) - 카드의 [자세히] 에서 보고 복사한다. 화면 글은 [lastMessage] final String lastDetail; /// 이동 ([move], rsync --remove-source-files) 뒤 원본 정리: '' 그대로 · 'keep' 빈 폴더를 지우고 원본 폴더는 남김 · /// 'all' 원본 폴더까지 (비었으면) 지움 (find 원본/ -type d -empty -delete) final String prune; const CopyTask({ required this.id, required this.sources, required this.dest, this.move = false, this.contents = false, this.method = 'builtin', this.options = '', this.once = false, this.bandwidthKBps = 0, this.lastRun = '', this.lastResult = '', this.lastMessage = '', this.lastFiles = 0, this.lastLeft = 0, this.lastDetail = '', this.prune = '', }); /// 같은 복사인지 (원본들 · 대상 · 이동) String get key => '${move ? 'M' : contents ? 'S' : 'C'}|${[...sources]..sort()}|$dest'; CopyTask copyWith({ String? method, String? options, bool? once, int? bandwidthKBps, bool? move, String? lastRun, String? lastResult, String? lastMessage, int? lastFiles, int? lastLeft, String? lastDetail, String? prune, }) => CopyTask( id: id, sources: sources, dest: dest, move: move ?? this.move, contents: contents, method: method ?? this.method, options: options ?? this.options, once: once ?? this.once, bandwidthKBps: bandwidthKBps ?? this.bandwidthKBps, lastRun: lastRun ?? this.lastRun, lastResult: lastResult ?? this.lastResult, lastMessage: lastMessage ?? this.lastMessage, lastFiles: lastFiles ?? this.lastFiles, lastLeft: lastLeft ?? this.lastLeft, lastDetail: lastDetail ?? this.lastDetail, prune: prune ?? this.prune, ); Map toJson() => { 'id': id, 'sources': sources, 'dest': dest, 'move': move, 'contents': contents, 'method': method, 'options': options, 'once': once, 'bandwidthKBps': bandwidthKBps, 'lastRun': lastRun, 'lastResult': lastResult, 'lastMessage': lastMessage, 'lastFiles': lastFiles, 'lastLeft': lastLeft, 'lastDetail': lastDetail, 'prune': prune, }; factory CopyTask.fromJson(Map j) => CopyTask( id: '${j['id']}', sources: [for (final x in (j['sources'] as List?) ?? const []) '$x'], dest: '${j['dest']}', move: j['move'] == true, contents: j['contents'] == true, method: const ['rsync', 'robocopy'].contains(j['method']) ? j['method'] as String : 'builtin', options: j['options'] as String? ?? '', once: j['once'] == true, bandwidthKBps: (j['bandwidthKBps'] as num?)?.toInt() ?? 0, lastRun: j['lastRun'] as String? ?? '', lastResult: j['lastResult'] as String? ?? '', lastMessage: j['lastMessage'] as String? ?? '', lastFiles: (j['lastFiles'] as num?)?.toInt() ?? 0, lastLeft: (j['lastLeft'] as num?)?.toInt() ?? 0, lastDetail: j['lastDetail'] as String? ?? '', prune: const ['keep', 'all'].contains(j['prune']) ? j['prune'] as String : '', ); }