JJ_MKVMaker/lib/app/settings/explorer_settings.dart

212 lines
12 KiB
Dart

import 'dart:io';
import 'settings_group.dart';
/// 약속 (설정 A): 환경 설정 > 파일 탐색기 (X-plore 참고) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다.
/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키)
class ExplorerSettings implements SettingsGroup {
/// 창: 'dual' 두 창 (기본 - 폰 세로도, 규칙 7 · 결정 3 10/22) · 'split' 왼쪽 폴더 트리 + 오른쪽 그 폴더의 파일 목록 ·
/// 'single' 한 창 · 'auto' 화면 크기 따라 (폰 세로는 한 창)
String layout = 'dual';
/// X-plore 비교 a: 한 창일 때 복사 · 이동 - 'clip' 담아 두었다가 붙여넣기 (지금 그대로, 처음 값) /
/// 'other' 숨은 다른 창의 폴더로 바로 (X-plore 처럼, 위에 "갈 곳" 줄). 예전 동작과 달라 처음 값은 그대로 (관리자 결정)
String onePaneCopy = 'clip';
/// 펼친 폴더를 다시 누르면 접기 (규칙 3 "폴더 한 번 누르기 = 펼치기 · 접기", 처음 값 켬 - E3 10/22).
/// 두 번 누르기의 두 번째는 접지 않는다 ([ClickPolicy] repeat)
bool retapCollapse = true;
/// X-plore 비교 40 (관리자 지시 10/20): 파일 탐색기 목록만의 글자 크기 (%, 80~150, 처음 100 = 지금 그대로).
/// 앱 화면 크기 · 기기 글자 크기와 곱해진다
int textScale = 100;
static const textScaleMin = 80, textScaleMax = 150;
/// X-plore 비교 40: 목록 줄 간격 'compact' 좁게 · 'normal' 보통 (처음 값 = 지금 그대로) · 'wide' 넓게
String rowSpacing = 'normal';
/// X-plore 비교 i: 일괄 이름 바꾸기의 마지막 규칙 (다음에 창이 그 값으로 열림, 비면 처음 값)
Map<String, dynamic> batchRenameLast = {};
/// X-plore 비교 c: X-plore 스타일에서 저장 장치 · 서버 · 즐겨찾기를 트리의 맨 위 항목으로 (위 줄 탭 · ★ 는 그대로 함께).
/// 처음 값 켬 (X-plore 스타일에서만 보임 - 다른 스타일은 그대로)
bool rootsInTree = true;
/// X-plore 스타일에서 줄 끝 표시를 늘 보이기. 처음 값 끔 - 규칙 4 "선택 표시는 선택 모드에서만" (E3 10/22)
bool alwaysMarks = false;
/// X-plore 비교 ⚖: X-plore 스타일 탐색기 안에서 옆으로 밀기 = 창 바꾸기 (설정으로만, 처음 값 끔 - 10/8 사용자 결정 "화면 가운데를 밀면 화면 이동" 을 지킴, 관리자 판단 10/18)
bool swipePanes = false;
/// X-plore 비교 e (관리자 결정): Android 앱 휴지통 보관 일 (처음 14일, 0 = 끔 - 예전처럼 바로 영구 삭제). Windows 는 시스템 휴지통
int androidTrashDays = 14;
/// 누르기: 'select' 한 번 = 선택 · 두 번 = 열기 (기본, 두 기기 - 규칙 2) / 'open' 한 번 = 바로 열기.
/// 길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴.
String click = 'select';
/// 모양: 'xplore' (기본) · 'windows' Windows 탐색기 · 'totalcmd' Total Commander.
/// 규칙 1 의 Windows 처음 값 (Total Commander) 과 저장된 값 묻기 (결정 1) 는 E6 (TC 모양) 과 함께 (관리자 결정 10/22)
String style = 'xplore';
/// 두 창 배치: 'side' 좌우 (기본, 118 - 폰 세로에서도) · 'stacked' 위아래 · 'auto' 화면 모양 따라 (가로로 넓으면 좌우, 세로로 길면 위아래)
String orientation = 'side';
/// 기능 버튼 줄: 'middle' 두 창 사이 (기본) · 'edge' 오른쪽 (위아래 배치면 아래) 끝 · 'hidden' 숨김
String toolbar = 'middle';
/// 기능 버튼 줄에 보일 버튼과 순서 (ExplorerButton 이름). 비어 있으면 기본 구성.
List<String> buttons = [];
/// 정렬 · 숨긴 항목
String sort = 'name';
bool sortDesc = false;
/// 84: 정렬을 폴더마다 기억 (처음: 끔 - 모든 폴더가 같은 정렬). 켜면 열 머리 · 정렬 창이 지금 폴더에만 쓰고 [folderSorts] 에 둔다
bool sortPerFolder = false;
/// X-plore 비교 5-1 ① (관리자 지시 10/21): 종류별 기본 정렬 - 사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름.
/// 처음 끔 (지금 정렬 그대로). 켜도 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억한다 ([folderSorts])
bool sortByKind = false;
/// X-plore 비교 5-1 ② (관리자 지시 10/21): 원격 (WebDAV · SFTP · SMB · FTP) 썸네일 - 'wifi' 데이터 요금제가 아닌 망에서만 (처음 값) ·
/// 'always' 늘 · 'off' 받지 않음. X-plore 모양의 목록에서 보이는 줄의 그림 · 동영상만, 동시에 2개까지
String remoteThumbs = 'wifi';
/// 원격 썸네일 캐시 상한 (MB, 처음 200) - 넘으면 오래 쓰지 않은 것부터 지운다
int remoteThumbCacheMb = 200;
static const remoteThumbCacheChoices = [50, 100, 200, 500, 1000];
/// X-plore 비교 5-1 ⑥ (관리자 지시 10/21): 파일 탐색기 단축키 중 사용자가 바꾼 것만 (동작 → 키, '' = 그 동작은 단축키 없음).
/// 비어 있으면 (처음) 스타일대로 (83) - ui/explorer_keys.dart
Map<String, String> keyOverrides = {};
/// 84: 폴더 → "기준|desc" (예: "size|1"). 오래된 것부터 지워 300개까지
Map<String, String> folderSorts = {};
/// 80: 폴더 크기 계산 - 'sort' 크기순 정렬일 때만 (처음 값) · 'always' 늘 · 'off' 안 함 (길게 누르기 [폴더 크기 계산] 은 늘 됨)
String folderSize = 'sort';
/// 82: 창 사이 끌어 놓기 - 'ask' 묻기 (처음 값, 관리자 판단) · 'copy' 복사 · 'move' 이동
String dropAction = 'ask';
/// 85: 탐색기의 복사 · 이동 진행 패널을 숨김 (아래 작은 표시만). 처음 값: 보임
bool progressHidden = false;
bool showHidden = false;
/// X-plore 숨기기 (관리자 지시 10/18): 파일 탐색기 목록에서 숨긴 파일 · 폴더의 경로 (파일 자체는 바꾸지 않는다).
/// 지우기 · 이동 대상에서는 빠지지 않는다. 목록 맨 아래 [숨긴 항목 보이기] 로 다시 본다
List<String> hiddenItems = [];
/// X-plore 맞추기 (관리자 지시 10/20): [목록에서 숨기기] 전에 무엇이 바뀌는지 확인 창 (처음 켬, 창의 [다음부터 묻지 않기] · 환경 설정에서 끔)
bool hideConfirm = true;
/// 마지막으로 연 폴더 (왼쪽 · 오른쪽 창)
List<String> paths = [];
/// 최근에 연 폴더 (내역, 최신이 앞)
List<String> history = [];
@override
void write(Map<String, Object?> j, {required bool plainSecrets}) {
j['explorerLayout'] = layout;
j['explorerOnePaneCopy'] = onePaneCopy;
j['explorerRetapCollapse'] = retapCollapse;
j['explorerTextScale'] = textScale;
j['explorerRowSpacing'] = rowSpacing;
j['batchRenameLast'] = batchRenameLast;
j['explorerRootsInTree'] = rootsInTree;
j['explorerAlwaysMarks'] = alwaysMarks;
j['explorerSwipePanes'] = swipePanes;
j['androidTrashDays'] = androidTrashDays;
j['explorerClick'] = click;
j['explorerStyle'] = style;
j['explorerOrientation'] = orientation;
j['explorerToolbar'] = toolbar;
j['explorerButtons'] = buttons;
j['explorerSort'] = sort;
j['explorerSortDesc'] = sortDesc;
j['explorerSortPerFolder'] = sortPerFolder;
j['explorerSortByKind'] = sortByKind;
j['explorerRemoteThumbs'] = remoteThumbs;
j['explorerRemoteThumbCacheMb'] = remoteThumbCacheMb;
j['explorerKeyOverrides'] = keyOverrides;
j['explorerFolderSorts'] = folderSorts;
j['explorerFolderSize'] = folderSize;
j['explorerDropAction'] = dropAction;
j['explorerProgressHidden'] = progressHidden;
j['explorerShowHidden'] = showHidden;
j['explorerHiddenItems'] = hiddenItems;
j['explorerHideConfirm'] = hideConfirm;
j['explorerPaths'] = paths;
j['explorerHistory'] = history;
// 예전 기본값 (두 창 · 한 번 누르면 선택) 은 사용자가 고른 값이 아니므로 한 번 새 기본값으로 옮긴다
j['explorerV2'] = true;
// E3 (10/22): 선택 규칙 (사용자 지시 모음) 의 처음 값으로 한 번 옮겼음
j['explorerRulesV1'] = true;
j['explorerOrientV3'] = true;
}
@override
void read(Map<String, dynamic> j) {
// 'auto' = 넓은 화면은 두 창, 좁은 화면 (폰 세로) 은 한 창. 예전 기본 'dual' 은 한 번 'auto' 로
onePaneCopy = j['explorerOnePaneCopy'] == 'other' ? 'other' : 'clip';
// E3: 규칙 이전 판이 저장한 false 는 예전 처음 값 - 한 번 규칙 3 (켬) 으로
retapCollapse = j['explorerRulesV1'] == true ? j['explorerRetapCollapse'] != false : true;
// 값이 없거나 틀리면 지금 그대로 (100% · 보통), 범위 밖은 끝으로
textScale = j['explorerTextScale'] is num
? (j['explorerTextScale'] as num).round().clamp(textScaleMin, textScaleMax)
: 100;
rowSpacing = const {'compact', 'normal', 'wide'}.contains(j['explorerRowSpacing']) ? j['explorerRowSpacing'] as String : 'normal';
batchRenameLast = j['batchRenameLast'] is Map ? Map<String, dynamic>.from(j['batchRenameLast'] as Map) : {};
rootsInTree = j['explorerRootsInTree'] != false;
// E3: 규칙 이전 판이 저장한 true 는 예전 처음 값 - 한 번 규칙 4 (끔) 로
alwaysMarks = j['explorerRulesV1'] == true && j['explorerAlwaysMarks'] == true;
swipePanes = j['explorerSwipePanes'] == true;
androidTrashDays = switch (j['androidTrashDays']) { final int d when d >= 0 && d <= 365 => d, _ => 14 };
// E3 (규칙 7 · 결정 3): 처음 값 두 창. 규칙 이전 판의 'auto' 는 예전 처음 값이라 한 번 두 창으로
layout = switch (j['explorerLayout']) {
'single' || 'split' => j['explorerLayout'] as String,
'auto' when j['explorerRulesV1'] == true => 'auto',
_ => 'dual',
};
// 규칙 2: 두 기기 모두 한 번 = 선택. 규칙 이전 Android 의 'open' 은 예전 처음 값이라 한 번 'select' 로
click = j['explorerClick'] == 'open' && (j['explorerRulesV1'] == true || !Platform.isAndroid) ? 'open' : 'select';
style = const ['windows', 'totalcmd'].contains(j['explorerStyle']) ? j['explorerStyle'] as String : 'xplore';
// 118: 예전 기본 '화면 모양 따라' 는 한 번 좌우로 옮긴다 (사용자가 고른 위아래는 그대로)
orientation = j['explorerOrientation'] == 'stacked'
? 'stacked'
: j['explorerOrientation'] == 'auto' && j['explorerOrientV3'] == true
? 'auto'
: 'side';
toolbar = const ['edge', 'hidden'].contains(j['explorerToolbar']) ? j['explorerToolbar'] as String : 'middle';
buttons = [for (final x in (j['explorerButtons'] as List?) ?? const []) '$x'];
sort = const ['date', 'size', 'type'].contains(j['explorerSort']) ? j['explorerSort'] as String : 'name';
sortDesc = j['explorerSortDesc'] as bool? ?? false;
folderSize = const ['sort', 'always', 'off'].contains(j['explorerFolderSize']) ? j['explorerFolderSize'] as String : 'sort';
sortPerFolder = j['explorerSortPerFolder'] == true;
sortByKind = j['explorerSortByKind'] == true;
remoteThumbs = const {'off', 'wifi', 'always'}.contains(j['explorerRemoteThumbs']) ? j['explorerRemoteThumbs'] as String : 'wifi';
keyOverrides = j['explorerKeyOverrides'] is Map
? {for (final e in (j['explorerKeyOverrides'] as Map).entries) if (e.key is String && e.value is String) e.key as String: e.value as String}
: {};
remoteThumbCacheMb = j['explorerRemoteThumbCacheMb'] is num
? (j['explorerRemoteThumbCacheMb'] as num).round().clamp(10, 10000)
: 200;
progressHidden = j['explorerProgressHidden'] == true;
dropAction = const ['ask', 'copy', 'move'].contains(j['explorerDropAction']) ? j['explorerDropAction'] as String : 'ask';
folderSorts = {
if (j['explorerFolderSorts'] is Map)
for (final e in (j['explorerFolderSorts'] as Map).entries)
if (e.key is String && e.value is String) e.key as String: e.value as String,
};
showHidden = j['explorerShowHidden'] as bool? ?? false;
hiddenItems = [for (final x in (j['explorerHiddenItems'] as List?) ?? const []) if (x is String && x.isNotEmpty) x];
hideConfirm = j['explorerHideConfirm'] as bool? ?? true;
paths = [for (final x in (j['explorerPaths'] as List?) ?? const []) '$x'];
history = [for (final x in (j['explorerHistory'] as List?) ?? const []) '$x'];
}
}