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 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 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 keyOverrides = {}; /// 84: 폴더 → "기준|desc" (예: "size|1"). 오래된 것부터 지워 300개까지 Map 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 hiddenItems = []; /// X-plore 맞추기 (관리자 지시 10/20): [목록에서 숨기기] 전에 무엇이 바뀌는지 확인 창 (처음 켬, 창의 [다음부터 묻지 않기] · 환경 설정에서 끔) bool hideConfirm = true; /// 마지막으로 연 폴더 (왼쪽 · 오른쪽 창) List paths = []; /// 최근에 연 폴더 (내역, 최신이 앞) List history = []; @override void write(Map 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 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.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']; } }