설정 A 분야 1: 파일 탐색기 (ExplorerSettings) - 키 31 · 옮김 표시 3
- lib/app/settings/explorer_settings.dart: 탐색기 설정의 이름 · 처음 값 · 읽기 · 쓰기를 한 곳에
- AppSettings 는 explorer 분야를 갖고 분야 목록을 돌며 합치기만 (toJson · fromJson 에서 탐색기 키를 지움)
- 부르는 곳: s.explorerLayout → s.explorer.layout 꼴 (옛 이름 getter 없음), 저장 키 이름은 그대로
- 고침: fromJson 이 바뀐 기본값 알림 (_migrated) 을 분야를 읽기 전에 계산하던 것 - 분야를 먼저 읽음
- JSON 약속 시험: 알림 목록 (__migrated) · 옛 판 설정 경우를 더함, 기준은 옛 판 (db45f80) 에서 다시 만듦
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
modular
parent
f9f67f11f4
commit
428fe99a82
|
|
@ -20,7 +20,7 @@ void main() {
|
|||
final env = Platform.environment;
|
||||
final out = env['JJ_SHOT_DIR'] ?? Directory.systemTemp.path;
|
||||
final c = AppController(PlatformServices.create());
|
||||
c.settings.explorerPaths = [env['JJ_SHOT_LEFT'] ?? Directory.current.path, env['JJ_SHOT_RIGHT'] ?? Directory.current.path];
|
||||
c.settings.explorer.paths = [env['JJ_SHOT_LEFT'] ?? Directory.current.path, env['JJ_SHOT_RIGHT'] ?? Directory.current.path];
|
||||
final key = GlobalKey();
|
||||
final light = env['JJ_SHOT_THEME'] == 'light';
|
||||
JjColors.use(light ? Brightness.light : Brightness.dark);
|
||||
|
|
@ -33,8 +33,8 @@ void main() {
|
|||
('split', const Size(1600, 900), 'split', 'windows'),
|
||||
]) {
|
||||
c.settings
|
||||
..explorerLayout = layout
|
||||
..explorerStyle = style;
|
||||
..explorer.layout = layout
|
||||
..explorer.style = style;
|
||||
tester.view.physicalSize = size;
|
||||
tester.view.devicePixelRatio = 1;
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ void main() {
|
|||
Directory(p.join(tmp.path, '폴더 하나')).createSync();
|
||||
Directory(p.join(tmp.path, 'Another folder with a long name')).createSync();
|
||||
final c = AppController(PlatformServices.create());
|
||||
c.settings.explorerPaths = [tmp.path, tmp.path];
|
||||
c.settings.explorer.paths = [tmp.path, tmp.path];
|
||||
final key = GlobalKey();
|
||||
// 폰처럼 (Android 는 트리 펼치기 화살표가 44 로 넓다 - Windows 로 그리면 폰보다 넉넉해 보인다)
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
|
|
@ -37,10 +37,10 @@ void main() {
|
|||
for (final style in ['xplore', 'windows', 'totalcmd']) {
|
||||
for (final (scale, spacing) in [(100, 'normal'), (150, 'wide'), (80, 'compact')]) {
|
||||
c.settings
|
||||
..explorerLayout = layout
|
||||
..explorerStyle = style
|
||||
..explorerTextScale = scale
|
||||
..explorerRowSpacing = spacing;
|
||||
..explorer.layout = layout
|
||||
..explorer.style = style
|
||||
..explorer.textScale = scale
|
||||
..explorer.rowSpacing = spacing;
|
||||
tester.view.physicalSize = Size(width, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ void main() {
|
|||
final c = AppController(PlatformServices.create());
|
||||
c.settings
|
||||
..rsyncPaths = [tmp.path, tmp.path]
|
||||
..explorerStyle = 'windows';
|
||||
..explorer.style = 'windows';
|
||||
final key = GlobalKey();
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ void main() {
|
|||
..createSync(recursive: true)
|
||||
..writeAsStringSync('old');
|
||||
c = AppController(PlatformServices.create());
|
||||
c.settings.explorerPaths = [left, right];
|
||||
c.settings.explorer.paths = [left, right];
|
||||
// 휴지통: 기본은 가짜 (화면 흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통
|
||||
useTestRecycleBin();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ void main() {
|
|||
..writeAsStringSync('x');
|
||||
c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerPaths = [left, right]
|
||||
..explorerDropAction = 'ask';
|
||||
..explorer.paths = [left, right]
|
||||
..explorer.dropAction = 'ask';
|
||||
});
|
||||
tearDown(() async {
|
||||
for (var i = 0;; i++) {
|
||||
|
|
@ -221,7 +221,7 @@ void main() {
|
|||
|
||||
testWidgets('75: 마우스 4 · 5 번은 마우스가 올라간 창에 · Alt+← / →', (t) async {
|
||||
await open(t);
|
||||
String at(int i) => c.settings.explorerPaths[i];
|
||||
String at(int i) => c.settings.explorer.paths[i];
|
||||
final sub = p.join(left, 'sub'), r1 = p.join(right, 'r1');
|
||||
await t.tap(find.text('r1'));
|
||||
await until(t, () => at(1) == r1 && find.text('x.txt').evaluate().isNotEmpty);
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ void main() {
|
|||
Directory(p.join(tmp.path, 'sub')).createSync();
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerPaths = [tmp.path, tmp.path]
|
||||
..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'))];
|
||||
|
|
|
|||
|
|
@ -90,9 +90,9 @@ void main() {
|
|||
File(p.join(dir.path, 'keep_$tag.txt')).writeAsStringSync('x');
|
||||
final c = controller();
|
||||
c.settings
|
||||
..explorerLayout = 'single'
|
||||
..explorerClick = 'select'
|
||||
..explorerPaths = [dir.path, dir.path];
|
||||
..explorer.layout = 'single'
|
||||
..explorer.click = 'select'
|
||||
..explorer.paths = [dir.path, dir.path];
|
||||
await show(t, size, ExplorerPage(c: c));
|
||||
await tapText(t, 'recycle_me_$tag.txt');
|
||||
await t.sendKeyEvent(LogicalKeyboardKey.delete);
|
||||
|
|
@ -119,9 +119,9 @@ void main() {
|
|||
for (final (tag, size) in sizes) {
|
||||
final c = controller();
|
||||
c.settings
|
||||
..explorerLayout = 'single'
|
||||
..explorerClick = 'select'
|
||||
..explorerPaths = [share.path, share.path];
|
||||
..explorer.layout = 'single'
|
||||
..explorer.click = 'select'
|
||||
..explorer.paths = [share.path, share.path];
|
||||
await show(t, size, ExplorerPage(c: c));
|
||||
await settle(t, 20);
|
||||
await tapText(t, 'on_share.txt');
|
||||
|
|
@ -142,9 +142,9 @@ void main() {
|
|||
for (final (tag, size) in sizes) {
|
||||
final c = controller();
|
||||
c.settings
|
||||
..explorerLayout = 'single'
|
||||
..explorerClick = 'select'
|
||||
..explorerPaths = [deep, deep];
|
||||
..explorer.layout = 'single'
|
||||
..explorer.click = 'select'
|
||||
..explorer.paths = [deep, deep];
|
||||
await show(t, size, ExplorerPage(c: c));
|
||||
await settle(t, 20);
|
||||
final f = find.textContaining('long_name_file');
|
||||
|
|
@ -185,8 +185,8 @@ void main() {
|
|||
for (final (tag, size) in sizes) {
|
||||
final c = controller();
|
||||
c.settings
|
||||
..explorerLayout = 'single'
|
||||
..explorerPaths = [tmp.path, tmp.path];
|
||||
..explorer.layout = 'single'
|
||||
..explorer.paths = [tmp.path, tmp.path];
|
||||
await show(t, size, ExplorerPage(c: c));
|
||||
Future<void> enter(String text) async {
|
||||
await t.tap(find.byTooltip('경로 입력').first);
|
||||
|
|
|
|||
|
|
@ -54,10 +54,10 @@ void main() {
|
|||
for (final w in widths) {
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerStyle = style
|
||||
..explorerLayout = 'single'
|
||||
..explorerTextScale = list
|
||||
..explorerPaths = [dir.path, dir.path];
|
||||
..explorer.style = style
|
||||
..explorer.layout = 'single'
|
||||
..explorer.textScale = list
|
||||
..explorer.paths = [dir.path, dir.path];
|
||||
t.view.physicalSize = Size(w, 800);
|
||||
t.view.devicePixelRatio = 1;
|
||||
await t.runAsync(() => t.pumpWidget(RepaintBoundary(
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ void main() {
|
|||
..writeAsStringSync('NEWER');
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerPaths = [left, right]
|
||||
..explorer.paths = [left, right]
|
||||
..copyConflict = 'overwrite';
|
||||
final root = p.rootPrefix(tmp.path);
|
||||
device.volumes = () => [(root, root.replaceAll(RegExp(r'[\\/]+$'), ''))];
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ void main() {
|
|||
device.volumes = () => [(tmp.path, 'T:')];
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerOrientation = 'side'
|
||||
..explorer.orientation = 'side'
|
||||
..rsyncPaths = [l, r];
|
||||
final shotKey = GlobalKey();
|
||||
await t.pumpWidget(MaterialApp(
|
||||
|
|
|
|||
|
|
@ -52,10 +52,10 @@ void main() {
|
|||
device.volumes = () => [(a.path, 'C:'), (b.path, 'D:')];
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerStyle = 'xplore'
|
||||
..explorerLayout = w < 720 ? 'single' : 'dual'
|
||||
..explorerOnePaneCopy = 'other'
|
||||
..explorerPaths = [a.path, b.path]
|
||||
..explorer.style = 'xplore'
|
||||
..explorer.layout = w < 720 ? 'single' : 'dual'
|
||||
..explorer.onePaneCopy = 'other'
|
||||
..explorer.paths = [a.path, b.path]
|
||||
..folderBookmarks = [FolderBookmark(name: 'Show S01', path: p.join(a.path, 'Show S01'))];
|
||||
final shotKey = GlobalKey();
|
||||
await t.runAsync(() => t.pumpWidget(RepaintBoundary(
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ import '../services/app_shell.dart' show appIconOf;
|
|||
import '../core/remote_idle.dart';
|
||||
import 'settings/records.dart';
|
||||
import 'settings/settings_group.dart';
|
||||
import 'settings/explorer_settings.dart';
|
||||
|
||||
export 'settings/records.dart';
|
||||
export 'settings/settings_group.dart';
|
||||
export 'settings/settings_store.dart';
|
||||
export 'settings/explorer_settings.dart';
|
||||
|
||||
/// 환경 설정 (settings.json 에 저장)
|
||||
class AppSettings {
|
||||
|
|
@ -181,82 +183,27 @@ class AppSettings {
|
|||
bool torNoticeSeen = false;
|
||||
|
||||
// ── 파일 탐색기 (X-plore 참고) ──
|
||||
/// 창: 'dual' 두 창 (기본 - 폰 세로도, 규칙 7 · 결정 3 10/22) · 'split' 왼쪽 폴더 트리 + 오른쪽 그 폴더의 파일 목록 ·
|
||||
/// 'single' 한 창 · 'auto' 화면 크기 따라 (폰 세로는 한 창)
|
||||
String explorerLayout = 'dual';
|
||||
|
||||
/// X-plore 비교 a: 한 창일 때 복사 · 이동 - 'clip' 담아 두었다가 붙여넣기 (지금 그대로, 처음 값) /
|
||||
/// 'other' 숨은 다른 창의 폴더로 바로 (X-plore 처럼, 위에 "갈 곳" 줄). 예전 동작과 달라 처음 값은 그대로 (관리자 결정)
|
||||
String explorerOnePaneCopy = 'clip';
|
||||
|
||||
/// 펼친 폴더를 다시 누르면 접기 (규칙 3 "폴더 한 번 누르기 = 펼치기 · 접기", 처음 값 켬 - E3 10/22).
|
||||
/// 두 번 누르기의 두 번째는 접지 않는다 ([ClickPolicy] repeat)
|
||||
bool explorerRetapCollapse = true;
|
||||
|
||||
/// X-plore 비교 40 (관리자 지시 10/20): 파일 탐색기 목록만의 글자 크기 (%, 80~150, 처음 100 = 지금 그대로).
|
||||
/// 앱 화면 크기 · 기기 글자 크기와 곱해진다
|
||||
int explorerTextScale = 100;
|
||||
static const explorerTextScaleMin = 80, explorerTextScaleMax = 150;
|
||||
|
||||
/// X-plore 비교 40: 목록 줄 간격 'compact' 좁게 · 'normal' 보통 (처음 값 = 지금 그대로) · 'wide' 넓게
|
||||
String explorerRowSpacing = 'normal';
|
||||
|
||||
/// X-plore 비교 i: 일괄 이름 바꾸기의 마지막 규칙 (다음에 창이 그 값으로 열림, 비면 처음 값)
|
||||
Map<String, dynamic> batchRenameLast = {};
|
||||
|
||||
/// X-plore 비교 c: X-plore 스타일에서 저장 장치 · 서버 · 즐겨찾기를 트리의 맨 위 항목으로 (위 줄 탭 · ★ 는 그대로 함께).
|
||||
/// 처음 값 켬 (X-plore 스타일에서만 보임 - 다른 스타일은 그대로)
|
||||
bool explorerRootsInTree = true;
|
||||
|
||||
/// X-plore 스타일에서 줄 끝 표시를 늘 보이기. 처음 값 끔 - 규칙 4 "선택 표시는 선택 모드에서만" (E3 10/22)
|
||||
bool explorerAlwaysMarks = false;
|
||||
|
||||
|
||||
/// X-plore 비교 ⚖: X-plore 스타일 탐색기 안에서 옆으로 밀기 = 창 바꾸기 (설정으로만, 처음 값 끔 - 10/8 사용자 결정 "화면 가운데를 밀면 화면 이동" 을 지킴, 관리자 판단 10/18)
|
||||
bool explorerSwipePanes = false;
|
||||
|
||||
/// X-plore 비교 e (관리자 결정): Android 앱 휴지통 보관 일 (처음 14일, 0 = 끔 - 예전처럼 바로 영구 삭제). Windows 는 시스템 휴지통
|
||||
int androidTrashDays = 14;
|
||||
|
||||
/// 누르기: 'select' 한 번 = 선택 · 두 번 = 열기 (기본, 두 기기 - 규칙 2) / 'open' 한 번 = 바로 열기.
|
||||
/// 길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴.
|
||||
String explorerClick = 'select';
|
||||
|
||||
/// 모양: 'xplore' (기본) · 'windows' Windows 탐색기 · 'totalcmd' Total Commander.
|
||||
/// 규칙 1 의 Windows 처음 값 (Total Commander) 과 저장된 값 묻기 (결정 1) 는 E6 (TC 모양) 과 함께 (관리자 결정 10/22)
|
||||
String explorerStyle = 'xplore';
|
||||
|
||||
/// 두 창 배치: 'side' 좌우 (기본, 118 - 폰 세로에서도) · 'stacked' 위아래 · 'auto' 화면 모양 따라 (가로로 넓으면 좌우, 세로로 길면 위아래)
|
||||
String explorerOrientation = 'side';
|
||||
|
||||
/// 기능 버튼 줄: 'middle' 두 창 사이 (기본) · 'edge' 오른쪽 (위아래 배치면 아래) 끝 · 'hidden' 숨김
|
||||
String explorerToolbar = 'middle';
|
||||
|
||||
/// 기능 버튼 줄에 보일 버튼과 순서 (ExplorerButton 이름). 비어 있으면 기본 구성.
|
||||
List<String> explorerButtons = [];
|
||||
|
||||
/// 정렬 · 숨긴 항목
|
||||
String explorerSort = 'name';
|
||||
bool explorerSortDesc = false;
|
||||
|
||||
/// 84: 정렬을 폴더마다 기억 (처음: 끔 - 모든 폴더가 같은 정렬). 켜면 열 머리 · 정렬 창이 지금 폴더에만 쓰고 [explorerFolderSorts] 에 둔다
|
||||
bool explorerSortPerFolder = false;
|
||||
|
||||
/// X-plore 비교 5-1 ① (관리자 지시 10/21): 종류별 기본 정렬 - 사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름.
|
||||
/// 처음 끔 (지금 정렬 그대로). 켜도 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억한다 ([explorerFolderSorts])
|
||||
bool explorerSortByKind = false;
|
||||
|
||||
/// X-plore 비교 5-1 ② (관리자 지시 10/21): 원격 (WebDAV · SFTP · SMB · FTP) 썸네일 - 'wifi' 데이터 요금제가 아닌 망에서만 (처음 값) ·
|
||||
/// 'always' 늘 · 'off' 받지 않음. X-plore 모양의 목록에서 보이는 줄의 그림 · 동영상만, 동시에 2개까지
|
||||
String explorerRemoteThumbs = 'wifi';
|
||||
|
||||
/// 원격 썸네일 캐시 상한 (MB, 처음 200) - 넘으면 오래 쓰지 않은 것부터 지운다
|
||||
int explorerRemoteThumbCacheMb = 200;
|
||||
static const remoteThumbCacheChoices = [50, 100, 200, 500, 1000];
|
||||
|
||||
/// X-plore 비교 5-1 ⑥ (관리자 지시 10/21): 파일 탐색기 단축키 중 사용자가 바꾼 것만 (동작 → 키, '' = 그 동작은 단축키 없음).
|
||||
/// 비어 있으면 (처음) 스타일대로 (83) - ui/explorer_keys.dart
|
||||
Map<String, String> explorerKeyOverrides = {};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// X-plore 비교 5-2 암호 보관함 (사용자 결정 10/21, 관리자 결정): 처음 끔. 보관함 자체는 설정이 아니라 그 폴더 (vault.json · *.jjv) 에 있다 -
|
||||
/// 설정을 처음 상태로 돌려도 보관함은 그대로 (끄기만 됨)
|
||||
|
|
@ -274,17 +221,9 @@ class AppSettings {
|
|||
/// 앱이 뒤로 간 뒤 이만큼 (초) 지나면 다시 잠금 (처음 60)
|
||||
int vaultAutoLockSeconds = 60;
|
||||
|
||||
/// 84: 폴더 → "기준|desc" (예: "size|1"). 오래된 것부터 지워 300개까지
|
||||
Map<String, String> explorerFolderSorts = {};
|
||||
|
||||
/// 80: 폴더 크기 계산 - 'sort' 크기순 정렬일 때만 (처음 값) · 'always' 늘 · 'off' 안 함 (길게 누르기 [폴더 크기 계산] 은 늘 됨)
|
||||
String explorerFolderSize = 'sort';
|
||||
|
||||
/// 82: 창 사이 끌어 놓기 - 'ask' 묻기 (처음 값, 관리자 판단) · 'copy' 복사 · 'move' 이동
|
||||
String explorerDropAction = 'ask';
|
||||
|
||||
/// 85: 탐색기의 복사 · 이동 진행 패널을 숨김 (아래 작은 표시만). 처음 값: 보임
|
||||
bool explorerProgressHidden = false;
|
||||
|
||||
/// 89: Wi-Fi 공유 - 저절로 끄는 시간 (분, 0 = 끄지 않음). 처음 값 30분 (관리자 지시 10/17)
|
||||
int wifiShareMinutes = 30;
|
||||
|
|
@ -297,14 +236,8 @@ class AppSettings {
|
|||
|
||||
/// 86: ZIP 으로 묶기의 압축 정도 (1 빠르게 · 6 보통 (처음 값) · 9 작게) - 마지막에 고른 것을 기억
|
||||
int zipLevel = 6;
|
||||
bool explorerShowHidden = false;
|
||||
|
||||
/// X-plore 숨기기 (관리자 지시 10/18): 파일 탐색기 목록에서 숨긴 파일 · 폴더의 경로 (파일 자체는 바꾸지 않는다).
|
||||
/// 지우기 · 이동 대상에서는 빠지지 않는다. 목록 맨 아래 [숨긴 항목 보이기] 로 다시 본다
|
||||
List<String> explorerHiddenItems = [];
|
||||
|
||||
/// X-plore 맞추기 (관리자 지시 10/20): [목록에서 숨기기] 전에 무엇이 바뀌는지 확인 창 (처음 켬, 창의 [다음부터 묻지 않기] · 환경 설정에서 끔)
|
||||
bool explorerHideConfirm = true;
|
||||
|
||||
// ── 복사 · 이동 (파일 탐색기) · rsync · lsync (Rsync 화면) ──
|
||||
/// 파일 탐색기의 복사 · 이동 방법: 'builtin' 현재 방식 (기본) · 'robocopy' (Windows). 파일만 고를 때 / 폴더가 들어 있을 때.
|
||||
|
|
@ -533,11 +466,7 @@ class AppSettings {
|
|||
}
|
||||
}
|
||||
|
||||
/// 마지막으로 연 폴더 (왼쪽 · 오른쪽 창)
|
||||
List<String> explorerPaths = [];
|
||||
|
||||
/// 최근에 연 폴더 (내역, 최신이 앞)
|
||||
List<String> explorerHistory = [];
|
||||
|
||||
/// 앱 안 브라우저의 데이터 폴더 (로그인 · 쿠키). 실행 중에 정해지며 저장하지 않음
|
||||
String webViewDataDir = '';
|
||||
|
|
@ -575,8 +504,11 @@ class AppSettings {
|
|||
|
||||
AppSettings();
|
||||
|
||||
/// 파일 탐색기 (X-plore 참고)
|
||||
final explorer = ExplorerSettings();
|
||||
|
||||
/// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다
|
||||
List<SettingsGroup> get groups => const [];
|
||||
List<SettingsGroup> get groups => [explorer];
|
||||
|
||||
/// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다
|
||||
Map<String, Object?> toJson({bool plainSecrets = false}) {
|
||||
|
|
@ -647,50 +579,15 @@ class AppSettings {
|
|||
'torHomeUrl': torHomeUrl,
|
||||
'torJavaScript': torJavaScript,
|
||||
'torNoticeSeen': torNoticeSeen,
|
||||
'explorerLayout': explorerLayout,
|
||||
'explorerOnePaneCopy': explorerOnePaneCopy,
|
||||
'explorerRetapCollapse': explorerRetapCollapse,
|
||||
'explorerTextScale': explorerTextScale,
|
||||
'explorerRowSpacing': explorerRowSpacing,
|
||||
'batchRenameLast': batchRenameLast,
|
||||
'explorerRootsInTree': explorerRootsInTree,
|
||||
'explorerAlwaysMarks': explorerAlwaysMarks,
|
||||
'explorerSwipePanes': explorerSwipePanes,
|
||||
'androidTrashDays': androidTrashDays,
|
||||
'explorerClick': explorerClick,
|
||||
// 예전 기본값 (두 창 · 한 번 누르면 선택) 은 사용자가 고른 값이 아니므로 한 번 새 기본값으로 옮긴다
|
||||
'explorerV2': true,
|
||||
// E3 (10/22): 선택 규칙 (사용자 지시 모음) 의 처음 값으로 한 번 옮겼음
|
||||
'explorerRulesV1': true,
|
||||
'explorerStyle': explorerStyle,
|
||||
'explorerOrientation': explorerOrientation,
|
||||
'explorerOrientV3': true,
|
||||
'explorerToolbar': explorerToolbar,
|
||||
'explorerButtons': explorerButtons,
|
||||
'explorerSort': explorerSort,
|
||||
'explorerSortDesc': explorerSortDesc,
|
||||
'explorerFolderSize': explorerFolderSize,
|
||||
'explorerDropAction': explorerDropAction,
|
||||
'explorerProgressHidden': explorerProgressHidden,
|
||||
'zipLevel': zipLevel,
|
||||
'wifiShareMinutes': wifiShareMinutes,
|
||||
'wifiShareTailscale': wifiShareTailscale,
|
||||
'wifiShareFolder': wifiShareFolder,
|
||||
'explorerSortPerFolder': explorerSortPerFolder,
|
||||
'explorerSortByKind': explorerSortByKind,
|
||||
'explorerRemoteThumbs': explorerRemoteThumbs,
|
||||
'explorerRemoteThumbCacheMb': explorerRemoteThumbCacheMb,
|
||||
'explorerKeyOverrides': explorerKeyOverrides,
|
||||
'vaultEnabled': vaultEnabled,
|
||||
'shizukuEnabled': shizukuEnabled,
|
||||
'rootEnabled': rootEnabled,
|
||||
'vaultMasterCopy': vaultMasterCopy,
|
||||
'vaultAutoLockSeconds': vaultAutoLockSeconds,
|
||||
'explorerFolderSorts': explorerFolderSorts,
|
||||
'explorerShowHidden': explorerShowHidden,
|
||||
'explorerHiddenItems': explorerHiddenItems,
|
||||
'explorerHideConfirm': explorerHideConfirm,
|
||||
'explorerPaths': explorerPaths,
|
||||
'copyMethodFile': copyMethodFile,
|
||||
'copyConflict': copyConflict,
|
||||
'mkvExisting': mkvExisting,
|
||||
|
|
@ -779,7 +676,6 @@ class AppSettings {
|
|||
'backgroundV2': true,
|
||||
'liveSyncOnStart': liveSyncOnStart,
|
||||
'copyTasks': [for (final x in copyTasks) x.toJson()],
|
||||
'explorerHistory': explorerHistory,
|
||||
'autoCheckUpdates': autoCheckUpdates,
|
||||
'lastUpdateCheck': lastUpdateCheck,
|
||||
'lastUpdateCheckVersion': lastUpdateCheckVersion,
|
||||
|
|
@ -896,70 +792,15 @@ class AppSettings {
|
|||
: defaultTorHome
|
||||
..torJavaScript = j['torJavaScript'] != false
|
||||
..torNoticeSeen = j['torNoticeSeen'] == true
|
||||
// 'auto' = 넓은 화면은 두 창, 좁은 화면 (폰 세로) 은 한 창. 예전 기본 'dual' 은 한 번 'auto' 로
|
||||
..explorerOnePaneCopy = j['explorerOnePaneCopy'] == 'other' ? 'other' : 'clip'
|
||||
// E3: 규칙 이전 판이 저장한 false 는 예전 처음 값 - 한 번 규칙 3 (켬) 으로
|
||||
..explorerRetapCollapse = j['explorerRulesV1'] == true ? j['explorerRetapCollapse'] != false : true
|
||||
// 값이 없거나 틀리면 지금 그대로 (100% · 보통), 범위 밖은 끝으로
|
||||
..explorerTextScale = j['explorerTextScale'] is num
|
||||
? (j['explorerTextScale'] as num).round().clamp(explorerTextScaleMin, explorerTextScaleMax)
|
||||
: 100
|
||||
..explorerRowSpacing = 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) : {}
|
||||
..explorerRootsInTree = j['explorerRootsInTree'] != false
|
||||
// E3: 규칙 이전 판이 저장한 true 는 예전 처음 값 - 한 번 규칙 4 (끔) 로
|
||||
..explorerAlwaysMarks = j['explorerRulesV1'] == true && j['explorerAlwaysMarks'] == true
|
||||
..explorerSwipePanes = j['explorerSwipePanes'] == true
|
||||
..androidTrashDays = switch (j['androidTrashDays']) { final int d when d >= 0 && d <= 365 => d, _ => 14 }
|
||||
// E3 (규칙 7 · 결정 3): 처음 값 두 창. 규칙 이전 판의 'auto' 는 예전 처음 값이라 한 번 두 창으로
|
||||
..explorerLayout = switch (j['explorerLayout']) {
|
||||
'single' || 'split' => j['explorerLayout'] as String,
|
||||
'auto' when j['explorerRulesV1'] == true => 'auto',
|
||||
_ => 'dual',
|
||||
}
|
||||
// 규칙 2: 두 기기 모두 한 번 = 선택. 규칙 이전 Android 의 'open' 은 예전 처음 값이라 한 번 'select' 로
|
||||
..explorerClick = j['explorerClick'] == 'open' && (j['explorerRulesV1'] == true || !Platform.isAndroid) ? 'open' : 'select'
|
||||
..explorerStyle = const ['windows', 'totalcmd'].contains(j['explorerStyle']) ? j['explorerStyle'] as String : 'xplore'
|
||||
// 118: 예전 기본 '화면 모양 따라' 는 한 번 좌우로 옮긴다 (사용자가 고른 위아래는 그대로)
|
||||
..explorerOrientation = j['explorerOrientation'] == 'stacked'
|
||||
? 'stacked'
|
||||
: j['explorerOrientation'] == 'auto' && j['explorerOrientV3'] == true
|
||||
? 'auto'
|
||||
: 'side'
|
||||
..explorerToolbar = const ['edge', 'hidden'].contains(j['explorerToolbar']) ? j['explorerToolbar'] as String : 'middle'
|
||||
..explorerButtons = [for (final x in (j['explorerButtons'] as List?) ?? const []) '$x']
|
||||
..explorerSort = const ['date', 'size', 'type'].contains(j['explorerSort']) ? j['explorerSort'] as String : 'name'
|
||||
..explorerSortDesc = j['explorerSortDesc'] as bool? ?? false
|
||||
..explorerFolderSize = const ['sort', 'always', 'off'].contains(j['explorerFolderSize']) ? j['explorerFolderSize'] as String : 'sort'
|
||||
..explorerSortPerFolder = j['explorerSortPerFolder'] == true
|
||||
..explorerSortByKind = j['explorerSortByKind'] == true
|
||||
..explorerRemoteThumbs = const {'off', 'wifi', 'always'}.contains(j['explorerRemoteThumbs']) ? j['explorerRemoteThumbs'] as String : 'wifi'
|
||||
..vaultEnabled = j['vaultEnabled'] == true
|
||||
..shizukuEnabled = j['shizukuEnabled'] == true
|
||||
..rootEnabled = j['rootEnabled'] == true
|
||||
..vaultMasterCopy = j['vaultMasterCopy'] == true
|
||||
..vaultAutoLockSeconds = j['vaultAutoLockSeconds'] is num ? (j['vaultAutoLockSeconds'] as num).round().clamp(0, 3600) : 60
|
||||
..explorerKeyOverrides = 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}
|
||||
: {}
|
||||
..explorerRemoteThumbCacheMb = j['explorerRemoteThumbCacheMb'] is num
|
||||
? (j['explorerRemoteThumbCacheMb'] as num).round().clamp(10, 10000)
|
||||
: 200
|
||||
..explorerProgressHidden = j['explorerProgressHidden'] == true
|
||||
..zipLevel = const [1, 6, 9].contains(j['zipLevel']) ? j['zipLevel'] as int : 6
|
||||
..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 : ''
|
||||
..explorerDropAction = const ['ask', 'copy', 'move'].contains(j['explorerDropAction']) ? j['explorerDropAction'] as String : 'ask'
|
||||
..explorerFolderSorts = {
|
||||
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,
|
||||
}
|
||||
..explorerShowHidden = j['explorerShowHidden'] as bool? ?? false
|
||||
..explorerHiddenItems = [for (final x in (j['explorerHiddenItems'] as List?) ?? const []) if (x is String && x.isNotEmpty) x]
|
||||
..explorerHideConfirm = j['explorerHideConfirm'] as bool? ?? true
|
||||
..explorerPaths = [for (final x in (j['explorerPaths'] as List?) ?? const []) '$x']
|
||||
..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'
|
||||
|
|
@ -1079,7 +920,6 @@ class AppSettings {
|
|||
for (final x in (j['copyTasks'] as List?) ?? const [])
|
||||
if (x is Map && x['dest'] is String) CopyTask.fromJson(x),
|
||||
]
|
||||
..explorerHistory = [for (final x in (j['explorerHistory'] as List?) ?? const []) '$x']
|
||||
..autoCheckUpdates = j['autoCheckUpdates'] as bool? ?? true
|
||||
..lastUpdateCheck = j['lastUpdateCheck'] as String? ?? ''
|
||||
..lastUpdateCheckVersion = j['lastUpdateCheckVersion'] as String? ?? ''
|
||||
|
|
@ -1103,10 +943,11 @@ class AppSettings {
|
|||
..aiSource = j['aiSource'] as String? ?? 'und'
|
||||
..aiTargets = (j['aiTargets'] as List?)?.cast<String>() ?? ['ko', 'en', 'ja']
|
||||
..aiWhisper = j['aiWhisper'] as String? ?? 'whisper-base';
|
||||
s.migrated = _migrated(j, s);
|
||||
// 분야를 먼저 읽는다 - 바뀐 기본값 알림 ([_migrated]) 이 분야의 값을 본다
|
||||
for (final g in s.groups) {
|
||||
g.read(j);
|
||||
}
|
||||
s.migrated = _migrated(j, s);
|
||||
s.extra = Map.of(j);
|
||||
return s;
|
||||
}
|
||||
|
|
@ -1163,8 +1004,8 @@ class AppSettings {
|
|||
static List<String> _migrated(Map<String, dynamic> j, AppSettings s) => [
|
||||
if (j['orientationV2'] != true && j['screenOrientation'] == 'landscape' && s.screenOrientation == 'auto')
|
||||
'orientation',
|
||||
if (j['explorerV2'] != true && j['explorerLayout'] == 'dual' && s.explorerLayout == 'auto') 'explorerLayout',
|
||||
if (j['explorerV2'] != true && j['explorerClick'] == 'select' && s.explorerClick == 'open') 'explorerClick',
|
||||
if (j['explorerV2'] != true && j['explorerLayout'] == 'dual' && s.explorer.layout == 'auto') 'explorerLayout',
|
||||
if (j['explorerV2'] != true && j['explorerClick'] == 'select' && s.explorer.click == 'open') 'explorerClick',
|
||||
if (j['explorerOrientV3'] != true && j['explorerOrientation'] == 'auto') 'explorerOrientation',
|
||||
if (j['aiImageV1'] != true && j['components'] is List && !(j['components'] as List).contains('aiimage')) 'aiImage',
|
||||
// 53 · 28: 앱 안 브라우저 쿠키를 쓰던 사람에게 넘기는 사이트가 넓어졌음을 한 번 알린다
|
||||
|
|
@ -1175,7 +1016,7 @@ class AppSettings {
|
|||
'background',
|
||||
// 193: 그 설정이 생기기 전 판 (설정 파일은 있는데 그 항목이 없음 - 예: 10.03) 에서 올라온 사람이 바로 겪는 동작 변화.
|
||||
// 예전 판에는 키가 없어 위의 "예전 값 → 새 값" 조건에 걸리지 않았다 (P11: 탐색기에서 한 번 눌렀는데 바로 열려 놀람)
|
||||
if (j.isNotEmpty && !j.containsKey('explorerClick') && s.explorerClick == 'open') 'explorerClickNew',
|
||||
if (j.isNotEmpty && !j.containsKey('explorerClick') && s.explorer.click == 'open') 'explorerClickNew',
|
||||
// E3 (10/22): 선택 규칙의 처음 값으로 옮긴 것 - 규칙 이전 판에서 올라온 사람에게 한 번
|
||||
if (j.isNotEmpty && j['explorerRulesV1'] != true && j['explorerAlwaysMarks'] != false) 'explorerMarksRule',
|
||||
if (j.isNotEmpty && j['explorerRulesV1'] != true && j['explorerRetapCollapse'] != true) 'explorerRetapRule',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,211 @@
|
|||
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'];
|
||||
}
|
||||
}
|
||||
|
|
@ -55,7 +55,7 @@ class ExplorerDeleteFlow {
|
|||
var permanent = !canRecycle || shift;
|
||||
// X-plore 숨기기: 지우는 폴더 안에 목록에서 숨긴 것이 있으면 알린다 (숨긴 것도 함께 지워진다)
|
||||
final hiddenInside = [
|
||||
for (final h in ctl.c.settings.explorerHiddenItems)
|
||||
for (final h in ctl.c.settings.explorer.hiddenItems)
|
||||
if (paths.any((x) => !samePath(h, x) && isSameOrInside(h, x))) h
|
||||
].length;
|
||||
final otherMarks = [for (final x in ctl.panes) if (!identical(x, pane)) x.sel.marked.length].fold(0, (a, b) => a + b);
|
||||
|
|
@ -100,7 +100,7 @@ class ExplorerDeleteFlow {
|
|||
if (canTrash && !permanent)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(trf('앱 휴지통에 {0}일 두었다가 지웁니다 (탐색기 [휴지통] 에서 되살리기).', [ctl.c.settings.androidTrashDays]),
|
||||
child: Text(trf('앱 휴지통에 {0}일 두었다가 지웁니다 (탐색기 [휴지통] 에서 되살리기).', [ctl.c.settings.explorer.androidTrashDays]),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
if (canRecycle)
|
||||
|
|
@ -231,7 +231,7 @@ class ExplorerDeleteFlow {
|
|||
}
|
||||
|
||||
/// X-plore 비교 e: Android 앱 휴지통 (보관 일이 0 이면 null - 바로 영구 삭제)
|
||||
AppTrash? get appTrash => androidTrash() && ctl.c.settings.androidTrashDays > 0
|
||||
AppTrash? get appTrash => androidTrash() && ctl.c.settings.explorer.androidTrashDays > 0
|
||||
? AppTrash([for (final (v, _) in ctl.local) v])
|
||||
: null;
|
||||
|
||||
|
|
@ -256,7 +256,7 @@ class ExplorerDeleteFlow {
|
|||
return;
|
||||
}
|
||||
final back = await Navigator.of(context).push<List<String>>(
|
||||
MaterialPageRoute(builder: (_) => TrashPage(trash: trash, days: ctl.c.settings.androidTrashDays)));
|
||||
MaterialPageRoute(builder: (_) => TrashPage(trash: trash, days: ctl.c.settings.explorer.androidTrashDays)));
|
||||
if (back != null && back.isNotEmpty && context.mounted) await ctl.refreshAll(back.map(p.dirname));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class ExplorerDrops {
|
|||
ctl
|
||||
..active = to
|
||||
..update();
|
||||
final mode = ctl.c.settings.explorerDropAction;
|
||||
final mode = ctl.c.settings.explorer.dropAction;
|
||||
final move = mode == 'ask' ? await askDropAction(context, load.paths, dest) : mode == 'move';
|
||||
if (move == null || !context.mounted) return;
|
||||
// 되돌리기에 쓸 것: 대상에 이미 있던 이름 (합친 폴더는 통째로 되돌리지 않게)
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ class ExplorerTopBar extends StatelessWidget {
|
|||
child: Row(children: [Icon(b.icon, size: 18), const SizedBox(width: 12), Text(tr(b.label))]),
|
||||
),
|
||||
// X-plore 숨기기: 큰 폴더를 펼쳐 목록 맨 아래가 멀 때도 (관리자 결정 10/18)
|
||||
if (!ctl.rsync && ctl.c.settings.explorerHiddenItems.isNotEmpty)
|
||||
if (!ctl.rsync && ctl.c.settings.explorer.hiddenItems.isNotEmpty)
|
||||
PopupMenuItem(
|
||||
value: 'appHidden',
|
||||
child: Row(children: [
|
||||
|
|
@ -97,7 +97,7 @@ class ExplorerTopBar extends StatelessWidget {
|
|||
Flexible(
|
||||
child: Text(ctl.showAppHidden
|
||||
? tr('목록에서 숨긴 항목 다시 숨기기')
|
||||
: trf('목록에서 숨긴 항목 보이기 ({0}개)', [ctl.c.settings.explorerHiddenItems.length])),
|
||||
: trf('목록에서 숨긴 항목 보이기 ({0}개)', [ctl.c.settings.explorer.hiddenItems.length])),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class ExplorerPanesBody extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = ctl.c.settings;
|
||||
final shape = '${s.explorerStyle}|${s.explorerLayout}|${s.explorerOrientation}|${s.explorerToolbar}';
|
||||
final shape = '${s.explorer.style}|${s.explorer.layout}|${s.explorer.orientation}|${s.explorer.toolbar}';
|
||||
if (ctl.ready && shape != layout.shape) {
|
||||
if (layout.shape.isNotEmpty) {
|
||||
for (final pane in ctl.panes) {
|
||||
|
|
@ -41,10 +41,10 @@ class ExplorerPanesBody extends StatelessWidget {
|
|||
return LayoutBuilder(builder: (context, box) {
|
||||
// 두 창이 다 들어가는지: 트리 (화살표 · 아이콘 · 여백 78 + 이름 40) + 목록 (아이콘 · ✔ · 여백 104 + 이름 50), 이름은 글자 배율만큼.
|
||||
// 1배면 360 폭에도 들어가고, 기기 글자 1.3 × 150% (약 2배) 면 412 폭에도 모자란다
|
||||
final wantSplit = !ctl.rsync && s.explorerLayout == 'split';
|
||||
final wantSplit = !ctl.rsync && s.explorer.layout == 'split';
|
||||
final textScale = MediaQuery.textScalerOf(context).scale(14) / 14 * layout.listTextScale;
|
||||
final barW = s.explorerToolbar == 'hidden' ? 0.0 : 72.0;
|
||||
final sideNow = switch (s.explorerOrientation) { 'side' => true, 'stacked' => false, _ => box.maxWidth >= box.maxHeight };
|
||||
final barW = s.explorer.toolbar == 'hidden' ? 0.0 : 72.0;
|
||||
final sideNow = switch (s.explorer.orientation) { 'side' => true, 'stacked' => false, _ => box.maxWidth >= box.maxHeight };
|
||||
final tightWas = layout.splitTight;
|
||||
layout.splitTight = wantSplit && sideNow && box.maxWidth - barW < 182 + 90 * textScale;
|
||||
// 바뀌면 이 builder 밖 (버튼 이름 "두 창" 등) 도 한 번 다시 그린다
|
||||
|
|
@ -57,7 +57,7 @@ class ExplorerPanesBody extends StatelessWidget {
|
|||
// 폭이 바뀌면 (좁은 한 창 ↔ 두 창) 이 builder 만 다시 불린다 - 그때도 새로 붙은 창을 보이게
|
||||
if (ctl.ready) layout.revealNewViews();
|
||||
final side = !(dual || split) ||
|
||||
switch (s.explorerOrientation) {
|
||||
switch (s.explorer.orientation) {
|
||||
'side' => true,
|
||||
'stacked' => false,
|
||||
_ => box.maxWidth >= box.maxHeight,
|
||||
|
|
@ -66,13 +66,13 @@ class ExplorerPanesBody extends StatelessWidget {
|
|||
// (두 창 + 가운데 버튼 줄이면 창이 140px 남짓이라 폴더 이름이 잘렸다)
|
||||
final rsyncOne = ctl.rsync && side && box.maxWidth < rsyncTwoPaneMinWidth;
|
||||
layout.narrowSplit = split && side && box.maxWidth < ExplorerLayout.narrowSplitWidth;
|
||||
final bar = s.explorerToolbar == 'hidden' && !ctl.rsync
|
||||
final bar = s.explorer.toolbar == 'hidden' && !ctl.rsync
|
||||
? null
|
||||
: ExplorerToolbar(
|
||||
// X-plore b: 길게 누르기 = 짝 동작 (Rsync 화면은 없음)
|
||||
onLongPress: ctl.rsync ? null : buttons.longPress,
|
||||
// Rsync 화면: 늘 같은 버튼 (→ ← ⇄ 모니터링) 을 두 창 사이에
|
||||
buttons: ctl.rsync ? ExplorerButton.rsyncButtons : ExplorerButton.fromSettings(s.explorerButtons),
|
||||
buttons: ctl.rsync ? ExplorerButton.rsyncButtons : ExplorerButton.fromSettings(s.explorer.buttons),
|
||||
vertical: side && !rsyncOne,
|
||||
panesSide: side,
|
||||
onPressed: buttons.press,
|
||||
|
|
@ -81,12 +81,12 @@ class ExplorerPanesBody extends StatelessWidget {
|
|||
_ => true,
|
||||
},
|
||||
selected: (b) =>
|
||||
(b == ExplorerButton.hidden && s.explorerShowHidden) || (b == ExplorerButton.select && sel.selecting),
|
||||
(b == ExplorerButton.hidden && s.explorer.showHidden) || (b == ExplorerButton.select && sel.selecting),
|
||||
twoPanes: dual || split,
|
||||
pinned: {ExplorerButton.orient, if (flows.transfers.clip.isNotEmpty) ExplorerButton.paste},
|
||||
);
|
||||
final middle = bar != null && (ctl.rsync || s.explorerToolbar == 'middle');
|
||||
final edge = bar != null && !ctl.rsync && s.explorerToolbar == 'edge';
|
||||
final middle = bar != null && (ctl.rsync || s.explorer.toolbar == 'middle');
|
||||
final edge = bar != null && !ctl.rsync && s.explorer.toolbar == 'edge';
|
||||
// 180: 창마다 같은 열쇠 - 가로 ↔ 세로 배치가 바뀌어도 (창 크기 · 작업 현황이 열림) 창을 새로 만들지 않는다 (스크롤이 맨 위로 가지 않게)
|
||||
Widget keep(int k, Widget w) => KeyedSubtree(key: layout.viewKeys[k], child: w);
|
||||
if (rsyncOne) {
|
||||
|
|
|
|||
|
|
@ -225,14 +225,14 @@ class ExplorerButtons {
|
|||
case ExplorerButton.sort:
|
||||
await flows.dialogs.sortDialog(cmd.context);
|
||||
case ExplorerButton.hidden:
|
||||
await ctl.c.updateSettings((s) => s.explorerShowHidden = !s.explorerShowHidden);
|
||||
await ctl.c.updateSettings((s) => s.explorer.showHidden = !s.explorer.showHidden);
|
||||
for (final x in ctl.panes) {
|
||||
x.cache.clear();
|
||||
for (final d in x.expanded.toList()) {
|
||||
await ctl.load(x, d, force: true);
|
||||
}
|
||||
}
|
||||
ctl.host.snack(ctl.c.settings.explorerShowHidden ? tr('숨은 항목을 보입니다.') : tr('숨은 항목을 감춥니다.'));
|
||||
ctl.host.snack(ctl.c.settings.explorer.showHidden ? tr('숨은 항목을 보입니다.') : tr('숨은 항목을 감춥니다.'));
|
||||
case ExplorerButton.history:
|
||||
await flows.dialogs.historyDialog(cmd.context, pane);
|
||||
case ExplorerButton.bookmarks:
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ class ExplorerController extends ChangeNotifier {
|
|||
final s = sizes.of(e.path);
|
||||
if (s != null) return s.label;
|
||||
if (sizes.busy(e.path)) return '…';
|
||||
final mode = c.settings.explorerFolderSize;
|
||||
final mode = c.settings.explorer.folderSize;
|
||||
if ((mode == 'always' || (mode == 'sort' && sortOf(q.current).$1 == 'size')) &&
|
||||
samePath(vDirname(e.path), q.current) &&
|
||||
!isDav(e.path)) {
|
||||
|
|
@ -165,7 +165,7 @@ class ExplorerController extends ChangeNotifier {
|
|||
Future<void> load(ExplorerPane q, String dir, {bool force = false}) async {
|
||||
final r = await loadDir(q, dir,
|
||||
force: force,
|
||||
showHidden: c.settings.explorerShowHidden,
|
||||
showHidden: c.settings.explorer.showHidden,
|
||||
sort: sorted,
|
||||
inside: _insideArchiveList,
|
||||
osBlocked: (d) => isAndroidRestricted(d) ? androidRestrictedError : null,
|
||||
|
|
@ -272,15 +272,15 @@ class ExplorerController extends ChangeNotifier {
|
|||
/// [dir] 의 정렬 (기준, 거꾸로) - 폴더마다 기억한 것 → 종류별 기본 정렬 → 전체 정렬
|
||||
(String, bool) sortOf(String dir, {List<FileEntry>? list}) {
|
||||
final s = c.settings;
|
||||
if (s.explorerSortPerFolder || s.explorerSortByKind) {
|
||||
final v = s.explorerFolderSorts[_sortKey(dir)];
|
||||
if (s.explorer.sortPerFolder || s.explorer.sortByKind) {
|
||||
final v = s.explorer.folderSorts[_sortKey(dir)];
|
||||
if (v != null && v.contains('|')) return (v.split('|').first, v.endsWith('|1'));
|
||||
}
|
||||
if (s.explorerSortByKind) {
|
||||
if (s.explorer.sortByKind) {
|
||||
final k = _kindSort(dir, list: list);
|
||||
if (k != null) return k;
|
||||
}
|
||||
return (s.explorerSort, s.explorerSortDesc);
|
||||
return (s.explorer.sort, s.explorer.sortDesc);
|
||||
}
|
||||
|
||||
/// X-plore 비교 5-1 ①: 파일의 반 넘게가 사진 · 동영상이면 날짜 (새것 먼저), 음악이면 이름, 그 밖은 null
|
||||
|
|
@ -301,19 +301,19 @@ class ExplorerController extends ChangeNotifier {
|
|||
|
||||
/// 정렬을 바꾼다 - "폴더마다 기억" (또는 종류별 정렬이 쓰이는 폴더) 이면 [dir] 에만, 아니면 전체. 모든 창을 다시 줄 세운다
|
||||
Future<void> setSort(String dir, String by, bool desc) async {
|
||||
final kindHere = c.settings.explorerSortByKind && _kindSort(dir) != null;
|
||||
final kindHere = c.settings.explorer.sortByKind && _kindSort(dir) != null;
|
||||
await c.updateSettings((x) {
|
||||
if (x.explorerSortPerFolder || kindHere) {
|
||||
final m = Map<String, String>.of(x.explorerFolderSorts)..remove(_sortKey(dir));
|
||||
if (x.explorer.sortPerFolder || kindHere) {
|
||||
final m = Map<String, String>.of(x.explorer.folderSorts)..remove(_sortKey(dir));
|
||||
m[_sortKey(dir)] = '$by|${desc ? 1 : 0}';
|
||||
while (m.length > 300) {
|
||||
m.remove(m.keys.first);
|
||||
}
|
||||
x.explorerFolderSorts = m;
|
||||
x.explorer.folderSorts = m;
|
||||
} else {
|
||||
x
|
||||
..explorerSort = by
|
||||
..explorerSortDesc = desc;
|
||||
..explorer.sort = by
|
||||
..explorer.sortDesc = desc;
|
||||
}
|
||||
});
|
||||
resortAll();
|
||||
|
|
@ -409,18 +409,18 @@ class ExplorerController extends ChangeNotifier {
|
|||
if (rsync) {
|
||||
unawaited(c.updateSettings((s) => s.rsyncPaths = paths));
|
||||
} else {
|
||||
final hist = [dir, ...c.settings.explorerHistory.where((h) => !samePath(h, dir))].take(30).toList();
|
||||
final hist = [dir, ...c.settings.explorer.history.where((h) => !samePath(h, dir))].take(30).toList();
|
||||
unawaited(c.updateSettings((s) => s
|
||||
..explorerPaths = paths
|
||||
..explorerHistory = hist));
|
||||
..explorer.paths = paths
|
||||
..explorer.history = hist));
|
||||
}
|
||||
if (i >= 0) active = i;
|
||||
}
|
||||
|
||||
// ───────── 줄 목록 · 목록에서 숨기기 ─────────
|
||||
|
||||
ExplorerStyle get look => ExplorerStyle.of(c.settings.explorerStyle);
|
||||
bool get rootsInTree => !rsync && look == ExplorerStyle.xplore && c.settings.explorerRootsInTree;
|
||||
ExplorerStyle get look => ExplorerStyle.of(c.settings.explorer.style);
|
||||
bool get rootsInTree => !rsync && look == ExplorerStyle.xplore && c.settings.explorer.rootsInTree;
|
||||
|
||||
List<TreeRow> rows(ExplorerPane q, {bool foldersOnly = false}) => buildTreeRows(q,
|
||||
foldersOnly: foldersOnly,
|
||||
|
|
@ -430,12 +430,12 @@ class ExplorerController extends ChangeNotifier {
|
|||
showHidden: showAppHidden,
|
||||
browsable: arc.contains);
|
||||
|
||||
bool appHidden(String path) => hidden.isHidden(c.settings.explorerHiddenItems, path);
|
||||
bool appHidden(String path) => hidden.isHidden(c.settings.explorer.hiddenItems, path);
|
||||
|
||||
/// 옮기거나 이름을 바꾼 것은 숨긴 목록도 새 경로로, 지운 것은 목록에서 뺀다 (관리자 결정 10/18)
|
||||
Future<void> followHidden({Iterable<(String, String)> moved = const [], Iterable<String> gone = const []}) async {
|
||||
final out = await HiddenList.follow(c.settings.explorerHiddenItems, moved: moved, gone: gone);
|
||||
if (out != null) await c.updateSettings((s) => s.explorerHiddenItems = out);
|
||||
final out = await HiddenList.follow(c.settings.explorer.hiddenItems, moved: moved, gone: gone);
|
||||
if (out != null) await c.updateSettings((s) => s.explorer.hiddenItems = out);
|
||||
}
|
||||
|
||||
void toggleAppHidden() {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class ExplorerJobs extends ChangeNotifier {
|
|||
|
||||
/// X-plore 숨기기: 옮긴 것은 숨긴 목록도 새 자리로 (원래 자리에 없고 새 자리에 있는 것만 - 같은 이름 바꾸기 · 실패는 그대로)
|
||||
Future<void> followMoved(TransferJob job) async {
|
||||
if (!job.move || job.contents || ctl.c.settings.explorerHiddenItems.isEmpty) return;
|
||||
if (!job.move || job.contents || ctl.c.settings.explorer.hiddenItems.isEmpty) return;
|
||||
final moved = <(String, String)>[];
|
||||
for (final s in job.sources) {
|
||||
final to = vJoin(job.dest, vBasename(s));
|
||||
|
|
|
|||
|
|
@ -52,11 +52,11 @@ class ExplorerLayout {
|
|||
});
|
||||
|
||||
/// X-plore 비교 40: 목록 글자 크기 (설정 %) · 줄 간격을 넣은 줄 높이
|
||||
double get listTextScale => ctl.c.settings.explorerTextScale / 100;
|
||||
double rowHeight() => ctl.look.rowHeightOf(context(), textScale: listTextScale, spacing: rowSpacingFactor(ctl.c.settings.explorerRowSpacing));
|
||||
double get listTextScale => ctl.c.settings.explorer.textScale / 100;
|
||||
double rowHeight() => ctl.look.rowHeightOf(context(), textScale: listTextScale, spacing: rowSpacingFactor(ctl.c.settings.explorer.rowSpacing));
|
||||
|
||||
/// 왼쪽 폴더 트리 + 오른쪽 파일 목록 배치
|
||||
bool get split => !ctl.rsync && ctl.c.settings.explorerLayout == 'split' && !splitTight;
|
||||
bool get split => !ctl.rsync && ctl.c.settings.explorer.layout == 'split' && !splitTight;
|
||||
|
||||
/// 관리자 지시 10/20 (28 검토): 글자가 커서 (기기 글자 × 목록 글자 크기) 좁은 화면에 트리 · 목록 두 창이 다 들어가지 않으면
|
||||
/// 그동안만 한 창 (트리 + 위 경로 막대) 으로 보인다 - 고른 배치 (설정) 는 그대로, 화면이 넓어지거나 글자가 작아지면 다시 두 창. [ExplorerPanesBody] 가 정한다
|
||||
|
|
@ -70,18 +70,18 @@ class ExplorerLayout {
|
|||
/// 좌우 두 창 (Rsync 화면은 늘)
|
||||
bool get dual =>
|
||||
ctl.rsync ||
|
||||
ctl.c.settings.explorerLayout == 'dual' ||
|
||||
ctl.c.settings.explorer.layout == 'dual' ||
|
||||
// 자동: 넓으면 두 창, 좁은 화면 (폰 세로) 은 한 창 - 같은 목록이 반씩 잘려 두 번 보이지 않게
|
||||
(ctl.c.settings.explorerLayout == 'auto' && !isCompact(context()));
|
||||
(ctl.c.settings.explorer.layout == 'auto' && !isCompact(context()));
|
||||
|
||||
|
||||
/// 트리 줄. [foldersOnly]: 폴더만 ("폴더 + 파일 목록" 배치의 왼쪽)
|
||||
/// X-plore 비교 c: 트리 맨 위에 저장 장치 · 서버 · 즐겨찾기 (X-plore 스타일 · 설정)
|
||||
/// X-plore 비교 ⚖: X-plore 스타일에서 표시 동그라미 늘 보이기 (설정)
|
||||
bool get alwaysMarks => !ctl.rsync && ctl.look == ExplorerStyle.xplore && ctl.c.settings.explorerAlwaysMarks;
|
||||
bool get alwaysMarks => !ctl.rsync && ctl.look == ExplorerStyle.xplore && ctl.c.settings.explorer.alwaysMarks;
|
||||
|
||||
/// X-plore 비교 ⚖: X-plore 스타일 탐색기 안에서 밀기 = 창 바꾸기 (설정)
|
||||
bool get swipePanes => !ctl.rsync && ctl.look == ExplorerStyle.xplore && ctl.c.settings.explorerSwipePanes;
|
||||
bool get swipePanes => !ctl.rsync && ctl.look == ExplorerStyle.xplore && ctl.c.settings.explorer.swipePanes;
|
||||
|
||||
/// 밀어서 창 바꾸기: 두 창이면 다른 창을 활성으로, 한 창이면 숨은 창과 바꿔 보인다
|
||||
bool switchPane(int dir) {
|
||||
|
|
@ -104,5 +104,5 @@ class ExplorerLayout {
|
|||
ExplorerPane? get other => dual || hiddenTarget ? ctl.panes[1 - ctl.active] : null;
|
||||
|
||||
/// X-plore 비교 a: 한 창인데 "숨은 다른 창으로" 복사 · 이동 (위에 갈 곳 줄)
|
||||
bool get hiddenTarget => !ctl.rsync && !dual && !split && ctl.c.settings.explorerOnePaneCopy == 'other';
|
||||
bool get hiddenTarget => !ctl.rsync && !dual && !split && ctl.c.settings.explorer.onePaneCopy == 'other';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class ExplorerSelect extends ChangeNotifier {
|
|||
bool selecting = false;
|
||||
|
||||
/// 한 번 누르면 선택 · 두 번 누르면 열기 (아니면 한 번에 바로 열기)
|
||||
bool get selectMode => ctl.c.settings.explorerClick != 'open';
|
||||
bool get selectMode => ctl.c.settings.explorer.click != 'open';
|
||||
|
||||
/// 마지막으로 누른 폴더 · 시각 (두 번 누르기의 두 번째를 가리려고)
|
||||
(String, DateTime)? _lastFolderTap;
|
||||
|
|
@ -83,7 +83,7 @@ class ExplorerSelect extends ChangeNotifier {
|
|||
list: list,
|
||||
repeat: repeat,
|
||||
tapOpens: !selectMode,
|
||||
retapCollapses: ctl.c.settings.explorerRetapCollapse);
|
||||
retapCollapses: ctl.c.settings.explorer.retapCollapse);
|
||||
await _act(pane, row, act, list: list);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ Future<bool> confirmAppHide(BuildContext context, ExplorerController ctl, List<S
|
|||
),
|
||||
);
|
||||
if (ok != true) return false;
|
||||
if (dontAsk) await ctl.c.updateSettings((s) => s.explorerHideConfirm = false);
|
||||
if (dontAsk) await ctl.c.updateSettings((s) => s.explorer.hideConfirm = false);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -233,9 +233,9 @@ class ExplorerItemMenu {
|
|||
pane.changed();
|
||||
}
|
||||
case 'appHide':
|
||||
if (ctl.c.settings.explorerHideConfirm && !await confirmAppHide(context, ctl, targets)) return;
|
||||
await ctl.c.updateSettings((s) => s.explorerHiddenItems = [
|
||||
...s.explorerHiddenItems,
|
||||
if (ctl.c.settings.explorer.hideConfirm && !await confirmAppHide(context, ctl, targets)) return;
|
||||
await ctl.c.updateSettings((s) => s.explorer.hiddenItems = [
|
||||
...s.explorer.hiddenItems,
|
||||
for (final x in targets)
|
||||
if (!ctl.appHidden(x)) x,
|
||||
]);
|
||||
|
|
@ -249,8 +249,8 @@ class ExplorerItemMenu {
|
|||
: trf('목록에서 숨겼습니다 ({0}개). 파일은 그대로이고, 목록 맨 아래의 [숨긴 항목 보이기] 로 다시 볼 수 있습니다.', [targets.length]));
|
||||
case 'appUnhide':
|
||||
final keys = {for (final x in targets) HiddenList.keyOf(x)};
|
||||
await ctl.c.updateSettings((s) => s.explorerHiddenItems = [
|
||||
for (final x in s.explorerHiddenItems)
|
||||
await ctl.c.updateSettings((s) => s.explorer.hiddenItems = [
|
||||
for (final x in s.explorer.hiddenItems)
|
||||
if (!keys.contains(HiddenList.keyOf(x))) x,
|
||||
]);
|
||||
for (final q in ctl.panes) {
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class ExplorerInput {
|
|||
if (const {'delete', 'deletePermanent', 'rename', 'refresh', 'filter', 'up', 'back', 'forward', 'switchPane'}.contains(e.key))
|
||||
e.key: e.value,
|
||||
}
|
||||
: effectiveExplorerKeys(tc: _tcKeys, overrides: ctl.c.settings.explorerKeyOverrides);
|
||||
: effectiveExplorerKeys(tc: _tcKeys, overrides: ctl.c.settings.explorer.keyOverrides);
|
||||
return {
|
||||
for (final e in keys.entries)
|
||||
if (cb[e.key] != null)
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ class ExplorerOpener {
|
|||
// 폴더를 읽지 못하면 (E10: 이제 오류로 알림) 고른 그림 하나만
|
||||
List<FileEntry> list;
|
||||
try {
|
||||
list = pane?.cache[dir] ?? ctl.sorted(await listEntries(dir, showHidden: ctl.c.settings.explorerShowHidden), dir);
|
||||
list = pane?.cache[dir] ?? ctl.sorted(await listEntries(dir, showHidden: ctl.c.settings.explorer.showHidden), dir);
|
||||
} catch (_) {
|
||||
list = const [];
|
||||
}
|
||||
|
|
@ -150,7 +150,7 @@ class ExplorerOpener {
|
|||
Future<void> slideshow(BuildContext context, ExplorerPane pane, String dir) async {
|
||||
List<FileEntry> list;
|
||||
try {
|
||||
list = pane.cache[dir] ?? ctl.sorted(await listEntries(dir, showHidden: ctl.c.settings.explorerShowHidden), dir);
|
||||
list = pane.cache[dir] ?? ctl.sorted(await listEntries(dir, showHidden: ctl.c.settings.explorer.showHidden), dir);
|
||||
} catch (e) {
|
||||
ctl.host.snack(trf('폴더를 읽을 수 없습니다: {0}', [e]));
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ class ExplorerRenames {
|
|||
});
|
||||
final done = await showBatchRename(context, ordered,
|
||||
isDir: vIsDirSync,
|
||||
initial: RenameRule.fromJson(ctl.c.settings.batchRenameLast),
|
||||
onRun: (r) => ctl.c.updateSettings((s) => s.batchRenameLast = r.toJson()));
|
||||
initial: RenameRule.fromJson(ctl.c.settings.explorer.batchRenameLast),
|
||||
onRun: (r) => ctl.c.updateSettings((s) => s.explorer.batchRenameLast = r.toJson()));
|
||||
if (done == null || done.isEmpty || !context.mounted) return;
|
||||
await _after(pane, done);
|
||||
if (!context.mounted) return;
|
||||
|
|
|
|||
|
|
@ -299,7 +299,7 @@ class ExplorerRowView extends StatelessWidget {
|
|||
if (up) return Icon(Icons.arrow_upward, size: look.iconSize, color: JjColors.textDim);
|
||||
if (isRoot) return Icon(isDav(e.path) ? Icons.cloud : Icons.sd_storage, color: JjColors.accent, size: look.iconSize - 2);
|
||||
// X-plore 비교 5-1 ② (관리자 지시 10/21): 원격은 설정 (처음 값 Wi-Fi 에서만) 에 따라 받아서 - 받기 전 · 못 받으면 아이콘
|
||||
if (look.rich && isDav(e.path) && ctl.c.settings.explorerRemoteThumbs != 'off' && remoteThumbKind(e)) {
|
||||
if (look.rich && isDav(e.path) && ctl.c.settings.explorer.remoteThumbs != 'off' && remoteThumbKind(e)) {
|
||||
final (icon, color) = fileIcon(look, e, open: open);
|
||||
return RemoteThumb(
|
||||
key: ValueKey('rthumb:${e.path}'), c: ctl.c, entry: e, fallback: Icon(icon, size: 28, color: color));
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class ExplorerSearch extends ChangeNotifier {
|
|||
builder: (_) => SearchResultsPage(
|
||||
root: root,
|
||||
cond: cond,
|
||||
showHidden: ctl.c.settings.explorerShowHidden,
|
||||
showHidden: ctl.c.settings.explorer.showHidden,
|
||||
otherDir: other,
|
||||
onOpen: (e) => opener.open(context, e.path, pane: pane),
|
||||
initial: initial,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class ExplorerTransferPanel extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 85: 숨겼으면 오른쪽 아래 작은 표시 "복사 · 이동 n개 · 45%" (누르면 다시 펼침)
|
||||
if (jobs.isNotEmpty && c.settings.explorerProgressHidden) {
|
||||
if (jobs.isNotEmpty && c.settings.explorer.progressHidden) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
|
|
@ -39,7 +39,7 @@ class ExplorerTransferPanel extends StatelessWidget {
|
|||
running: run.length,
|
||||
percent: pct,
|
||||
failed: failed,
|
||||
onTap: () => c.updateSettings((x) => x.explorerProgressHidden = false));
|
||||
onTap: () => c.updateSettings((x) => x.explorer.progressHidden = false));
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
@ -69,7 +69,7 @@ class ExplorerTransferPanel extends StatelessWidget {
|
|||
height: 28,
|
||||
child: TextButton.icon(
|
||||
style: TextButton.styleFrom(visualDensity: VisualDensity.compact),
|
||||
onPressed: () => c.updateSettings((x) => x.explorerProgressHidden = true),
|
||||
onPressed: () => c.updateSettings((x) => x.explorer.progressHidden = true),
|
||||
icon: const Icon(Icons.expand_more, size: 18),
|
||||
label: Text(tr('숨기기'), style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class ExplorerViewDialogs {
|
|||
ChoiceChip(label: Text(label), selected: by == v, onSelected: (_) => set(() => by = v)),
|
||||
]),
|
||||
SwitchListTile(value: desc, title: Text(tr('거꾸로')), onChanged: (x) => set(() => desc = x)),
|
||||
if (ctl.c.settings.explorerSortPerFolder)
|
||||
if (ctl.c.settings.explorer.sortPerFolder)
|
||||
Text(trf('이 폴더에만: {0}', [ctl.displayPath(dir)]), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
]),
|
||||
actions: [
|
||||
|
|
@ -74,7 +74,7 @@ class ExplorerViewDialogs {
|
|||
}
|
||||
|
||||
Future<void> historyDialog(BuildContext context, ExplorerPane pane) async {
|
||||
final hist = ctl.c.settings.explorerHistory.where(reachable).toList();
|
||||
final hist = ctl.c.settings.explorer.history.where(reachable).toList();
|
||||
final pick = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => SimpleDialog(
|
||||
|
|
@ -97,17 +97,17 @@ class ExplorerViewDialogs {
|
|||
final s = ctl.c.settings;
|
||||
if (!twoPanes()) {
|
||||
await ctl.c.updateSettings((x) => x
|
||||
..explorerLayout = 'dual'
|
||||
..explorerOrientation = 'side');
|
||||
..explorer.layout = 'dual'
|
||||
..explorer.orientation = 'side');
|
||||
} else {
|
||||
// 지금 보이는 배치의 반대 ('화면 모양 따라' 였으면 지금 모양에서 정함)
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final nowSide = switch (s.explorerOrientation) {
|
||||
final nowSide = switch (s.explorer.orientation) {
|
||||
'side' => true,
|
||||
'stacked' => false,
|
||||
_ => size.width >= size.height,
|
||||
};
|
||||
await ctl.c.updateSettings((x) => x.explorerOrientation = nowSide ? 'stacked' : 'side');
|
||||
await ctl.c.updateSettings((x) => x.explorer.orientation = nowSide ? 'stacked' : 'side');
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
ctl.update();
|
||||
|
|
@ -118,7 +118,7 @@ class ExplorerViewDialogs {
|
|||
|
||||
Future<void> layoutDialog(BuildContext context) async {
|
||||
final s = ctl.c.settings;
|
||||
var layout = s.explorerLayout, orient = s.explorerOrientation, bar = s.explorerToolbar;
|
||||
var layout = s.explorer.layout, orient = s.explorer.orientation, bar = s.explorer.toolbar;
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
|
|
@ -181,9 +181,9 @@ class ExplorerViewDialogs {
|
|||
);
|
||||
if (ok != true) return;
|
||||
await ctl.c.updateSettings((x) => x
|
||||
..explorerLayout = layout
|
||||
..explorerOrientation = orient
|
||||
..explorerToolbar = bar);
|
||||
..explorer.layout = layout
|
||||
..explorer.orientation = orient
|
||||
..explorer.toolbar = bar);
|
||||
if (layout == 'single') {
|
||||
ctl
|
||||
..active = 0
|
||||
|
|
@ -196,7 +196,7 @@ class ExplorerViewDialogs {
|
|||
}
|
||||
|
||||
Future<void> buttonsDialog(BuildContext context) async {
|
||||
final current = ExplorerButton.fromSettings(ctl.c.settings.explorerButtons);
|
||||
final current = ExplorerButton.fromSettings(ctl.c.settings.explorer.buttons);
|
||||
final order = [...current, ...ExplorerButton.explorerValues.where((b) => !current.contains(b))];
|
||||
final on = {...current};
|
||||
final ok = await showDialog<bool>(
|
||||
|
|
@ -251,6 +251,6 @@ class ExplorerViewDialogs {
|
|||
);
|
||||
if (ok != true) return;
|
||||
final names = [for (final b in order) if (on.contains(b)) b.name];
|
||||
await ctl.c.updateSettings((x) => x.explorerButtons = names.isEmpty ? [ExplorerButton.layout.name] : names);
|
||||
await ctl.c.updateSettings((x) => x.explorer.buttons = names.isEmpty ? [ExplorerButton.layout.name] : names);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class ExplorerKeysPage extends StatelessWidget {
|
|||
static Future<void> open(BuildContext context, AppController c) =>
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => ExplorerKeysPage(c: c)));
|
||||
|
||||
bool get _tc => ExplorerStyle.of(c.settings.explorerStyle) == ExplorerStyle.totalcmd;
|
||||
bool get _tc => ExplorerStyle.of(c.settings.explorer.style) == ExplorerStyle.totalcmd;
|
||||
|
||||
Future<void> _change(BuildContext context, String id) async {
|
||||
final picked = await showDialog<SingleActivator?>(
|
||||
|
|
@ -24,7 +24,7 @@ class ExplorerKeysPage extends StatelessWidget {
|
|||
builder: (_) => _KeyCaptureDialog(c: c, action: id, tc: _tc),
|
||||
);
|
||||
if (picked == null) return;
|
||||
await c.updateSettings((x) => x.explorerKeyOverrides = {...x.explorerKeyOverrides, id: _none(picked) ? '' : keySpec(picked)});
|
||||
await c.updateSettings((x) => x.explorer.keyOverrides = {...x.explorer.keyOverrides, id: _none(picked) ? '' : keySpec(picked)});
|
||||
}
|
||||
|
||||
static bool _none(SingleActivator a) => a.trigger == LogicalKeyboardKey.abort;
|
||||
|
|
@ -33,7 +33,7 @@ class ExplorerKeysPage extends StatelessWidget {
|
|||
Widget build(BuildContext context) => ListenableBuilder(
|
||||
listenable: c,
|
||||
builder: (context, _) {
|
||||
final ov = c.settings.explorerKeyOverrides;
|
||||
final ov = c.settings.explorer.keyOverrides;
|
||||
final keys = effectiveExplorerKeys(tc: _tc, overrides: ov);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
|
@ -41,7 +41,7 @@ class ExplorerKeysPage extends StatelessWidget {
|
|||
actions: [
|
||||
if (ov.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: () => c.updateSettings((x) => x.explorerKeyOverrides = {}),
|
||||
onPressed: () => c.updateSettings((x) => x.explorer.keyOverrides = {}),
|
||||
child: Text(tr('모두 처음 값으로')),
|
||||
),
|
||||
],
|
||||
|
|
@ -51,7 +51,7 @@ class ExplorerKeysPage extends StatelessWidget {
|
|||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Text(
|
||||
trf('지금 스타일 ({0}) 의 키입니다. 바꾼 키는 모든 스타일에 쓰이고, 다른 동작과 겹치면 그 동작의 키는 없어집니다. Esc 는 바꿀 수 없습니다',
|
||||
[ExplorerStyle.of(c.settings.explorerStyle).label]),
|
||||
[ExplorerStyle.of(c.settings.explorer.style).label]),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
for (final id in explorerKeyActions)
|
||||
|
|
@ -65,7 +65,7 @@ class ExplorerKeysPage extends StatelessWidget {
|
|||
trailing: Wrap(spacing: 4, children: [
|
||||
if (ov.containsKey(id))
|
||||
TextButton(
|
||||
onPressed: () => c.updateSettings((x) => x.explorerKeyOverrides = {...x.explorerKeyOverrides}..remove(id)),
|
||||
onPressed: () => c.updateSettings((x) => x.explorer.keyOverrides = {...x.explorer.keyOverrides}..remove(id)),
|
||||
child: Text(tr('처음 값으로')),
|
||||
),
|
||||
OutlinedButton(onPressed: () => _change(context, id), child: Text(tr('바꾸기'))),
|
||||
|
|
@ -101,7 +101,7 @@ class _KeyCaptureDialogState extends State<_KeyCaptureDialog> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final k = _key;
|
||||
final clash = k == null ? null : keyConflict(widget.action, k, tc: widget.tc, overrides: widget.c.settings.explorerKeyOverrides);
|
||||
final clash = k == null ? null : keyConflict(widget.action, k, tc: widget.tc, overrides: widget.c.settings.explorer.keyOverrides);
|
||||
return AlertDialog(
|
||||
title: Text(trf('{0} - 단축키', [explorerKeyLabel(widget.action)])),
|
||||
content: Focus(
|
||||
|
|
|
|||
|
|
@ -148,8 +148,8 @@ class _ExplorerPageState extends State<ExplorerPage> implements ExplorerHost, Ex
|
|||
pane.addListener(_onPane);
|
||||
}
|
||||
_resumeReload = AppLifecycleListener(onResume: () => unawaited(_ctl.reloadOpen(localOnly: true)));
|
||||
_hiddenSeen = c.settings.explorerHiddenItems;
|
||||
_kindSeen = c.settings.explorerSortByKind;
|
||||
_hiddenSeen = c.settings.explorer.hiddenItems;
|
||||
_kindSeen = c.settings.explorer.sortByKind;
|
||||
c.addListener(_onSettings);
|
||||
_init();
|
||||
}
|
||||
|
|
@ -182,9 +182,9 @@ class _ExplorerPageState extends State<ExplorerPage> implements ExplorerHost, Ex
|
|||
_ctl.local = await widget.device.listVolumes();
|
||||
if (_ctl.local.isEmpty) _ctl.local = [(Platform.isWindows ? r'C:\' : '/', Platform.isWindows ? 'C:' : '/')];
|
||||
// X-plore 비교 e: 보관 일이 지난 휴지통 항목을 지운다 (뒤에서)
|
||||
if (_flows.deletes.appTrash case final t?) unawaited(t.purgeOlderThan(c.settings.androidTrashDays).catchError((_) => 0));
|
||||
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.explorerPaths;
|
||||
final saved = widget.rsync ? c.settings.rsyncPaths : c.settings.explorer.paths;
|
||||
// 창마다 따로 읽는다: 한 창이 WebDAV 서버를 기다려도 다른 창 (이 기기 파일) 은 바로 보이게
|
||||
// 165: 지난 폴더가 느린 네트워크 드라이브여도 화면이 멈추지 않게 (뒤에서 확인, 오래 걸리면 그냥 열어 본다 - 못 읽으면 그 창에 이유)
|
||||
final reach = await Future.wait(
|
||||
|
|
@ -238,7 +238,7 @@ class _ExplorerPageState extends State<ExplorerPage> implements ExplorerHost, Ex
|
|||
|
||||
void _onSettings() {
|
||||
if (!mounted) return;
|
||||
if (_kindSeen != null && _kindSeen != c.settings.explorerSortByKind) {
|
||||
if (_kindSeen != null && _kindSeen != c.settings.explorer.sortByKind) {
|
||||
for (final pane in _ctl.panes) {
|
||||
for (final k in pane.cache.keys.toList()) {
|
||||
pane.cache[k] = _ctl.sorted(pane.cache[k]!, k);
|
||||
|
|
@ -246,8 +246,8 @@ class _ExplorerPageState extends State<ExplorerPage> implements ExplorerHost, Ex
|
|||
pane.changed();
|
||||
}
|
||||
}
|
||||
_kindSeen = c.settings.explorerSortByKind;
|
||||
final now = c.settings.explorerHiddenItems;
|
||||
_kindSeen = c.settings.explorer.sortByKind;
|
||||
final now = c.settings.explorer.hiddenItems;
|
||||
if (identical(now, _hiddenSeen)) return;
|
||||
_hiddenSeen = now;
|
||||
setState(() {});
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import 'theme.dart';
|
|||
Future<void> showHiddenItems(BuildContext context, AppController c) => showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(builder: (ctx, set) {
|
||||
final list = c.settings.explorerHiddenItems;
|
||||
final list = c.settings.explorer.hiddenItems;
|
||||
Future<void> unhide(List<String> which) async {
|
||||
await c.updateSettings((s) => s.explorerHiddenItems = [
|
||||
for (final x in s.explorerHiddenItems)
|
||||
await c.updateSettings((s) => s.explorer.hiddenItems = [
|
||||
for (final x in s.explorer.hiddenItems)
|
||||
if (!which.any((w) => samePath(w, x))) x,
|
||||
]);
|
||||
set(() {});
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Future<RemoteThumbs> remoteThumbsOf(AppController c) => _hub ??= () async {
|
|||
final dir = Directory(p.join(await c.services.storage.tempDirectory(), 'jj_rthumbs'));
|
||||
final h = RemoteThumbs(
|
||||
dir: dir,
|
||||
maxBytes: () => c.settings.explorerRemoteThumbCacheMb * 1024 * 1024,
|
||||
maxBytes: () => c.settings.explorer.remoteThumbCacheMb * 1024 * 1024,
|
||||
allowed: () => remoteThumbsAllowedNow(c),
|
||||
make: (path, out) => _make(c, path, out),
|
||||
);
|
||||
|
|
@ -75,7 +75,7 @@ bool _metered = true;
|
|||
|
||||
/// 지금 받아도 되는지: 'off' 아니오 · 'always' 예 · 'wifi' (처음 값) 는 데이터 요금제가 아닌 망에서만 (10초 동안 기억)
|
||||
Future<bool> remoteThumbsAllowedNow(AppController c) async {
|
||||
switch (c.settings.explorerRemoteThumbs) {
|
||||
switch (c.settings.explorer.remoteThumbs) {
|
||||
case 'off':
|
||||
return false;
|
||||
case 'always':
|
||||
|
|
|
|||
|
|
@ -56,13 +56,13 @@ class _FilesSettingsSectionState extends State<FilesSettingsSection> {
|
|||
subtitle: Text(tr('길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴 (복사 · 이동 · 삭제 · 이름 변경 …). '
|
||||
'Ctrl + 클릭: 여러 개 고르기')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.explorerClick,
|
||||
value: s.explorer.click,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'select', child: Text(desk ? tr('한 번: 선택 · 두 번: 실행 (기본)') : tr('한 번: 선택 · 두 번: 실행'))),
|
||||
DropdownMenuItem(value: 'open', child: Text(desk ? tr('한 번: 바로 실행') : tr('한 번: 바로 실행 (기본)'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerClick = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.click = v!),
|
||||
),
|
||||
),
|
||||
// 82: 창 사이 끌어 놓기 (처음: 묻기)
|
||||
|
|
@ -70,13 +70,13 @@ class _FilesSettingsSectionState extends State<FilesSettingsSection> {
|
|||
title: Text(tr('끌어 놓기 기본')),
|
||||
subtitle: Text(tr('다른 창으로 끌어 놓았을 때. 어느 쪽이든 복사 · 이동 확인 창 (같은 이름 등) 은 그대로 뜹니다')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.explorerDropAction,
|
||||
value: s.explorer.dropAction,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'ask', child: Text(tr('묻기 (기본)'))),
|
||||
DropdownMenuItem(value: 'copy', child: Text(tr('복사'))),
|
||||
DropdownMenuItem(value: 'move', child: Text(tr('이동'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerDropAction = v ?? 'ask'),
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.dropAction = v ?? 'ask'),
|
||||
),
|
||||
),
|
||||
// 84: 정렬을 폴더마다 기억 (처음: 끔)
|
||||
|
|
@ -84,8 +84,8 @@ class _FilesSettingsSectionState extends State<FilesSettingsSection> {
|
|||
title: Text(tr('정렬을 폴더마다 기억')),
|
||||
subtitle: Text(tr('켜면 열 머리 · 정렬 기준에서 바꾼 정렬이 그 폴더에만 쓰입니다. 끄면 모든 폴더가 같은 정렬입니다')),
|
||||
trailing: Switch(
|
||||
value: s.explorerSortPerFolder,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerSortPerFolder = v),
|
||||
value: s.explorer.sortPerFolder,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.sortPerFolder = v),
|
||||
),
|
||||
),
|
||||
// X-plore 비교 5-2 (사용자 결정 10/21): 시스템 폴더 보기 (Shizuku, 읽기 전용) - Android, 켤 때마다 경고
|
||||
|
|
@ -133,8 +133,8 @@ class _FilesSettingsSectionState extends State<FilesSettingsSection> {
|
|||
title: Text(tr('종류별 기본 정렬')),
|
||||
subtitle: Text(tr('사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름으로. 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억합니다')),
|
||||
trailing: Switch(
|
||||
value: s.explorerSortByKind,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerSortByKind = v),
|
||||
value: s.explorer.sortByKind,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.sortByKind = v),
|
||||
),
|
||||
),
|
||||
// X-plore 비교 5-1 ② (관리자 지시 10/21): 원격 썸네일 · 캐시
|
||||
|
|
@ -142,25 +142,25 @@ class _FilesSettingsSectionState extends State<FilesSettingsSection> {
|
|||
title: Text(tr('원격 썸네일')),
|
||||
subtitle: Text(tr('WebDAV · SFTP · SMB · FTP 의 그림 · 동영상을 X-plore 모양 목록에서 작은 그림으로. 보이는 줄만, 동시에 2개까지 받습니다')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.explorerRemoteThumbs,
|
||||
value: s.explorer.remoteThumbs,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'off', child: Text(tr('받지 않음'))),
|
||||
DropdownMenuItem(value: 'wifi', child: Text(tr('Wi-Fi 에서만 (기본)'))),
|
||||
DropdownMenuItem(value: 'always', child: Text(tr('늘 (모바일 데이터에서도)'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerRemoteThumbs = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.remoteThumbs = v!),
|
||||
),
|
||||
),
|
||||
SettingTile(
|
||||
title: Text(tr('원격 썸네일 캐시')),
|
||||
subtitle: Text(tr('넘으면 오래 보지 않은 것부터 지웁니다')),
|
||||
trailing: DropdownButton<int>(
|
||||
value: s.explorerRemoteThumbCacheMb,
|
||||
value: s.explorer.remoteThumbCacheMb,
|
||||
items: [
|
||||
for (final v in {...AppSettings.remoteThumbCacheChoices, s.explorerRemoteThumbCacheMb}.toList()..sort())
|
||||
for (final v in {...ExplorerSettings.remoteThumbCacheChoices, s.explorer.remoteThumbCacheMb}.toList()..sort())
|
||||
DropdownMenuItem(value: v, child: Text(v == 200 ? trf('{0}MB (기본)', [v]) : '${v}MB')),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerRemoteThumbCacheMb = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.remoteThumbCacheMb = v!),
|
||||
),
|
||||
),
|
||||
RemoteThumbCacheTile(c: c),
|
||||
|
|
@ -170,22 +170,22 @@ class _FilesSettingsSectionState extends State<FilesSettingsSection> {
|
|||
subtitle: Text(tr('폴더 안 파일 크기의 합을 목록에 보입니다. 큰 폴더 · NAS 는 읽는 데 오래 걸릴 수 있습니다. '
|
||||
'길게 누르기 [폴더 크기 계산] 은 늘 됩니다')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.explorerFolderSize,
|
||||
value: s.explorer.folderSize,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'sort', child: Text(tr('크기순 정렬일 때만 (기본)'))),
|
||||
DropdownMenuItem(value: 'always', child: Text(tr('늘 계산'))),
|
||||
DropdownMenuItem(value: 'off', child: Text(tr('계산 안 함'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerFolderSize = v ?? 'sort'),
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.folderSize = v ?? 'sort'),
|
||||
),
|
||||
),
|
||||
// X-plore 비교 5-1 ⑥ (관리자 지시 10/21): 단축키 바꾸기 (키보드가 있는 PC 에서만 보임)
|
||||
if (desk)
|
||||
ListTile(
|
||||
title: Text(tr('파일 탐색기 단축키')),
|
||||
subtitle: Text(s.explorerKeyOverrides.isEmpty
|
||||
subtitle: Text(s.explorer.keyOverrides.isEmpty
|
||||
? tr('스타일대로 (바꾼 것 없음)')
|
||||
: trf('바꾼 것 {0}개', [s.explorerKeyOverrides.length])),
|
||||
: trf('바꾼 것 {0}개', [s.explorer.keyOverrides.length])),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => ExplorerKeysPage.open(context, c),
|
||||
),
|
||||
|
|
@ -200,26 +200,26 @@ class _FilesSettingsSectionState extends State<FilesSettingsSection> {
|
|||
'그 밖: Ctrl+C · X · V (복사 · 잘라내기 · 붙여넣기) · Ctrl+A 모두 표시 · F5 새로 고침'),
|
||||
].join('\n')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.explorerStyle,
|
||||
value: s.explorer.style,
|
||||
items: [
|
||||
for (final st in ExplorerStyle.values)
|
||||
DropdownMenuItem(value: st.name, child: Text(tr(st.label))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerStyle = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.style = v!),
|
||||
),
|
||||
),
|
||||
SettingTile(
|
||||
title: Text(tr('창 배치')),
|
||||
subtitle: Text(tr('파일 탐색기 위쪽 ⋮ 메뉴에서도 바꿀 수 있습니다 (좌우 · 위아래 · 버튼 줄 위치 · 버튼 구성)')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.explorerLayout,
|
||||
value: s.explorer.layout,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'auto', child: Text(tr('화면 크기 따라'))),
|
||||
DropdownMenuItem(value: 'dual', child: Text(tr('두 창 (기본)'))),
|
||||
DropdownMenuItem(value: 'split', child: Text(tr('폴더 + 파일 목록'))),
|
||||
DropdownMenuItem(value: 'single', child: Text(tr('한 창'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerLayout = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.layout = v!),
|
||||
),
|
||||
),
|
||||
// X-plore 비교 e: Android 앱 휴지통 보관 일
|
||||
|
|
@ -228,95 +228,95 @@ class _FilesSettingsSectionState extends State<FilesSettingsSection> {
|
|||
title: Text(tr('휴지통 보관')),
|
||||
subtitle: Text(tr('지운 것을 저장 장치의 앱 휴지통에 두었다가 이 날이 지나면 지웁니다. 끄면 바로 영구 삭제')),
|
||||
trailing: DropdownButton<int>(
|
||||
value: const [0, 3, 7, 14, 30, 60].contains(s.androidTrashDays) ? s.androidTrashDays : 14,
|
||||
value: const [0, 3, 7, 14, 30, 60].contains(s.explorer.androidTrashDays) ? s.explorer.androidTrashDays : 14,
|
||||
items: [
|
||||
for (final d in const [0, 3, 7, 14, 30, 60])
|
||||
DropdownMenuItem(value: d, child: Text(d == 0 ? tr('끔') : trf('{0}일', [d]))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.androidTrashDays = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.androidTrashDays = v!),
|
||||
),
|
||||
),
|
||||
// X-plore 비교 ⚖ (관리자 결정): X-plore 스타일의 표시 동그라미 · 밀기 · 폴더 표시 규칙
|
||||
SwitchListTile(
|
||||
title: Text(tr('X-plore 스타일: 선택 표시 늘 보이기')),
|
||||
subtitle: Text(tr('끄면 (처음 값) [선택] 을 켰을 때만 보입니다. 다른 스타일은 늘 선택 모드에서만')),
|
||||
value: s.explorerAlwaysMarks,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerAlwaysMarks = v),
|
||||
value: s.explorer.alwaysMarks,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.alwaysMarks = v),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(tr('X-plore 스타일: 옆으로 밀면 창 바꾸기')),
|
||||
subtitle: Text(tr('탐색기 안에서 밀면 다른 창으로. 다른 화면으로 가려면 위 막대에서 밉니다')),
|
||||
value: s.explorerSwipePanes,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerSwipePanes = v),
|
||||
value: s.explorer.swipePanes,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.swipePanes = v),
|
||||
),
|
||||
// X-plore 비교 c: 저장 장치 · 서버 · 즐겨찾기를 트리 맨 위에 (X-plore 스타일)
|
||||
SwitchListTile(
|
||||
title: Text(tr('저장 장치 · 서버 · 즐겨찾기를 트리 맨 위에')),
|
||||
subtitle: Text(tr('X-plore 스타일에서 X-plore 처럼 한 트리에 보입니다 (위 줄 탭 · ★ 는 그대로)')),
|
||||
value: s.explorerRootsInTree,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerRootsInTree = v),
|
||||
value: s.explorer.rootsInTree,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.rootsInTree = v),
|
||||
),
|
||||
// X-plore 비교 d: 지금 폴더를 다시 누르면 접기
|
||||
SwitchListTile(
|
||||
title: Text(tr('펼친 폴더를 다시 누르면 접기')),
|
||||
subtitle: Text(tr('처음 값 켬. 끄면 접기 · 펼치기는 › 화살표로만 (두 번 눌러도 목록이 움직이지 않음)')),
|
||||
value: s.explorerRetapCollapse,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerRetapCollapse = v),
|
||||
value: s.explorer.retapCollapse,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.retapCollapse = v),
|
||||
),
|
||||
// X-plore 비교 40 (관리자 지시 10/20): 목록만의 글자 크기 · 줄 간격 (세 스타일 모두, 앱 화면 크기 · 기기 글자 크기와 곱함)
|
||||
SettingTile(
|
||||
title: Text(tr('목록 글자 크기')),
|
||||
subtitle: Text(tr('파일 탐색기 목록의 이름 · 크기 글자만. 앱 화면 크기 · 기기 글자 크기와 곱해집니다')),
|
||||
trailing: DropdownButton<int>(
|
||||
value: s.explorerTextScale,
|
||||
value: s.explorer.textScale,
|
||||
items: [
|
||||
for (var v = AppSettings.explorerTextScaleMin; v <= AppSettings.explorerTextScaleMax; v += 10)
|
||||
for (var v = ExplorerSettings.textScaleMin; v <= ExplorerSettings.textScaleMax; v += 10)
|
||||
DropdownMenuItem(value: v, child: Text(v == 100 ? trf('{0}% (기본)', [v]) : '$v%')),
|
||||
if (s.explorerTextScale % 10 != 0)
|
||||
DropdownMenuItem(value: s.explorerTextScale, child: Text('${s.explorerTextScale}%')),
|
||||
if (s.explorer.textScale % 10 != 0)
|
||||
DropdownMenuItem(value: s.explorer.textScale, child: Text('${s.explorer.textScale}%')),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerTextScale = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.textScale = v!),
|
||||
),
|
||||
),
|
||||
SettingTile(
|
||||
title: Text(tr('줄 간격')),
|
||||
subtitle: Text(tr('파일 탐색기 목록의 줄 높이. 좁게는 손가락으로 누르기 어려울 수 있습니다')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.explorerRowSpacing,
|
||||
value: s.explorer.rowSpacing,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'compact', child: Text(tr('좁게'))),
|
||||
DropdownMenuItem(value: 'normal', child: Text(tr('보통 (기본)'))),
|
||||
DropdownMenuItem(value: 'wide', child: Text(tr('넓게'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerRowSpacing = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.rowSpacing = v!),
|
||||
),
|
||||
),
|
||||
// X-plore 숨기기: 목록에서 숨긴 것 보기 · 풀기 (관리자 결정 10/18)
|
||||
ListTile(
|
||||
title: Text(trf('목록에서 숨긴 항목 ({0}개)', [s.explorerHiddenItems.length])),
|
||||
title: Text(trf('목록에서 숨긴 항목 ({0}개)', [s.explorer.hiddenItems.length])),
|
||||
subtitle: Text(tr('파일 탐색기의 길게 누르기 [목록에서 숨기기] 로 숨긴 것. 파일은 그대로이고 목록에서만 빠집니다')),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
enabled: s.explorerHiddenItems.isNotEmpty,
|
||||
enabled: s.explorer.hiddenItems.isNotEmpty,
|
||||
onTap: () => showHiddenItems(context, c),
|
||||
),
|
||||
// X-plore 맞추기 (관리자 지시 10/20): 숨기기 전 확인 창
|
||||
SwitchListTile(
|
||||
title: Text(tr('목록에서 숨기기 전에 묻기')),
|
||||
subtitle: Text(tr('무엇이 바뀌는지 (목록에서만 빠지고 파일은 그대로) 확인 창을 띄웁니다')),
|
||||
value: s.explorerHideConfirm,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerHideConfirm = v),
|
||||
value: s.explorer.hideConfirm,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.hideConfirm = v),
|
||||
),
|
||||
// X-plore 비교 a: 한 창일 때 복사 · 이동이 갈 곳
|
||||
SettingTile(
|
||||
title: Text(tr('한 창일 때 복사 · 이동')),
|
||||
subtitle: Text(tr('숨은 다른 창으로: X-plore 처럼 위에 "갈 곳" 을 늘 보이고 그 폴더로 바로 복사 · 이동합니다')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.explorerOnePaneCopy,
|
||||
value: s.explorer.onePaneCopy,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'clip', child: Text(tr('담아 두고 붙여넣기 (기본)'))),
|
||||
DropdownMenuItem(value: 'other', child: Text(tr('숨은 다른 창으로'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerOnePaneCopy = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorer.onePaneCopy = v!),
|
||||
),
|
||||
),
|
||||
// 복사 · 이동 방법 (현재 방식 · robocopy) · 속도 제한
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ void main() {
|
|||
File(p.join(tmp.path, 'a.txt')).writeAsStringSync('a');
|
||||
Directory(p.join(tmp.path, 'sub')).createSync();
|
||||
c.settings
|
||||
..explorerPaths = [tmp.path, tmp.path]
|
||||
..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'))];
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ 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.explorerLayout, s.zipComic], ['auto', 30, 'dual', false]);
|
||||
expect([s.liveSyncOnStart, s.liveSyncIntervalSec, s.explorer.layout, s.zipComic], ['auto', 30, 'dual', false]);
|
||||
expect([AppSettings.uiScaleMin, AppSettings.uiScaleMax], [0.5, 2.5]);
|
||||
// 바꾼 값은 남는다
|
||||
final b = AppSettings.fromJson((AppSettings()
|
||||
|
|
@ -145,6 +145,6 @@ void main() {
|
|||
// 강제 종료 뒤 다시 켬 = 파일을 새로 읽음
|
||||
final again = await tester.runAsync(() => SettingsStore(file, MemorySecretStore()).load());
|
||||
expect(again!.migrated, isEmpty);
|
||||
expect(again.explorerOrientation, 'side');
|
||||
expect(again.explorer.orientation, 'side');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,8 +126,8 @@ void main() {
|
|||
..createSync(recursive: true)
|
||||
..writeAsStringSync('d');
|
||||
c.settings
|
||||
..explorerLayout = 'single'
|
||||
..explorerPaths = [dir, dir];
|
||||
..explorer.layout = 'single'
|
||||
..explorer.paths = [dir, dir];
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -366,7 +366,7 @@ void main() {
|
|||
final center = CopyCenter.of(c);
|
||||
c.settings
|
||||
..rsyncOptions = '-avPog'
|
||||
..explorerLayout = 'single'
|
||||
..explorer.layout = 'single'
|
||||
..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')));
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ void main() {
|
|||
device.trash = true;
|
||||
device.volumes = () => [(tmp.path, 'T:')];
|
||||
// [휴지통] 버튼이 줄에 보이게 (처음 구성은 좁으면 [더 보기] 로 간다)
|
||||
c.settings.explorerButtons = ['trash', for (final b in ExplorerButton.defaultOrder) if (b != ExplorerButton.trash) b.name];
|
||||
c.settings.explorer.buttons = ['trash', for (final b in ExplorerButton.defaultOrder) if (b != ExplorerButton.trash) b.name];
|
||||
addTearDown(() {
|
||||
});
|
||||
await open(tester);
|
||||
|
|
@ -180,7 +180,7 @@ void main() {
|
|||
}, skip: !Platform.isWindows);
|
||||
|
||||
testWidgets('바로 실행 모드: 폴더를 누르면 펼치고, 파일을 누르면 바로 실행', (tester) async {
|
||||
c.settings.explorerClick = 'open';
|
||||
c.settings.explorer.click = 'open';
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('sub')));
|
||||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ void main() {
|
|||
host.show(() => ExplorerTransferPanel(c: ctl.c, jobs: [job], rsync: false), ctl.c);
|
||||
await t.pump();
|
||||
expect(find.textContaining('완료'), findsOneWidget);
|
||||
await t.runAsync(() => ctl.c.updateSettings((s) => s.explorerProgressHidden = true));
|
||||
await t.runAsync(() => ctl.c.updateSettings((s) => s.explorer.progressHidden = true));
|
||||
await t.pump();
|
||||
expect([find.byType(TransferProgressChip).evaluate().length, find.textContaining('완료').evaluate().length], [1, 0]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ void main() {
|
|||
File(p.join(left, 'doc.txt')).setLastModifiedSync(DateTime(2026, 10, 9, 12));
|
||||
File(p.join(right, 'doc.txt')).setLastModifiedSync(DateTime(2026, 10, 1, 8));
|
||||
c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings.explorerPaths = [left, right];
|
||||
c.settings.explorer.paths = [left, right];
|
||||
final root = p.rootPrefix(tmp.path);
|
||||
device.volumes = () => [(root, root.replaceAll(RegExp(r'[\\/]+$'), ''))];
|
||||
// 휴지통: 기본은 가짜 (흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통
|
||||
|
|
@ -294,7 +294,7 @@ void main() {
|
|||
const locked = r'C:\System Volume Information';
|
||||
if (!Platform.isWindows || !Directory(locked).existsSync()) return;
|
||||
if (skipIfReadable(locked)) return;
|
||||
c.settings.explorerPaths = [locked, right];
|
||||
c.settings.explorer.paths = [locked, right];
|
||||
device.volumes = () => [(r'C:\', 'C:'), (p.rootPrefix(tmp.path), p.rootPrefix(tmp.path).substring(0, 2))];
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -306,7 +306,7 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('135: 두 창이 같은 폴더 - [이동] 은 "이미 이 폴더에 있습니다" 창 · [복사] 는 사본을 만들지 묻는다', (tester) async {
|
||||
c.settings.explorerPaths = [left, left];
|
||||
c.settings.explorer.paths = [left, left];
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -343,7 +343,7 @@ void main() {
|
|||
testWidgets('135 보충: 섞어 고른 [이동] - 이미 그 폴더에 있는 것만 건너뛰고 나머지는 옮긴다 (확인 창 · 끝난 알림에 적음)', (tester) async {
|
||||
File(p.join(left, 'a.txt')).writeAsStringSync('a');
|
||||
File(p.join(right, 'b.txt')).writeAsStringSync('b');
|
||||
c.settings.explorerPaths = [tmp.path, right]; // 왼쪽 창은 left · right 를 함께 보는 위 폴더
|
||||
c.settings.explorer.paths = [tmp.path, right]; // 왼쪽 창은 left · right 를 함께 보는 위 폴더
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -375,7 +375,7 @@ void main() {
|
|||
tester.view.physicalSize = const Size(1200, 560);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final before = c.settings.explorerShowHidden;
|
||||
final before = c.settings.explorer.showHidden;
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(device: device, c: c))));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||
expect(tester.takeException(), isNull, reason: '넘침 없음');
|
||||
|
|
@ -385,8 +385,8 @@ void main() {
|
|||
await calm(tester);
|
||||
expect(find.text('숨은 항목 표시'), findsOneWidget);
|
||||
await tester.runAsync(() => tester.tap(find.text('숨은 항목 표시')));
|
||||
await settle(tester, () => c.settings.explorerShowHidden != before, rounds: 20);
|
||||
expect(c.settings.explorerShowHidden, !before);
|
||||
await settle(tester, () => c.settings.explorer.showHidden != before, rounds: 20);
|
||||
expect(c.settings.explorer.showHidden, !before);
|
||||
// 창 배치 버튼은 좁아도 늘 줄에 (사용자 요청) · 처음 값은 자주 쓰는 것부터 (복사 · 이동 · 삭제 …)
|
||||
expect(find.byTooltip('창 배치: 좌우 (누르면 위아래)'), findsOneWidget);
|
||||
expect(ExplorerButton.fromSettings(const []).take(9).map((b) => b.name),
|
||||
|
|
@ -499,7 +499,7 @@ void main() {
|
|||
|
||||
testWidgets('37-2: 보던 폴더가 밖에서 지워지면 새로 읽을 때 남아 있는 상위 폴더로 옮기고 알린다', (tester) async {
|
||||
final deep = Directory(p.join(left, 'a', 'b'))..createSync(recursive: true);
|
||||
c.settings.explorerPaths = [deep.path, right];
|
||||
c.settings.explorer.paths = [deep.path, right];
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ void main() {
|
|||
expect(q.expanded, containsAll([a, show]));
|
||||
expect([for (final e in q.cache[show]!) p.basename(e.path)], ['ep1.mkv']);
|
||||
expect(host.revealed, [show], reason: 'Host 에 스크롤 부탁');
|
||||
expect(ctl.c.settings.explorerPaths.first, show, reason: '마지막 폴더 기억');
|
||||
expect(ctl.c.settings.explorer.paths.first, show, reason: '마지막 폴더 기억');
|
||||
await t.runAsync(() => ctl.goTo(q, b));
|
||||
expect(q.root, b, reason: '다른 장치');
|
||||
await t.runAsync(() => ctl.travel(q, forward: false));
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ void main() {
|
|||
..writeAsStringSync('x');
|
||||
Directory(p.join(tmp.path, 'right')).createSync();
|
||||
c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings.explorerPaths = [p.join(tmp.path, 'left'), p.join(tmp.path, 'right')];
|
||||
c.settings.explorer.paths = [p.join(tmp.path, 'left'), p.join(tmp.path, 'right')];
|
||||
stuck = Completer<bool>();
|
||||
dead = {r'R:\'};
|
||||
// T: 시험 폴더 (정상) · Q: 응답 없는 네트워크 드라이브 · R: 연결 안 된 드라이브
|
||||
|
|
@ -80,8 +80,8 @@ void main() {
|
|||
device.volumes = () => [...empties, (tmp.path, 'T:')];
|
||||
device.probe = (path) => Future.value(!dead.contains(path));
|
||||
c.settings
|
||||
..explorerLayout = 'dual'
|
||||
..explorerOrientation = 'side';
|
||||
..explorer.layout = 'dual'
|
||||
..explorer.orientation = 'side';
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
|
|||
|
|
@ -173,12 +173,12 @@ void main() {
|
|||
await tester.pump();
|
||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||||
await calm(tester);
|
||||
expect([c.settings.explorerLayout, c.settings.explorerToolbar], ['single', 'hidden']);
|
||||
expect([c.settings.explorer.layout, c.settings.explorer.toolbar], ['single', 'hidden']);
|
||||
expect(find.text('상위 폴더'), findsNothing); // 버튼 줄 숨김
|
||||
expect(find.text('doc.txt'), findsOneWidget); // 한 창 (왼쪽만)
|
||||
|
||||
// ⋮ 메뉴에서 버튼 구성: "찾기" 빼기
|
||||
await c.updateSettings((s) => s.explorerToolbar = 'middle');
|
||||
await c.updateSettings((s) => s.explorer.toolbar = 'middle');
|
||||
await calm(tester);
|
||||
await tester.tap(find.byTooltip('창 배치 · 버튼 구성'));
|
||||
await calm(tester);
|
||||
|
|
@ -191,14 +191,14 @@ void main() {
|
|||
await tester.pump();
|
||||
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
||||
await calm(tester);
|
||||
expect(c.settings.explorerButtons, isNot(contains('addMkv')));
|
||||
expect(c.settings.explorerButtons, contains('copy'));
|
||||
expect(c.settings.explorer.buttons, isNot(contains('addMkv')));
|
||||
expect(c.settings.explorer.buttons, contains('copy'));
|
||||
expect(find.text('MKV 목록에 추가'), findsNothing);
|
||||
|
||||
// 설정 저장 · 읽기
|
||||
final back = AppSettings.fromJson(c.settings.toJson());
|
||||
expect([back.explorerLayout, back.explorerToolbar, back.explorerButtons],
|
||||
[c.settings.explorerLayout, c.settings.explorerToolbar, c.settings.explorerButtons]);
|
||||
expect([back.explorer.layout, back.explorer.toolbar, back.explorer.buttons],
|
||||
[c.settings.explorer.layout, c.settings.explorer.toolbar, c.settings.explorer.buttons]);
|
||||
});
|
||||
|
||||
testWidgets('Rsync 화면: 늘 좌우 두 창 · 폴더만 · 창마다 하나만 고르기 · → ← ⇄ · 모니터링', (tester) async {
|
||||
|
|
@ -207,7 +207,7 @@ void main() {
|
|||
File(p.join(right, 'r.txt')).writeAsStringSync('r');
|
||||
File(p.join(right, 'rsub', 'back.txt')).writeAsStringSync('b'); // ⇄ 비교에서 ← 로 건너갈 것
|
||||
c.settings
|
||||
..explorerLayout = 'single' // 파일 탐색기 배치와 상관없이 두 창
|
||||
..explorer.layout = 'single' // 파일 탐색기 배치와 상관없이 두 창
|
||||
..rsyncPaths = [left, right];
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ void main() {
|
|||
|
||||
testWidgets('X-plore 숨기기: 길게 누르기 [목록에서 숨기기] → 목록에서만 빠짐 (파일 그대로) · 맨 아래 [숨긴 항목 보이기] · [숨기기 풀기]', (tester) async {
|
||||
// 폴더 + 파일 목록 배치: 오른쪽 목록은 그 폴더의 것만이라 짧다 (트리는 Temp 폴더 줄이 많아 맨 아래가 멀다)
|
||||
c.settings.explorerLayout = 'split';
|
||||
c.settings.explorer.layout = 'split';
|
||||
await open(tester);
|
||||
final doc = p.join(left, 'doc.txt');
|
||||
await tester.longPress(find.text('doc.txt'));
|
||||
|
|
@ -30,7 +30,7 @@ void main() {
|
|||
await settle(tester, () => find.text('doc.txt').evaluate().isEmpty);
|
||||
expect(find.text('doc.txt'), findsNothing);
|
||||
expect(File(doc).existsSync(), isTrue, reason: '파일은 바꾸지 않는다');
|
||||
expect(c.settings.explorerHiddenItems, [doc]);
|
||||
expect(c.settings.explorer.hiddenItems, [doc]);
|
||||
// 목록 맨 아래에 [숨긴 항목 1개 보이기]
|
||||
expect(find.text('숨긴 항목 1개 보이기'), findsOneWidget);
|
||||
await idle(tester);
|
||||
|
|
@ -44,14 +44,14 @@ void main() {
|
|||
expect(find.text('목록에서 숨기기'), findsNothing);
|
||||
await act(tester, () => tester.tap(find.text('숨기기 풀기')));
|
||||
await settle(tester, () => find.textContaining('숨긴 항목').evaluate().isEmpty);
|
||||
expect(c.settings.explorerHiddenItems, isEmpty);
|
||||
expect(c.settings.explorer.hiddenItems, isEmpty);
|
||||
expect(find.text('doc.txt'), findsOneWidget);
|
||||
expect(find.ancestor(of: find.text('doc.txt'), matching: find.byType(Opacity)), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('X-plore 숨기기 확인 창: [취소] 면 그대로 · [다음부터 묻지 않기] 를 고르면 다음엔 묻지 않고 바로 숨김 (설정 저장)', (tester) async {
|
||||
c.settings.explorerLayout = 'split';
|
||||
expect(AppSettings().explorerHideConfirm, isTrue, reason: '처음은 묻기');
|
||||
c.settings.explorer.layout = 'split';
|
||||
expect(AppSettings().explorer.hideConfirm, isTrue, reason: '처음은 묻기');
|
||||
File(p.join(left, 'two.txt')).writeAsStringSync('2');
|
||||
await open(tester);
|
||||
await tester.longPress(find.text('doc.txt'));
|
||||
|
|
@ -60,7 +60,7 @@ void main() {
|
|||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('취소')));
|
||||
await calm(tester);
|
||||
expect(c.settings.explorerHiddenItems, isEmpty, reason: '취소하면 숨기지 않는다');
|
||||
expect(c.settings.explorer.hiddenItems, isEmpty, reason: '취소하면 숨기지 않는다');
|
||||
expect(find.text('doc.txt'), findsOneWidget);
|
||||
// 다음부터 묻지 않기 + 숨기기
|
||||
await tester.longPress(find.text('doc.txt'));
|
||||
|
|
@ -70,16 +70,16 @@ void main() {
|
|||
await act(tester, () => tester.tap(find.text('다음부터 묻지 않기')));
|
||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '숨기기')));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isEmpty);
|
||||
expect(c.settings.explorerHideConfirm, isFalse);
|
||||
expect(AppSettings.fromJson(c.settings.toJson()).explorerHideConfirm, isFalse);
|
||||
expect(AppSettings.fromJson({}).explorerHideConfirm, isTrue, reason: '예전 설정 (값 없음) 은 묻기');
|
||||
expect(c.settings.explorer.hideConfirm, isFalse);
|
||||
expect(AppSettings.fromJson(c.settings.toJson()).explorer.hideConfirm, isFalse);
|
||||
expect(AppSettings.fromJson({}).explorer.hideConfirm, isTrue, reason: '예전 설정 (값 없음) 은 묻기');
|
||||
// 이제는 창 없이 바로
|
||||
await tester.longPress(find.text('two.txt'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('목록에서 숨기기')));
|
||||
await settle(tester, () => find.text('two.txt').evaluate().isEmpty);
|
||||
expect(find.textContaining('파일 탐색기 목록에서만 빠집니다'), findsNothing);
|
||||
expect(c.settings.explorerHiddenItems, [p.join(left, 'doc.txt'), p.join(left, 'two.txt')]);
|
||||
expect(c.settings.explorer.hiddenItems, [p.join(left, 'doc.txt'), p.join(left, 'two.txt')]);
|
||||
});
|
||||
|
||||
testWidgets('X-plore 맞추기: 폴더 길게 누르기 [갤러리 · 음악 앱에서 숨기기 (.nomedia)] - 확인 창 뒤 빈 .nomedia, 다시 누르면 지우기 · 안의 파일은 그대로', (tester) async {
|
||||
|
|
@ -129,7 +129,7 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('X-plore 숨기기: 숨긴 것이 든 폴더를 지울 때 확인 창에 알림 (숨긴 것도 함께 지워진다)', (tester) async {
|
||||
c.settings.explorerHiddenItems = [p.join(left, 'sub', 'inner.txt')];
|
||||
c.settings.explorer.hiddenItems = [p.join(left, 'sub', 'inner.txt')];
|
||||
await open(tester);
|
||||
await tester.longPress(find.text('sub'));
|
||||
await calm(tester);
|
||||
|
|
@ -150,7 +150,7 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('X-plore 숨기기: 이름을 바꾸면 숨긴 목록도 새 경로로 · 영구로 지우면 목록에서 뺀다 (관리자 결정)', (tester) async {
|
||||
c.settings.explorerHiddenItems = [p.join(left, 'sub', 'inner.txt')];
|
||||
c.settings.explorer.hiddenItems = [p.join(left, 'sub', 'inner.txt')];
|
||||
await open(tester);
|
||||
await tester.longPress(find.text('sub'));
|
||||
await calm(tester);
|
||||
|
|
@ -159,7 +159,7 @@ void main() {
|
|||
await act(tester, () => tester.enterText(find.byType(TextField), 'sub2'));
|
||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||||
await settle(tester, () => find.text('sub2').evaluate().isNotEmpty);
|
||||
expect(c.settings.explorerHiddenItems, [p.join(left, 'sub2', 'inner.txt')]);
|
||||
expect(c.settings.explorer.hiddenItems, [p.join(left, 'sub2', 'inner.txt')]);
|
||||
await idle(tester);
|
||||
await tester.longPress(find.text('sub2'));
|
||||
await calm(tester);
|
||||
|
|
@ -169,29 +169,29 @@ void main() {
|
|||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '영구 삭제')));
|
||||
await settle(tester, () => find.text('sub2').evaluate().isEmpty);
|
||||
await settle(tester, () => c.settings.explorerHiddenItems.isEmpty);
|
||||
expect(c.settings.explorerHiddenItems, isEmpty);
|
||||
await settle(tester, () => c.settings.explorer.hiddenItems.isEmpty);
|
||||
expect(c.settings.explorer.hiddenItems, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('X-plore 숨기기: 다른 창으로 옮기면 숨긴 목록도 새 자리로 (그 안의 것도)', (tester) async {
|
||||
c.settings.explorerHiddenItems = [p.join(left, 'sub', 'inner.txt')];
|
||||
c.settings.explorer.hiddenItems = [p.join(left, 'sub', 'inner.txt')];
|
||||
await open(tester);
|
||||
await tester.longPress(find.text('sub'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.byTooltip('다른 창으로 이동').last));
|
||||
await settle(tester, () => find.widgetWithText(FilledButton, '이동').evaluate().isNotEmpty);
|
||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '이동')));
|
||||
await settle(tester, () => c.settings.explorerHiddenItems.first != p.join(left, 'sub', 'inner.txt'));
|
||||
await settle(tester, () => c.settings.explorer.hiddenItems.first != p.join(left, 'sub', 'inner.txt'));
|
||||
expect(File(p.join(right, 'sub', 'inner.txt')).existsSync(), isTrue);
|
||||
expect(c.settings.explorerHiddenItems, [p.join(right, 'sub', 'inner.txt')]);
|
||||
expect(c.settings.explorer.hiddenItems, [p.join(right, 'sub', 'inner.txt')]);
|
||||
await idle(tester);
|
||||
});
|
||||
|
||||
testWidgets('X-plore 숨기기: 위 막대 ⋮ [목록에서 숨긴 항목 보이기] · 환경 설정의 [목록에서 숨긴 항목] 에서 풀기', (tester) async {
|
||||
final doc = p.join(left, 'doc.txt');
|
||||
c.settings
|
||||
..explorerLayout = 'split'
|
||||
..explorerHiddenItems = [doc];
|
||||
..explorer.layout = 'split'
|
||||
..explorer.hiddenItems = [doc];
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -214,7 +214,7 @@ void main() {
|
|||
expect(find.text('목록에서 숨긴 항목 (1개)'), findsOneWidget);
|
||||
await act(tester, () => tester.tap(find.byTooltip('숨기기 풀기')));
|
||||
await calm(tester);
|
||||
expect(c.settings.explorerHiddenItems, isEmpty);
|
||||
expect(c.settings.explorer.hiddenItems, isEmpty);
|
||||
expect(find.text('숨긴 항목이 없습니다'), findsOneWidget);
|
||||
await act(tester, () => tester.tap(find.text('닫기')));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||
|
|
@ -224,8 +224,8 @@ void main() {
|
|||
testWidgets('80: 크기순 정렬이면 폴더 크기를 계산해 보이고, "안 함" 이어도 길게 누르기 [폴더 크기 계산]', (tester) async {
|
||||
File(p.join(left, 'sub', 'big.bin')).writeAsBytesSync(List.filled(3000, 1));
|
||||
c.settings
|
||||
..explorerSort = 'size'
|
||||
..explorerSortDesc = true;
|
||||
..explorer.sort = 'size'
|
||||
..explorer.sortDesc = true;
|
||||
await open(tester);
|
||||
// sub = inner.txt (5) + big.bin (3000)
|
||||
await settle(tester, () => find.text('3KB').evaluate().isNotEmpty);
|
||||
|
|
@ -241,8 +241,8 @@ void main() {
|
|||
testWidgets('80: "계산 안 함" - 크기를 보이지 않다가 [폴더 크기 계산] 을 누르면', (tester) async {
|
||||
File(p.join(left, 'sub', 'big.bin')).writeAsBytesSync(List.filled(3000, 1));
|
||||
c.settings
|
||||
..explorerSort = 'size'
|
||||
..explorerFolderSize = 'off';
|
||||
..explorer.sort = 'size'
|
||||
..explorer.folderSize = 'off';
|
||||
await open(tester);
|
||||
await settle(tester, () => false, rounds: 15);
|
||||
expect(find.text('3KB'), findsNothing);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ void main() {
|
|||
|
||||
testWidgets('83: Total Commander 모양 단축키 - F5 복사 · F7 새 폴더 · Space/Insert 표시 (Space 는 빠른 필터가 아님)', (tester) async {
|
||||
File(p.join(left, 'e.txt')).writeAsStringSync('e');
|
||||
c.settings.explorerStyle = 'totalcmd';
|
||||
c.settings.explorer.style = 'totalcmd';
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('doc')));
|
||||
await settle(tester, () => false, rounds: 15);
|
||||
|
|
@ -51,7 +51,7 @@ void main() {
|
|||
if (bin is! FakeRecycleBin) return markTestSkipped('실제 휴지통 (JJ_TEST_REAL_RECYCLE=1) 에서는 건너뜀');
|
||||
if (!Platform.isWindows) return markTestSkipped('휴지통은 Windows 만 (Android 는 앱 휴지통 시험)');
|
||||
final victim = File(p.join(left, 'tcdel.txt'))..writeAsStringSync('x');
|
||||
c.settings.explorerStyle = 'totalcmd';
|
||||
c.settings.explorer.style = 'totalcmd';
|
||||
await open(tester);
|
||||
await settle(tester, () => find.text('tcdel').evaluate().isNotEmpty);
|
||||
await act(tester, () => tester.tap(find.text('tcdel')));
|
||||
|
|
@ -80,7 +80,7 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('83: Windows 탐색기 모양 단축키 - Ctrl+A 모두 표시 · Ctrl+C 담기 · Ctrl+V 붙여넣기', (tester) async {
|
||||
c.settings.explorerStyle = 'windows';
|
||||
c.settings.explorer.style = 'windows';
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||||
await settle(tester, () => false, rounds: 15);
|
||||
|
|
@ -151,7 +151,7 @@ void main() {
|
|||
File(p.join(right, 'doc.txt')).writeAsStringSync('old');
|
||||
File(p.join(left, 'e.txt')).writeAsStringSync('e');
|
||||
c.settings
|
||||
..explorerDropAction = 'move'
|
||||
..explorer.dropAction = 'move'
|
||||
..copyConflict = 'overwrite';
|
||||
await open(tester);
|
||||
// 전체 시험 중 (부하) 에는 목록을 늦게 읽어 e.txt 를 못 찾았다 - 두 파일이 보일 때까지
|
||||
|
|
@ -177,7 +177,7 @@ void main() {
|
|||
..createSync(recursive: true)
|
||||
..writeAsStringSync('mine');
|
||||
c.settings
|
||||
..explorerDropAction = 'move'
|
||||
..explorer.dropAction = 'move'
|
||||
..copyConflict = 'overwrite';
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('선택').first));
|
||||
|
|
@ -232,21 +232,21 @@ void main() {
|
|||
}
|
||||
|
||||
await act(tester, () => tester.tap(find.text('숨기기')));
|
||||
await until(() => c.settings.explorerProgressHidden && find.text('복사 · 이동 끝').evaluate().isNotEmpty);
|
||||
expect(c.settings.explorerProgressHidden, isTrue, reason: '숨김은 기억');
|
||||
await until(() => c.settings.explorer.progressHidden && find.text('복사 · 이동 끝').evaluate().isNotEmpty);
|
||||
expect(c.settings.explorer.progressHidden, isTrue, reason: '숨김은 기억');
|
||||
expect(find.text('숨기기'), findsNothing);
|
||||
expect(find.text('복사 · 이동 끝'), findsOneWidget, reason: '아래 작은 표시');
|
||||
// 작은 표시를 누름 (아래 알림이 덮고 있어도 - 알림이 닫히기를 기다리면 그사이 작은 표시가 내려갈 수 있다)
|
||||
tester.widget<TransferProgressChip>(find.byType(TransferProgressChip)).onTap();
|
||||
await until(() => !c.settings.explorerProgressHidden);
|
||||
await until(() => !c.settings.explorer.progressHidden);
|
||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||
expect(c.settings.explorerProgressHidden, isFalse, reason: '누르면 다시 펼침');
|
||||
expect(c.settings.explorer.progressHidden, isFalse, reason: '누르면 다시 펼침');
|
||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||
await calm(tester);
|
||||
});
|
||||
|
||||
testWidgets('81: 빠른 보기 (글 파일 앞부분) - Windows 는 TC 키 F3 (규칙 1), Android 는 Space · 필터에 치는 중에는 그냥 빈칸', (tester) async {
|
||||
c.settings.explorerStyle = 'windows';
|
||||
c.settings.explorer.style = 'windows';
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||||
await settle(tester, () => false, rounds: 15);
|
||||
|
|
@ -270,7 +270,7 @@ void main() {
|
|||
await calm(tester);
|
||||
|
||||
// TC: F3
|
||||
await c.updateSettings((s) => s.explorerStyle = 'totalcmd');
|
||||
await c.updateSettings((s) => s.explorer.style = 'totalcmd');
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('doc')));
|
||||
await settle(tester, () => false, rounds: 15);
|
||||
|
|
@ -284,7 +284,7 @@ void main() {
|
|||
// P0 (15 지적 10/17): 글자 칸에 치는 중에는 파일 단축키가 키를 가로채지 않는다 (예전: 필터 칸의 Delete → 파일 지우기 창)
|
||||
for (final style in const ['windows', 'xplore', 'totalcmd']) {
|
||||
testWidgets('P0 $style: 빠른 필터 칸에서 Ctrl+A · C · V · Delete · Space · F8 · Backspace 는 파일에 쓰이지 않는다', (tester) async {
|
||||
c.settings.explorerStyle = style;
|
||||
c.settings.explorer.style = style;
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text(style == 'totalcmd' ? 'doc' : 'doc.txt')));
|
||||
await settle(tester, () => false, rounds: 15);
|
||||
|
|
@ -345,35 +345,35 @@ void main() {
|
|||
|
||||
testWidgets('84: 열 머리를 누르면 그 기준으로 · 다시 누르면 거꾸로 (처음: 모든 폴더 같은 정렬)', (tester) async {
|
||||
File(p.join(left, 'a_big.txt')).writeAsStringSync('x' * 50);
|
||||
c.settings.explorerStyle = 'windows';
|
||||
c.settings.explorer.style = 'windows';
|
||||
await open(tester);
|
||||
double y(String name) => tester.getTopLeft(find.text(name).first).dy;
|
||||
expect(y('a_big.txt'), lessThan(y('doc.txt')), reason: '처음: 이름순');
|
||||
await act(tester, () => tester.tap(find.text('크기').first));
|
||||
await calm(tester);
|
||||
expect((c.settings.explorerSort, c.settings.explorerSortDesc), ('size', false));
|
||||
expect((c.settings.explorer.sort, c.settings.explorer.sortDesc), ('size', false));
|
||||
expect(y('doc.txt'), lessThan(y('a_big.txt')), reason: '크기 오름차순: doc.txt (5) 가 먼저');
|
||||
await act(tester, () => tester.tap(find.text('크기').first));
|
||||
await calm(tester);
|
||||
expect(c.settings.explorerSortDesc, isTrue, reason: '같은 열을 다시 누르면 거꾸로');
|
||||
expect(c.settings.explorer.sortDesc, isTrue, reason: '같은 열을 다시 누르면 거꾸로');
|
||||
// (거꾸로면 트리에 펼쳐진 위 폴더 (Temp 등) 의 차례도 바뀌어 시험 폴더가 화면 밖으로 갈 수 있어 줄 위치는 보지 않는다)
|
||||
expect(find.byIcon(Icons.arrow_drop_down), findsWidgets, reason: '지금 정렬 열에 ▼');
|
||||
});
|
||||
|
||||
testWidgets('84: "정렬을 폴더마다 기억" 을 켜면 그 폴더에만 (전체 정렬 · 다른 창은 그대로)', (tester) async {
|
||||
c.settings
|
||||
..explorerStyle = 'totalcmd'
|
||||
..explorerSortPerFolder = true;
|
||||
..explorer.style = 'totalcmd'
|
||||
..explorer.sortPerFolder = true;
|
||||
await open(tester);
|
||||
// 왼쪽 창에서 날짜 열
|
||||
await act(tester, () => tester.tap(find.text('날짜').first));
|
||||
await calm(tester);
|
||||
expect(c.settings.explorerSort, 'name', reason: '전체 정렬은 그대로');
|
||||
expect(c.settings.explorerFolderSorts.values, contains('date|0'));
|
||||
expect(c.settings.explorerFolderSorts.length, 1);
|
||||
expect(c.settings.explorer.sort, 'name', reason: '전체 정렬은 그대로');
|
||||
expect(c.settings.explorer.folderSorts.values, contains('date|0'));
|
||||
expect(c.settings.explorer.folderSorts.length, 1);
|
||||
// 저장 · 읽기
|
||||
expect(AppSettings.fromJson(c.settings.toJson()).explorerFolderSorts, c.settings.explorerFolderSorts);
|
||||
expect(AppSettings.fromJson(c.settings.toJson()).explorerSortPerFolder, isTrue);
|
||||
expect(AppSettings.fromJson({'explorerFolderSorts': 'broken'}).explorerFolderSorts, isEmpty, reason: '틀린 값은 비움');
|
||||
expect(AppSettings.fromJson(c.settings.toJson()).explorer.folderSorts, c.settings.explorer.folderSorts);
|
||||
expect(AppSettings.fromJson(c.settings.toJson()).explorer.sortPerFolder, isTrue);
|
||||
expect(AppSettings.fromJson({'explorerFolderSorts': 'broken'}).explorer.folderSorts, isEmpty, reason: '틀린 값은 비움');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,10 +53,10 @@ void main() {
|
|||
expect(keyConflict('copy', const SingleActivator(LogicalKeyboardKey.escape), tc: false, overrides: {}), 'escape');
|
||||
expect(keyConflict('copy', const SingleActivator(k, control: true), tc: false, overrides: {}), isNull);
|
||||
// 설정 저장 · 읽기 · 틀린 꼴은 비움
|
||||
final back = AppSettings.fromJson((AppSettings()..explorerKeyOverrides = {'copy': spec}).toJson());
|
||||
expect(back.explorerKeyOverrides, {'copy': spec});
|
||||
expect(AppSettings.fromJson({'explorerKeyOverrides': 'x'}).explorerKeyOverrides, isEmpty);
|
||||
expect(AppSettings().explorerKeyOverrides, isEmpty);
|
||||
final back = AppSettings.fromJson((AppSettings()..explorer.keyOverrides = {'copy': spec}).toJson());
|
||||
expect(back.explorer.keyOverrides, {'copy': spec});
|
||||
expect(AppSettings.fromJson({'explorerKeyOverrides': 'x'}).explorer.keyOverrides, isEmpty);
|
||||
expect(AppSettings().explorer.keyOverrides, isEmpty);
|
||||
});
|
||||
|
||||
late Directory tmp;
|
||||
|
|
@ -71,9 +71,9 @@ void main() {
|
|||
addTearDown(tester.view.reset);
|
||||
final c = ctl();
|
||||
c.settings
|
||||
..explorerPaths = [tmp.path, tmp.path]
|
||||
..explorerStyle = 'windows'
|
||||
..explorerKeyOverrides = {'newFolder': keySpec(const SingleActivator(k, control: true, shift: true))};
|
||||
..explorer.paths = [tmp.path, tmp.path]
|
||||
..explorer.style = 'windows'
|
||||
..explorer.keyOverrides = {'newFolder': keySpec(const SingleActivator(k, control: true, shift: true))};
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
|
||||
for (var i = 0; i < 20; i++) {
|
||||
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
|
||||
|
|
@ -99,7 +99,7 @@ void main() {
|
|||
tester.view.physicalSize = const Size(1000, 1600);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final c = ctl()..settings.explorerStyle = 'windows';
|
||||
final c = ctl()..settings.explorer.style = 'windows';
|
||||
await tester.pumpWidget(MaterialApp(home: ExplorerKeysPage(c: c)));
|
||||
Future<void> change(String label) async {
|
||||
await tester.tap(find.descendant(of: find.widgetWithText(ListTile, label), matching: find.text('바꾸기')));
|
||||
|
|
@ -112,18 +112,18 @@ void main() {
|
|||
expect(find.textContaining('이미 "새로 고침" 에 쓰는 키입니다'), findsOneWidget);
|
||||
await tester.tap(find.text('저장'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(c.settings.explorerKeyOverrides['copy'], keySpec(const SingleActivator(LogicalKeyboardKey.f5)));
|
||||
expect(c.settings.explorer.keyOverrides['copy'], keySpec(const SingleActivator(LogicalKeyboardKey.f5)));
|
||||
expect(find.descendant(of: find.widgetWithText(ListTile, '새로 고침'), matching: find.text('(없음)')), findsOneWidget,
|
||||
reason: '겹친 동작은 키를 잃음');
|
||||
// 처음 값으로
|
||||
await tester.tap(find.descendant(of: find.widgetWithText(ListTile, '다른 창으로 복사'), matching: find.text('처음 값으로')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(c.settings.explorerKeyOverrides, isEmpty);
|
||||
expect(c.settings.explorer.keyOverrides, isEmpty);
|
||||
// 단축키 없음
|
||||
await change('붙여넣기');
|
||||
await tester.tap(find.text('단축키 없음'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(c.settings.explorerKeyOverrides['paste'], '');
|
||||
expect(c.settings.explorer.keyOverrides['paste'], '');
|
||||
// Esc 는 저장할 수 없음
|
||||
await change('삭제');
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ void main() {
|
|||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings.explorerPaths = [tmp.path, tmp.path];
|
||||
c.settings.explorer.paths = [tmp.path, tmp.path];
|
||||
// 기기 글꼴 · 글자 크기가 시험 글꼴보다 크다 (S10 일본어에서 잘림) - 글자 크기를 키운 경우도
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(
|
||||
builder: (ctx, child) => MediaQuery(data: MediaQuery.of(ctx).copyWith(textScaler: TextScaler.linear(scale)), child: child!),
|
||||
|
|
|
|||
|
|
@ -70,15 +70,15 @@ void main() {
|
|||
testWidgets('창 배치: 두 창 = 창 둘 + 버튼 줄 · 한 창 = 창 하나 · 폴더 + 파일 목록 = 트리 + 목록 · 좁은 Rsync = [왼쪽 | 오른쪽]', (t) async {
|
||||
final h = await open(t);
|
||||
expect([find.byType(ExplorerPaneView).evaluate().length, find.byType(ExplorerToolbar).evaluate().length], [2, 1]);
|
||||
h.ctl.c.settings.explorerLayout = 'single';
|
||||
h.ctl.c.settings.explorer.layout = 'single';
|
||||
h.redraw();
|
||||
await t.pump();
|
||||
expect(find.byType(ExplorerPaneView).evaluate().length, 1);
|
||||
h.ctl.c.settings.explorerLayout = 'split';
|
||||
h.ctl.c.settings.explorer.layout = 'split';
|
||||
h.redraw();
|
||||
await t.pump();
|
||||
expect([find.byType(ExplorerPaneView).evaluate().length, find.byType(ExplorerListView).evaluate().length], [1, 1]);
|
||||
h.ctl.c.settings.explorerToolbar = 'hidden';
|
||||
h.ctl.c.settings.explorer.toolbar = 'hidden';
|
||||
h.redraw();
|
||||
await t.pump();
|
||||
expect(find.byType(ExplorerToolbar), findsNothing, reason: '버튼 줄 숨김');
|
||||
|
|
|
|||
|
|
@ -77,11 +77,11 @@ void main() {
|
|||
final s = m.ctl.c.settings;
|
||||
m.ctl.active = 0;
|
||||
expect([m.layout.dual, m.layout.split, identical(m.layout.other, m.ctl.panes[1]), m.layout.rowHeight() > 0], [true, false, true, true]);
|
||||
s.explorerLayout = 'single';
|
||||
s.explorer.layout = 'single';
|
||||
expect([m.layout.dual, m.layout.hiddenTarget, m.layout.other], [false, false, null], reason: '처음 값은 담아 두기');
|
||||
s.explorerOnePaneCopy = 'other';
|
||||
s.explorer.onePaneCopy = 'other';
|
||||
expect([m.layout.hiddenTarget, identical(m.layout.other, m.ctl.panes[1])], [true, true]);
|
||||
s.explorerLayout = 'split';
|
||||
s.explorer.layout = 'split';
|
||||
expect(m.layout.split, isTrue);
|
||||
m.layout.splitTight = true;
|
||||
expect(m.layout.split, isFalse);
|
||||
|
|
@ -106,7 +106,7 @@ void main() {
|
|||
|
||||
testWidgets('보기 창: 정렬 기준 고르고 확인 = 그 정렬 · 버튼 구성 [취소] = 그대로', (t) async {
|
||||
final m = await open(t);
|
||||
final before = [...m.ctl.c.settings.explorerButtons];
|
||||
final before = [...m.ctl.c.settings.explorer.buttons];
|
||||
unawaited(m.flows.dialogs.sortDialog(m.host.context));
|
||||
await t.pump();
|
||||
await t.tap(find.text('크기'));
|
||||
|
|
@ -118,7 +118,7 @@ void main() {
|
|||
await t.pump();
|
||||
await t.tap(find.text('취소'));
|
||||
await t.pump();
|
||||
expect(m.ctl.c.settings.explorerButtons, before);
|
||||
expect(m.ctl.c.settings.explorer.buttons, before);
|
||||
});
|
||||
|
||||
testWidgets('이름 바꾸기: 이름 창 → 실제로 바뀜 · 초점이 새 이름으로 / Wi-Fi 공유는 원격 폴더면 알림', (t) async {
|
||||
|
|
|
|||
|
|
@ -78,10 +78,10 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('X-plore a: 한 창 + "숨은 다른 창으로" - 위에 갈 곳 줄 · [복사] 는 그 폴더로 · [지금 폴더로] 로 바꾸기 (처음 값은 담아 두기)', (tester) async {
|
||||
expect(AppSettings().explorerOnePaneCopy, 'clip', reason: '처음 값은 지금 그대로');
|
||||
expect(AppSettings().explorer.onePaneCopy, 'clip', reason: '처음 값은 지금 그대로');
|
||||
c.settings
|
||||
..explorerLayout = 'single'
|
||||
..explorerOnePaneCopy = 'other';
|
||||
..explorer.layout = 'single'
|
||||
..explorer.onePaneCopy = 'other';
|
||||
await open(tester);
|
||||
expect(find.textContaining('갈 곳: '), findsOneWidget);
|
||||
expect(find.textContaining('갈 곳: right'), findsOneWidget, reason: '숨은 창의 폴더 이름 (전체 경로는 풍선 도움말)');
|
||||
|
|
@ -94,8 +94,8 @@ void main() {
|
|||
expect(File(p.join(right, 'doc.txt')).readAsStringSync(), 'hello');
|
||||
// [지금 폴더로] → 갈 곳이 지금 보는 폴더 (left)
|
||||
await act(tester, () => tester.tap(find.text('지금 폴더로')));
|
||||
await settle(tester, () => c.settings.explorerPaths[1] == left, rounds: 50);
|
||||
expect(c.settings.explorerPaths[1], left);
|
||||
await settle(tester, () => c.settings.explorer.paths[1] == left, rounds: 50);
|
||||
expect(c.settings.explorer.paths[1], left);
|
||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||
await calm(tester);
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
|
|
@ -195,7 +195,7 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('규칙 3: 펼친 폴더를 다시 누르면 접힘 · 두 번 누르기의 두 번째는 접지 않음 (처음 값 켬)', (tester) async {
|
||||
expect(AppSettings().explorerRetapCollapse, isTrue, reason: '규칙 3 (10/22)');
|
||||
expect(AppSettings().explorer.retapCollapse, isTrue, reason: '규칙 3 (10/22)');
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('sub')));
|
||||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||||
|
|
@ -215,7 +215,7 @@ void main() {
|
|||
testWidgets('X-plore c: X-plore 스타일 트리 맨 위에 다른 저장 장치 · 즐겨찾기 - 누르면 그곳으로, 즐겨찾기는 펼침', (tester) async {
|
||||
device.volumes = () => [(left, 'L:'), (right, 'R:')];
|
||||
c.settings
|
||||
..explorerStyle = 'xplore'
|
||||
..explorer.style = 'xplore'
|
||||
..folderBookmarks = [FolderBookmark(name: '내 즐겨찾기', path: p.join(left, 'sub'))];
|
||||
await open(tester);
|
||||
final otherRoot = find.byKey(ValueKey<String>('k1:$right'));
|
||||
|
|
@ -227,27 +227,27 @@ void main() {
|
|||
await calm(tester);
|
||||
expect(find.text('내 즐겨찾기'), findsOneWidget);
|
||||
await act(tester, () => tester.tap(find.text('내 즐겨찾기')));
|
||||
await settle(tester, () => c.settings.explorerPaths.first == p.join(left, 'sub'), rounds: 80);
|
||||
expect(c.settings.explorerPaths.first, p.join(left, 'sub'));
|
||||
await settle(tester, () => c.settings.explorer.paths.first == p.join(left, 'sub'), rounds: 80);
|
||||
expect(c.settings.explorer.paths.first, p.join(left, 'sub'));
|
||||
await act(tester, () => tester.tap(find.byKey(ValueKey<String>('k1:$right')).first));
|
||||
await settle(tester, () => c.settings.explorerPaths.first == right, rounds: 80);
|
||||
expect(c.settings.explorerPaths.first, right, reason: '다른 저장 장치 줄 = 그 저장 장치로');
|
||||
await settle(tester, () => c.settings.explorer.paths.first == right, rounds: 80);
|
||||
expect(c.settings.explorer.paths.first, right, reason: '다른 저장 장치 줄 = 그 저장 장치로');
|
||||
// 다른 스타일은 그대로 (맨 위 항목 없음)
|
||||
await tester.runAsync(() => c.updateSettings((x) => x.explorerStyle = 'windows'));
|
||||
await tester.runAsync(() => c.updateSettings((x) => x.explorer.style = 'windows'));
|
||||
await calm(tester);
|
||||
expect(find.byKey(const ValueKey<String>('k2:\u0000favorites')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('X-plore ⚖: 탐색기 안에서 옆으로 밀기 = 창 바꾸기 (한 창이면 숨은 창과 바꿈)', (tester) async {
|
||||
expect(AppSettings().explorerSwipePanes, isFalse, reason: '처음 값은 끔 - 화면 가운데를 밀면 화면 이동 (10/8 사용자 결정)');
|
||||
c.settings.explorerSwipePanes = true;
|
||||
c.settings.explorerLayout = 'single';
|
||||
expect(AppSettings().explorer.swipePanes, isFalse, reason: '처음 값은 끔 - 화면 가운데를 밀면 화면 이동 (10/8 사용자 결정)');
|
||||
c.settings.explorer.swipePanes = true;
|
||||
c.settings.explorer.layout = 'single';
|
||||
await open(tester);
|
||||
expect(c.settings.explorerPaths, [left, right]);
|
||||
expect(c.settings.explorer.paths, [left, right]);
|
||||
// 화면 가운데 높이에서 (위 · 아래 가장자리 1/5 는 밀기로 보지 않는다 - SwipeNav)
|
||||
await tester.flingFrom(tester.getCenter(find.byType(ExplorerPage)), const Offset(-400, 0), 1500);
|
||||
await settle(tester, () => c.settings.explorerPaths.first == right, rounds: 60);
|
||||
expect(c.settings.explorerPaths, [right, left], reason: '숨은 창이 보이는 창으로');
|
||||
await settle(tester, () => c.settings.explorer.paths.first == right, rounds: 60);
|
||||
expect(c.settings.explorer.paths, [right, left], reason: '숨은 창이 보이는 창으로');
|
||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ void main() {
|
|||
Directory(right).createSync();
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerPaths = [left, right]
|
||||
..explorer.paths = [left, right]
|
||||
..copyBandwidthKBps = 512; // 큰 파일이 6초쯤 걸리게
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -69,7 +69,7 @@ void main() {
|
|||
device.volumes = () => [(tmp.path, 'T:')];
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerOrientation = 'side'
|
||||
..explorer.orientation = 'side'
|
||||
..rsyncPaths = [l, r];
|
||||
tester.view.physicalSize = const Size(400, 860);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -109,7 +109,7 @@ void main() {
|
|||
device.volumes = () => [(tmp.path, 'T:')];
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerOrientation = 'side'
|
||||
..explorer.orientation = 'side'
|
||||
..rsyncPaths = [l, r];
|
||||
tester.view.physicalSize = const Size(412, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -145,7 +145,7 @@ void main() {
|
|||
device.volumes = () => [(tmp.path, 'T:')];
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerOrientation = 'side'
|
||||
..explorer.orientation = 'side'
|
||||
..rsyncPaths = [l, r];
|
||||
tester.view.physicalSize = const Size(860, 400);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -167,7 +167,7 @@ void main() {
|
|||
device.volumes = () => [(inner, '내장 저장소'), (sd, 'SD 카드')];
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerOrientation = 'side'
|
||||
..explorer.orientation = 'side'
|
||||
..rsyncPaths = [p.join(inner, 'L'), p.join(sd, 'R')];
|
||||
tester.view.physicalSize = const Size(412, 880);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -193,7 +193,7 @@ void main() {
|
|||
Directory(p.join(r, 'rsub')).createSync(recursive: true);
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerLayout = 'single'
|
||||
..explorer.layout = 'single'
|
||||
..rsyncPaths = [l, r];
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -226,8 +226,8 @@ void main() {
|
|||
File(p.join(l, 'top.txt')).writeAsStringSync('t');
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerLayout = 'single'
|
||||
..explorerPaths = [l, l];
|
||||
..explorer.layout = 'single'
|
||||
..explorer.paths = [l, l];
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -259,8 +259,8 @@ void main() {
|
|||
}
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerLayout = 'single'
|
||||
..explorerPaths = [dl, dl];
|
||||
..explorer.layout = 'single'
|
||||
..explorer.paths = [dl, dl];
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -293,9 +293,9 @@ void main() {
|
|||
File(p.join(l, 'zz_file.txt')).writeAsStringSync('t');
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerLayout = 'dual'
|
||||
..explorerOrientation = 'stacked'
|
||||
..explorerPaths = [l, l];
|
||||
..explorer.layout = 'dual'
|
||||
..explorer.orientation = 'stacked'
|
||||
..explorer.paths = [l, l];
|
||||
tester.view.physicalSize = const Size(420, 640);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -311,17 +311,17 @@ void main() {
|
|||
});
|
||||
|
||||
test('118: 두 창 배치 처음 값은 좌우 · 예전 "화면 모양 따라" 는 한 번 좌우로 (알림) · 고른 위아래는 그대로', () {
|
||||
expect(AppSettings().explorerOrientation, 'side');
|
||||
expect(AppSettings.fromJson({}).explorerOrientation, 'side');
|
||||
expect(AppSettings().explorer.orientation, 'side');
|
||||
expect(AppSettings.fromJson({}).explorer.orientation, 'side');
|
||||
final old = AppSettings.fromJson({'explorerOrientation': 'auto'});
|
||||
expect(old.explorerOrientation, 'side');
|
||||
expect(old.explorer.orientation, 'side');
|
||||
expect(old.migrated, contains('explorerOrientation'));
|
||||
final stacked = AppSettings.fromJson({'explorerOrientation': 'stacked'});
|
||||
expect(stacked.explorerOrientation, 'stacked');
|
||||
expect(stacked.explorer.orientation, 'stacked');
|
||||
expect(stacked.migrated, isNot(contains('explorerOrientation')));
|
||||
// 옮긴 뒤 사용자가 다시 '화면 모양 따라' 를 고르면 그대로
|
||||
final again = AppSettings.fromJson((AppSettings()..explorerOrientation = 'auto').toJson());
|
||||
expect(again.explorerOrientation, 'auto');
|
||||
final again = AppSettings.fromJson((AppSettings()..explorer.orientation = 'auto').toJson());
|
||||
expect(again.explorer.orientation, 'auto');
|
||||
expect(again.migrated, isEmpty);
|
||||
});
|
||||
|
||||
|
|
@ -334,8 +334,8 @@ void main() {
|
|||
..writeAsStringSync('a');
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerLayout = 'single'
|
||||
..explorerPaths = [l, l];
|
||||
..explorer.layout = 'single'
|
||||
..explorer.paths = [l, l];
|
||||
// 폰 세로처럼 좁고 긴 화면
|
||||
tester.view.physicalSize = const Size(500, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -356,15 +356,15 @@ void main() {
|
|||
}
|
||||
|
||||
await press('두 창으로 보기 (좌우)', '두 창으로');
|
||||
expect((c.settings.explorerLayout, c.settings.explorerOrientation), ('dual', 'side'), reason: '폰 세로에서도 좌우');
|
||||
expect((c.settings.explorer.layout, c.settings.explorer.orientation), ('dual', 'side'), reason: '폰 세로에서도 좌우');
|
||||
expect(find.text('a.txt'), findsNWidgets(2));
|
||||
final left = tester.getCenter(find.text('a.txt').first), right = tester.getCenter(find.text('a.txt').last);
|
||||
expect(right.dx, greaterThan(left.dx));
|
||||
await press('창 배치: 좌우 (누르면 위아래)', '좌우 ⇆');
|
||||
expect(c.settings.explorerOrientation, 'stacked');
|
||||
expect(c.settings.explorer.orientation, 'stacked');
|
||||
final top = tester.getCenter(find.text('a.txt').first), bottom = tester.getCenter(find.text('a.txt').last);
|
||||
expect(bottom.dy, greaterThan(top.dy));
|
||||
await press('창 배치: 위아래 (누르면 좌우)', '위아래 ⇅');
|
||||
expect(c.settings.explorerOrientation, 'side');
|
||||
expect(c.settings.explorer.orientation, 'side');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ void main() {
|
|||
|
||||
testWidgets('폴더 + 파일 목록 배치 · Windows 탐색기 / Total Commander 스타일', (tester) async {
|
||||
c.settings
|
||||
..explorerLayout = 'split'
|
||||
..explorerStyle = 'windows';
|
||||
..explorer.layout = 'split'
|
||||
..explorer.style = 'windows';
|
||||
await open(tester);
|
||||
// 왼쪽 트리는 폴더만, 오른쪽 목록은 고른 폴더 (left) 의 내용
|
||||
expect(find.text('sub'), findsNWidgets(2)); // 트리 + 목록
|
||||
|
|
@ -62,7 +62,7 @@ void main() {
|
|||
expect(find.text('inner.txt'), findsOneWidget); // 목록에 바로 보임
|
||||
|
||||
// Total Commander: [폴더] · <DIR> · 확장자 칸
|
||||
await c.updateSettings((s) => s.explorerStyle = 'totalcmd');
|
||||
await c.updateSettings((s) => s.explorer.style = 'totalcmd');
|
||||
await settle(tester, () => find.text('[sub]').evaluate().length == 2);
|
||||
expect(find.text('[sub]'), findsNWidgets(2));
|
||||
expect(find.text('<DIR>'), findsOneWidget);
|
||||
|
|
@ -115,7 +115,7 @@ void main() {
|
|||
expect(find.byTooltip('표시 지우기'), findsNothing);
|
||||
expect(find.byTooltip('표시 (여러 개 고르기)'), findsNothing);
|
||||
// 설정 "늘 보이기" 를 켜면 X-plore 스타일은 선택 모드 밖에서도 보임
|
||||
await tester.runAsync(() => c.updateSettings((x) => x.explorerAlwaysMarks = true));
|
||||
await tester.runAsync(() => c.updateSettings((x) => x.explorer.alwaysMarks = true));
|
||||
await settle(tester, () => false, rounds: 5);
|
||||
expect(find.byTooltip('표시 (여러 개 고르기)'), findsWidgets);
|
||||
});
|
||||
|
|
@ -333,7 +333,7 @@ void main() {
|
|||
testWidgets('75: 창마다 뒤로 · 앞으로 - 버튼 · Alt+← / → · 마우스 4 · 5번', (tester) async {
|
||||
await open(tester);
|
||||
final sub = p.join(left, 'sub');
|
||||
String now() => c.settings.explorerPaths.first;
|
||||
String now() => c.settings.explorer.paths.first;
|
||||
await act(tester, () => tester.tap(find.text('sub').first));
|
||||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||||
// 뒤로 (버튼): 왼쪽 창은 left 로
|
||||
|
|
@ -374,7 +374,7 @@ void main() {
|
|||
await open(tester);
|
||||
final r1 = p.join(right, 'r1');
|
||||
final sub = p.join(left, 'sub');
|
||||
String at(int i) => c.settings.explorerPaths[i];
|
||||
String at(int i) => c.settings.explorer.paths[i];
|
||||
await act(tester, () => tester.tap(find.text('r1')));
|
||||
await settle(tester, () => at(1) == r1 && find.text('x.txt').evaluate().isNotEmpty, rounds: 50);
|
||||
// 왼쪽 창이 활성
|
||||
|
|
@ -399,7 +399,7 @@ void main() {
|
|||
testWidgets('75: 지워진 폴더로 앞으로 - 그 항목은 버리고 지금 폴더 그대로 · 이유를 알림', (tester) async {
|
||||
await open(tester);
|
||||
final sub = p.join(left, 'sub');
|
||||
String now() => c.settings.explorerPaths.first;
|
||||
String now() => c.settings.explorer.paths.first;
|
||||
await act(tester, () => tester.tap(find.text('sub')));
|
||||
await settle(tester, () => now() == sub, rounds: 50);
|
||||
await act(tester, () => tester.tap(find.byTooltip('뒤로 (Alt+←)').first));
|
||||
|
|
|
|||
|
|
@ -52,11 +52,11 @@ void main() {
|
|||
addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, null));
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerPaths = [dir, dir]
|
||||
..explorerLayout = 'split'
|
||||
..explorerStyle = 'windows'
|
||||
..explorerSort = sort
|
||||
..explorerSortByKind = kind;
|
||||
..explorer.paths = [dir, dir]
|
||||
..explorer.layout = 'split'
|
||||
..explorer.style = 'windows'
|
||||
..explorer.sort = sort
|
||||
..explorer.sortByKind = kind;
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
|
||||
for (var i = 0; i < 300 && (i < 10 || find.text(wait).evaluate().isEmpty); i++) {
|
||||
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
|
||||
|
|
@ -77,9 +77,9 @@ void main() {
|
|||
}
|
||||
|
||||
test('설정: 처음 끔 · 저장 · 읽기 · 예전 설정 (값 없음) 은 끔', () {
|
||||
expect(AppSettings().explorerSortByKind, isFalse);
|
||||
expect(AppSettings.fromJson((AppSettings()..explorerSortByKind = true).toJson()).explorerSortByKind, isTrue);
|
||||
expect(AppSettings.fromJson({}).explorerSortByKind, isFalse);
|
||||
expect(AppSettings().explorer.sortByKind, isFalse);
|
||||
expect(AppSettings.fromJson((AppSettings()..explorer.sortByKind = true).toJson()).explorer.sortByKind, isTrue);
|
||||
expect(AppSettings.fromJson({}).explorer.sortByKind, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('끄면 (처음) 사진 폴더도 지금 정렬 (이름) 그대로', (tester) async {
|
||||
|
|
@ -98,8 +98,8 @@ void main() {
|
|||
await tester.pump();
|
||||
}
|
||||
expect(order(tester, ['a.jpg', 'b.jpg', 'c.mp4', 'd.jpg']), ['a.jpg', 'b.jpg', 'c.mp4', 'd.jpg'], reason: '누른 것이 바로 보인다');
|
||||
expect(c.settings.explorerFolderSorts.values, contains('name|0'));
|
||||
expect((c.settings.explorerSort, c.settings.explorerSortDesc), ('name', false), reason: '전체 정렬은 그대로');
|
||||
expect(c.settings.explorer.folderSorts.values, contains('name|0'));
|
||||
expect((c.settings.explorer.sort, c.settings.explorer.sortDesc), ('name', false), reason: '전체 정렬은 그대로');
|
||||
await close(tester);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ void main() {
|
|||
device.volumes = () => [(tmp.path, 'T:')];
|
||||
c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), shell: NoopShell()));
|
||||
c.settings
|
||||
..explorerPaths = [left, right]
|
||||
..explorer.paths = [left, right]
|
||||
..copyMethodFile = 'builtin'
|
||||
..copyMethodFolder = 'builtin';
|
||||
});
|
||||
|
|
@ -56,7 +56,7 @@ void main() {
|
|||
for (final layout in ['dual', 'split']) {
|
||||
for (final selecting in [false, true]) {
|
||||
testWidgets('180: 복사해도 자리 · 스크롤 그대로 ($layout${selecting ? ' · 선택 모드' : ''})', (tester) async {
|
||||
c.settings.explorerLayout = layout;
|
||||
c.settings.explorer.layout = layout;
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -120,8 +120,8 @@ void main() {
|
|||
|
||||
testWidgets('180: 창 크기가 바뀌어 두 창이 가로 ↔ 세로로 바뀌어도 스크롤 그대로', (tester) async {
|
||||
c.settings
|
||||
..explorerLayout = 'dual'
|
||||
..explorerOrientation = 'auto';
|
||||
..explorer.layout = 'dual'
|
||||
..explorer.orientation = 'auto';
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -146,8 +146,8 @@ void main() {
|
|||
|
||||
testWidgets('183: [새로 고침] 은 두 창 모두 (같은 폴더를 보는 다른 창에 밖에서 지운 파일이 남지 않게)', (tester) async {
|
||||
c.settings
|
||||
..explorerLayout = 'dual'
|
||||
..explorerPaths = [left, left];
|
||||
..explorer.layout = 'dual'
|
||||
..explorer.paths = [left, left];
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
@ -162,8 +162,8 @@ void main() {
|
|||
|
||||
testWidgets('232: 앱으로 돌아오면 보이는 폴더를 다시 읽는다 (다른 앱에서 지우고 · 만든 것이 바로 보이게)', (tester) async {
|
||||
c.settings
|
||||
..explorerLayout = 'dual'
|
||||
..explorerPaths = [left, left];
|
||||
..explorer.layout = 'dual'
|
||||
..explorer.paths = [left, left];
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
|
|||
|
|
@ -21,16 +21,16 @@ void main() {
|
|||
|
||||
test('설정: 처음은 100% · 보통 (지금 그대로), 저장 · 읽기, 틀린 값은 그대로 쪽으로 · 범위 밖은 끝으로', () {
|
||||
final s = AppSettings();
|
||||
expect((s.explorerTextScale, s.explorerRowSpacing), (100, 'normal'));
|
||||
expect((s.explorer.textScale, s.explorer.rowSpacing), (100, 'normal'));
|
||||
s
|
||||
..explorerTextScale = 130
|
||||
..explorerRowSpacing = 'wide';
|
||||
..explorer.textScale = 130
|
||||
..explorer.rowSpacing = 'wide';
|
||||
final back = AppSettings.fromJson(s.toJson());
|
||||
expect((back.explorerTextScale, back.explorerRowSpacing), (130, 'wide'));
|
||||
expect(AppSettings.fromJson({}).explorerTextScale, 100);
|
||||
expect(AppSettings.fromJson({'explorerTextScale': 'x', 'explorerRowSpacing': 'huge'}).explorerRowSpacing, 'normal');
|
||||
expect(AppSettings.fromJson({'explorerTextScale': 300}).explorerTextScale, 150);
|
||||
expect(AppSettings.fromJson({'explorerTextScale': 10}).explorerTextScale, 80);
|
||||
expect((back.explorer.textScale, back.explorer.rowSpacing), (130, 'wide'));
|
||||
expect(AppSettings.fromJson({}).explorer.textScale, 100);
|
||||
expect(AppSettings.fromJson({'explorerTextScale': 'x', 'explorerRowSpacing': 'huge'}).explorer.rowSpacing, 'normal');
|
||||
expect(AppSettings.fromJson({'explorerTextScale': 300}).explorer.textScale, 150);
|
||||
expect(AppSettings.fromJson({'explorerTextScale': 10}).explorer.textScale, 80);
|
||||
});
|
||||
|
||||
/// [name] 글자의 실제 글자 크기 (기기 · 목록 글자 크기를 곱한 것)
|
||||
|
|
@ -54,14 +54,14 @@ void main() {
|
|||
Directory(p.join(tmp.path, 'sub')).createSync();
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerPaths = [tmp.path, tmp.path]
|
||||
..explorer.paths = [tmp.path, tmp.path]
|
||||
// 폴더 + 파일 목록: 오른쪽 목록은 그 폴더의 것만이라 짧다 (트리는 Temp 의 다른 폴더가 많아 아래로 밀린다)
|
||||
..explorerLayout = 'split'
|
||||
..explorerStyle = style
|
||||
..explorer.layout = 'split'
|
||||
..explorer.style = style
|
||||
// ✔ 칸 자리를 보는 시험: 표시를 늘 보이게 (설정 - 처음 값은 규칙 4 로 선택 모드에서만)
|
||||
..explorerAlwaysMarks = true
|
||||
..explorerTextScale = scale
|
||||
..explorerRowSpacing = spacing;
|
||||
..explorer.alwaysMarks = true
|
||||
..explorer.textScale = scale
|
||||
..explorer.rowSpacing = spacing;
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(
|
||||
builder: (ctx, child) => MediaQuery(data: MediaQuery.of(ctx).copyWith(textScaler: TextScaler.linear(device)), child: child!),
|
||||
home: ExplorerPage(c: c))));
|
||||
|
|
@ -119,7 +119,7 @@ void main() {
|
|||
final c = await open(tester, style, sub, device: 1.3, scale: 150, spacing: 'wide');
|
||||
await tester.pump();
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(c.settings.explorerLayout, 'split', reason: '고른 배치는 바꾸지 않는다');
|
||||
expect(c.settings.explorer.layout, 'split', reason: '고른 배치는 바꾸지 않는다');
|
||||
expect(find.text(sub), findsOneWidget, reason: '트리 · 목록 두 창이 아니라 한 창');
|
||||
expect(find.text(a), findsOneWidget, reason: '한 창 트리에 파일도 보인다');
|
||||
final para = tester.renderObject<RenderParagraph>(find.text(sub));
|
||||
|
|
@ -156,7 +156,7 @@ void main() {
|
|||
final sub = style == 'totalcmd' ? '[sub]' : 'sub';
|
||||
final c = await open(tester, style, sub, device: 1.3, scale: 150);
|
||||
await tester.pump();
|
||||
expect(c.settings.explorerLayout, 'split');
|
||||
expect(c.settings.explorer.layout, 'split');
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.text(type), findsNothing, reason: '좁아서 유형 열 숨김');
|
||||
expect(find.text('크기'), findsOneWidget, reason: '크기는 남김');
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ void main() {
|
|||
}
|
||||
|
||||
testWidgets('파일 탐색기: WebDAV 탭 → 서버 추가 (연결 확인) → 목록 · 열기 (받아서) · 로컬 → WebDAV 복사', (tester) async {
|
||||
c.settings.explorerPaths = [left, left];
|
||||
c.settings.explorer.paths = [left, left];
|
||||
await pump(tester, ExplorerPage(device: device, c: c));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||
// 서버가 없으면 위쪽 저장 장치 옆에 "WebDAV" 탭 (창마다)
|
||||
|
|
@ -165,7 +165,7 @@ void main() {
|
|||
addTearDown(() => SecretGate.check = null);
|
||||
c.settings
|
||||
..webdavServers = [s]
|
||||
..explorerPaths = [left, left]
|
||||
..explorer.paths = [left, left]
|
||||
..folderBookmarks = [const FolderBookmark(name: 'NAS 원격', path: 'dav://nas/원격')];
|
||||
DavRegistry.configure([s]);
|
||||
await pump(tester, ExplorerPage(device: device, c: c));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,254 @@
|
|||
{
|
||||
"__migrated": [
|
||||
"orientation",
|
||||
"explorerOrientation",
|
||||
"cookieScope",
|
||||
"explorerMarksRule",
|
||||
"explorerRetapRule",
|
||||
"explorerLayoutRule",
|
||||
"swipeNav",
|
||||
"dnsForced",
|
||||
"copySkipNewer"
|
||||
],
|
||||
"addFinishedDownloads": true,
|
||||
"aiAgreed": [],
|
||||
"aiAutoDevice": "",
|
||||
"aiAutoDeviceFor": "",
|
||||
"aiCfg": 1.0,
|
||||
"aiCount": 1,
|
||||
"aiDevice": "auto",
|
||||
"aiEngine": "local",
|
||||
"aiHeight": 512,
|
||||
"aiImageV1": true,
|
||||
"aiNegative": "",
|
||||
"aiPrompt": "",
|
||||
"aiQualityButton": false,
|
||||
"aiQualityStrength": "normal",
|
||||
"aiRecent": [],
|
||||
"aiSaveDir": "",
|
||||
"aiServices": [],
|
||||
"aiSource": "und",
|
||||
"aiSteps": 4,
|
||||
"aiStrength": 0.6,
|
||||
"aiTaesd": true,
|
||||
"aiTargets": [
|
||||
"ko",
|
||||
"en",
|
||||
"ja"
|
||||
],
|
||||
"aiUpAfter": "upscaled",
|
||||
"aiUpDir": "",
|
||||
"aiUpFormat": "same",
|
||||
"aiUpJpgQuality": 92,
|
||||
"aiUpMemory": "normal",
|
||||
"aiUpModel": "auto",
|
||||
"aiUpScale": 2,
|
||||
"aiUpSecPerMp": {},
|
||||
"aiWhisper": "whisper-base",
|
||||
"aiWidth": 512,
|
||||
"allFilesHint2": false,
|
||||
"allowInnerToOuter": true,
|
||||
"androidTrashDays": 14,
|
||||
"appIcon": "yellow",
|
||||
"askAiOptions": true,
|
||||
"autoCheckUpdates": true,
|
||||
"backgroundV2": true,
|
||||
"batchRenameLast": {},
|
||||
"browserEngine": "edge",
|
||||
"browserSwipeHinted": false,
|
||||
"cefWhenElevated": false,
|
||||
"cleanupPrechecked": [
|
||||
"work",
|
||||
"download"
|
||||
],
|
||||
"clipboardWatch": true,
|
||||
"closeAction": "background",
|
||||
"components": [
|
||||
"mkv",
|
||||
"browser",
|
||||
"explorer",
|
||||
"rsync",
|
||||
"downloads",
|
||||
"aiimage",
|
||||
"viewer"
|
||||
],
|
||||
"cookieScopeV2": true,
|
||||
"copyBandwidthKBps": 0,
|
||||
"copyConflict": "ask",
|
||||
"copyMethodFile": "builtin",
|
||||
"copyMethodFolder": "builtin",
|
||||
"copyRunMode": "each",
|
||||
"copySkipNewer": true,
|
||||
"copyTasks": [],
|
||||
"davExternalOpen": "ask",
|
||||
"dnsDohUrl": "https://dns.google/dns-query",
|
||||
"dnsMode": "custom",
|
||||
"dnsPrimary": "8.8.8.8",
|
||||
"dnsSecondary": "8.8.4.4",
|
||||
"downloadRoot": null,
|
||||
"encode": {
|
||||
"brightness": 0,
|
||||
"codec": "copy",
|
||||
"contrast": 0,
|
||||
"fit": "fill",
|
||||
"frame": "original",
|
||||
"quality": "normal",
|
||||
"resolution": "original",
|
||||
"rotate": "none",
|
||||
"saturation": 0,
|
||||
"temperature": 0
|
||||
},
|
||||
"explorerAlwaysMarks": false,
|
||||
"explorerButtons": [],
|
||||
"explorerClick": "open",
|
||||
"explorerDropAction": "ask",
|
||||
"explorerFolderSize": "sort",
|
||||
"explorerFolderSorts": {},
|
||||
"explorerHiddenItems": [],
|
||||
"explorerHideConfirm": true,
|
||||
"explorerHistory": [],
|
||||
"explorerKeyOverrides": {},
|
||||
"explorerLayout": "dual",
|
||||
"explorerOnePaneCopy": "clip",
|
||||
"explorerOrientV3": true,
|
||||
"explorerOrientation": "side",
|
||||
"explorerPaths": [],
|
||||
"explorerProgressHidden": false,
|
||||
"explorerRemoteThumbCacheMb": 200,
|
||||
"explorerRemoteThumbs": "wifi",
|
||||
"explorerRetapCollapse": true,
|
||||
"explorerRootsInTree": true,
|
||||
"explorerRowSpacing": "normal",
|
||||
"explorerRulesV1": true,
|
||||
"explorerShowHidden": false,
|
||||
"explorerSort": "name",
|
||||
"explorerSortByKind": false,
|
||||
"explorerSortDesc": false,
|
||||
"explorerSortPerFolder": false,
|
||||
"explorerStyle": "xplore",
|
||||
"explorerSwipePanes": false,
|
||||
"explorerTextScale": 100,
|
||||
"explorerToolbar": "middle",
|
||||
"explorerV2": true,
|
||||
"externalBrowser": "system",
|
||||
"externalPlayers": {},
|
||||
"folderBookmarks": [],
|
||||
"ftpServers": [],
|
||||
"homeUrl": "https://www.youtube.com/",
|
||||
"imageExts": [
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
"gif",
|
||||
"webp",
|
||||
"bmp"
|
||||
],
|
||||
"lastRunVersion": "",
|
||||
"lastUpdateCheck": "",
|
||||
"lastUpdateCheckVersion": "",
|
||||
"liveSyncIntervalSec": 30,
|
||||
"liveSyncOnStart": "auto",
|
||||
"liveSyncPairs": [],
|
||||
"loginCookieSites": [
|
||||
"youtube.com",
|
||||
"google.com",
|
||||
"instagram.com",
|
||||
"x.com",
|
||||
"twitter.com",
|
||||
"naver.com"
|
||||
],
|
||||
"maxParallelDownloads": 3,
|
||||
"maxParallelJobs": 5,
|
||||
"migrationNotice": true,
|
||||
"minimizeToTray": true,
|
||||
"mkvCompactList": "auto",
|
||||
"mkvExisting": "ask",
|
||||
"mkvOutputRoot": null,
|
||||
"moveTargets": [],
|
||||
"navOrder": [
|
||||
"mkv",
|
||||
"browser",
|
||||
"tor",
|
||||
"explorer",
|
||||
"rsync",
|
||||
"downloads",
|
||||
"aiimage"
|
||||
],
|
||||
"openFileAction": "play",
|
||||
"openFileWindow": "same",
|
||||
"openSubtitlesSaved": {
|
||||
"key": false,
|
||||
"password": false,
|
||||
"user": false
|
||||
},
|
||||
"orientationV2": true,
|
||||
"playlistMode": "series",
|
||||
"preferredSubtitleLanguage": "",
|
||||
"readerAutoSeconds": 3,
|
||||
"readerBrightness": 0.0,
|
||||
"readerFit": "page",
|
||||
"readerGallery": false,
|
||||
"readerRtl": false,
|
||||
"readerSplit": "auto",
|
||||
"readerSystemBars": true,
|
||||
"readerVolumeKeys": false,
|
||||
"recycleOnDelete": true,
|
||||
"rememberPasswords": true,
|
||||
"remoteIdleSeconds": 60,
|
||||
"robocopyLinksV1": true,
|
||||
"robocopyOptions": "/E /COPY:DAT /DCOPY:T /R:2 /W:2",
|
||||
"rollbackKeepModels": "",
|
||||
"rootEnabled": false,
|
||||
"rsyncOptions": "-avPog",
|
||||
"rsyncPath": "",
|
||||
"rsyncPaths": [],
|
||||
"rsyncSource": "download",
|
||||
"runInBackground": true,
|
||||
"screenOrientation": "auto",
|
||||
"sftpServers": [],
|
||||
"shizukuEnabled": false,
|
||||
"showHotkey": "Ctrl+Shift+X",
|
||||
"showLog": true,
|
||||
"skippedVersion": "",
|
||||
"smbRequireSecure": false,
|
||||
"smbServers": [],
|
||||
"startMenuShortcut": true,
|
||||
"startScreen": "home",
|
||||
"subtitleScale": 1.0,
|
||||
"swipeNav": true,
|
||||
"swipeWrap": true,
|
||||
"syncStopToast": true,
|
||||
"torHomeUrl": "https://check.torproject.org/",
|
||||
"torIdleMinutes": 2,
|
||||
"torJavaScript": true,
|
||||
"torKeepData": false,
|
||||
"torNoticeSeen": false,
|
||||
"torrentStallSeconds": 60,
|
||||
"tripleTapAction": "move",
|
||||
"uiBrightness": "dark",
|
||||
"uiLanguage": "ko",
|
||||
"uiLanguagesAdded": [],
|
||||
"uiScale": 1.0,
|
||||
"uiScaleDefault": 1.0,
|
||||
"vaultAutoLockSeconds": 60,
|
||||
"vaultEnabled": false,
|
||||
"vaultMasterCopy": false,
|
||||
"webJavaScript": true,
|
||||
"webPauseOnLeave": true,
|
||||
"webTranslate": false,
|
||||
"webdavServers": [],
|
||||
"wifiShareFolder": "",
|
||||
"wifiShareMinutes": 30,
|
||||
"wifiShareTailscale": true,
|
||||
"youtubeAdHide": true,
|
||||
"youtubeAdSkip": true,
|
||||
"ytContainer": "mp4",
|
||||
"ytCookiesBrowser": "*internal",
|
||||
"ytCookiesFile": "",
|
||||
"ytExpandPlaylists": true,
|
||||
"ytPreferH264": false,
|
||||
"ytQuality": "best",
|
||||
"zipComic": false,
|
||||
"zipComicV2": true,
|
||||
"zipLevel": 6
|
||||
}
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
{
|
||||
"__migrated": [
|
||||
"zipComic",
|
||||
"explorerMarksRule",
|
||||
"explorerRetapRule"
|
||||
],
|
||||
"addFinishedDownloads": false,
|
||||
"aiAgreed": [],
|
||||
"aiAutoDevice": "x_",
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ void main() {
|
|||
Directory(p.join(tmp.path, 'sub')).createSync();
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerPaths = [tmp.path, tmp.path]
|
||||
..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'))];
|
||||
|
|
|
|||
|
|
@ -311,14 +311,14 @@ void main() {
|
|||
settingsStore: store);
|
||||
await c.init();
|
||||
// 보통 저장: 모르는 키 · 다른 판의 열쇠 남음 (40)
|
||||
await c.updateSettings((s) => s.explorerLayout = 'dual');
|
||||
await c.updateSettings((s) => s.explorer.layout = 'dual');
|
||||
Map<String, dynamic> saved() => jsonDecode(file.readAsStringSync()) as Map<String, dynamic>;
|
||||
expect(saved()['zz_new_version'], {'keep': 1});
|
||||
expect(secrets.values['nfs.pw.n1'], 'new-version-pw');
|
||||
// 앱 설정 전체: 모르는 키 없음 · 다른 판의 열쇠는 아직 남음 (비밀번호 체크를 안 함)
|
||||
await resetAllSettings(c);
|
||||
expect(saved().containsKey('zz_new_version'), isFalse);
|
||||
expect(saved()['explorerLayout'], AppSettings().explorerLayout);
|
||||
expect(saved()['explorerLayout'], AppSettings().explorer.layout);
|
||||
expect(secrets.values['nfs.pw.n1'], 'new-version-pw', reason: '[저장된 실제 비밀번호] 를 고르지 않으면 남김');
|
||||
// 저장된 실제 비밀번호 · API 키: 다른 판의 열쇠도 지움 · 마스터 (sec.) 는 남김
|
||||
await clearStoredSecrets(c);
|
||||
|
|
@ -327,9 +327,9 @@ void main() {
|
|||
});
|
||||
|
||||
test('최상으로 "앱 설정 전체" 를 지우면 처음 상태', () async {
|
||||
final c = controller()..settings.explorerLayout = 'single';
|
||||
final c = controller()..settings.explorer.layout = 'single';
|
||||
await resetAllSettings(c);
|
||||
expect(c.settings.explorerLayout, AppSettings().explorerLayout);
|
||||
expect(c.settings.explorer.layout, AppSettings().explorer.layout);
|
||||
expect(c.settings.webdavServers, isEmpty);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,13 +44,13 @@ void main() {
|
|||
|
||||
test('설정: 처음 값 Wi-Fi 에서만 · 캐시 200MB, 틀린 값은 처음 값으로', () {
|
||||
final s = AppSettings();
|
||||
expect((s.explorerRemoteThumbs, s.explorerRemoteThumbCacheMb), ('wifi', 200));
|
||||
expect(AppSettings.fromJson({'explorerRemoteThumbs': 'x', 'explorerRemoteThumbCacheMb': 'y'}).explorerRemoteThumbs, 'wifi');
|
||||
expect((s.explorer.remoteThumbs, s.explorer.remoteThumbCacheMb), ('wifi', 200));
|
||||
expect(AppSettings.fromJson({'explorerRemoteThumbs': 'x', 'explorerRemoteThumbCacheMb': 'y'}).explorer.remoteThumbs, 'wifi');
|
||||
final back = AppSettings.fromJson((AppSettings()
|
||||
..explorerRemoteThumbs = 'off'
|
||||
..explorerRemoteThumbCacheMb = 500)
|
||||
..explorer.remoteThumbs = 'off'
|
||||
..explorer.remoteThumbCacheMb = 500)
|
||||
.toJson());
|
||||
expect((back.explorerRemoteThumbs, back.explorerRemoteThumbCacheMb), ('off', 500));
|
||||
expect((back.explorer.remoteThumbs, back.explorer.remoteThumbCacheMb), ('off', 500));
|
||||
});
|
||||
|
||||
/// 파일 읽기 · 쓰기 (실제 IO) 를 기다린다 - [ok] 가 될 때까지 (5초까지)
|
||||
|
|
@ -140,9 +140,9 @@ void main() {
|
|||
activeMeteredOf = () async => false;
|
||||
resetRemoteThumbNetForTest();
|
||||
expect(await remoteThumbsAllowedNow(c), isTrue, reason: 'Wi-Fi (집 Wi-Fi 위의 Tailscale 포함)');
|
||||
c.settings.explorerRemoteThumbs = 'off';
|
||||
c.settings.explorer.remoteThumbs = 'off';
|
||||
expect(await remoteThumbsAllowedNow(c), isFalse);
|
||||
c.settings.explorerRemoteThumbs = 'always';
|
||||
c.settings.explorer.remoteThumbs = 'always';
|
||||
activeMeteredOf = () async => true;
|
||||
resetRemoteThumbNetForTest();
|
||||
expect(await remoteThumbsAllowedNow(c), isTrue);
|
||||
|
|
@ -196,7 +196,7 @@ void main() {
|
|||
expect(await (DavPath.parse('dav://nas/v.bin').client as DavClient).rangeSupported('/v.bin'), isFalse);
|
||||
final calls = <List<String>>[];
|
||||
final c = AppController(PlatformServices(mediaTool: _Ffmpeg(calls), storage: DesktopStorageService()));
|
||||
c.settings.explorerRemoteThumbs = 'always';
|
||||
c.settings.explorer.remoteThumbs = 'always';
|
||||
resetRemoteThumbsForTest();
|
||||
addTearDown(resetRemoteThumbsForTest);
|
||||
final h = await remoteThumbsOf(c);
|
||||
|
|
@ -247,7 +247,7 @@ void main() {
|
|||
await tool.runFfmpeg(['-y', '-f', 'lavfi', '-i', 'testsrc=size=320x240:rate=10', '-t', '7', '-pix_fmt', 'yuv420p', mp4]);
|
||||
server.ranges = true;
|
||||
final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService()));
|
||||
c.settings.explorerRemoteThumbs = 'always';
|
||||
c.settings.explorer.remoteThumbs = 'always';
|
||||
resetRemoteThumbsForTest();
|
||||
addTearDown(resetRemoteThumbsForTest);
|
||||
final h = await remoteThumbsOf(c);
|
||||
|
|
|
|||
|
|
@ -130,8 +130,8 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..rootEnabled = on
|
||||
..explorerPaths = [tmp.path, tmp.path]
|
||||
..explorerLayout = 'single';
|
||||
..explorer.paths = [tmp.path, tmp.path]
|
||||
..explorer.layout = 'single';
|
||||
return c;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ void main() {
|
|||
c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..webdavServers = [s]
|
||||
..explorerLayout = 'single'
|
||||
..explorer.layout = 'single'
|
||||
..rsyncPaths = [left, 'dav://nas/'];
|
||||
DavRegistry.configure([s]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ void main() {
|
|||
return const JsonEncoder.withIndent(' ').convert(sort(v));
|
||||
}
|
||||
|
||||
String roundTrip(String text) => canonical(AppSettings.fromJson(jsonDecode(text) as Map<String, dynamic>).toJson());
|
||||
/// 읽고 다시 쓴 것 + 읽을 때 정한 "바뀐 기본값 알림" 목록 (파일에는 없지만 읽기의 결과)
|
||||
String roundTrip(String text) {
|
||||
final s = AppSettings.fromJson(jsonDecode(text) as Map<String, dynamic>);
|
||||
return canonical({...s.toJson(), '__migrated': s.migrated});
|
||||
}
|
||||
|
||||
/// 모든 키의 값을 바꾼 입력 (bool 은 반대, 수는 +7, 글은 앞에 x_, 안쪽 묶음도) - 읽기의 검사 · 고침도 함께 지나게
|
||||
Object? change(Object? v) => switch (v) {
|
||||
|
|
@ -54,6 +58,16 @@ void main() {
|
|||
expect(canonical(AppSettings().toJson()), read(defFile));
|
||||
});
|
||||
|
||||
test('옛 판 설정 (옮김 표시 없음 · 예전 처음 값): 읽은 값과 바뀐 기본값 알림이 나누기 전 판과 같다', () {
|
||||
const legacy = {'explorerLayout': 'auto', 'explorerClick': 'open', 'explorerOrientation': 'auto', 'screenOrientation': 'landscape'};
|
||||
final outFile = File(p.join(fixtures, 'settings_golden_legacy_out.json'));
|
||||
if (write) {
|
||||
outFile.writeAsStringSync(roundTrip(jsonEncode(legacy)));
|
||||
return;
|
||||
}
|
||||
expect(roundTrip(jsonEncode(legacy)), outFile.readAsStringSync().replaceAll('\r\n', '\n'));
|
||||
});
|
||||
|
||||
final expectPath = Platform.environment['JJ_USER_SETTINGS_EXPECT'];
|
||||
test('사용자 설정 파일 (복사본): 읽고 쓰면 나누기 전 판과 같다', () {
|
||||
final user = File(p.join(Platform.environment['APPDATA'] ?? '', 'com.jj', 'jj_mkvmaker', 'settings.json'));
|
||||
|
|
|
|||
|
|
@ -142,16 +142,16 @@ void main() {
|
|||
});
|
||||
expect(old.screenOrientation, 'auto');
|
||||
// 규칙 7 · 결정 3 (10/22): 처음 배치가 두 창이 되어 예전 'dual' 은 그대로 (옮겼다는 알림 없음)
|
||||
expect(old.explorerLayout, 'dual');
|
||||
expect(old.explorer.layout, 'dual');
|
||||
expect(old.zipComic, isFalse);
|
||||
expect(old.migrated, containsAll(['orientation', 'zipComic']));
|
||||
expect(old.migrated, isNot(contains('explorerLayout')));
|
||||
// 다시 저장 · 읽기: 이제 사용자가 고른 값으로 보고 그대로 · 알림 없음
|
||||
final again = AppSettings.fromJson((old
|
||||
..explorerLayout = 'dual'
|
||||
..explorer.layout = 'dual'
|
||||
..screenOrientation = 'landscape')
|
||||
.toJson());
|
||||
expect(again.explorerLayout, 'dual');
|
||||
expect(again.explorer.layout, 'dual');
|
||||
expect(again.screenOrientation, 'landscape');
|
||||
expect(again.migrated, isEmpty);
|
||||
// 처음 설치 (빈 설정) 는 알릴 것 없음
|
||||
|
|
@ -166,16 +166,16 @@ void main() {
|
|||
'explorerRetapCollapse': false,
|
||||
'explorerClick': 'open',
|
||||
});
|
||||
expect([old.explorerLayout, old.explorerAlwaysMarks, old.explorerRetapCollapse], ['dual', false, true]);
|
||||
expect(old.explorerClick, Platform.isAndroid ? 'select' : 'open'); // Windows 의 'open' 은 사용자가 고른 값
|
||||
expect([old.explorer.layout, old.explorer.alwaysMarks, old.explorer.retapCollapse], ['dual', false, true]);
|
||||
expect(old.explorer.click, Platform.isAndroid ? 'select' : 'open'); // Windows 의 'open' 은 사용자가 고른 값
|
||||
expect(old.migrated, containsAll(['explorerLayoutRule', 'explorerMarksRule', 'explorerRetapRule']));
|
||||
// 저장한 뒤 사용자가 다시 고른 값은 그대로 · 다시 묻지 않음
|
||||
final again = AppSettings.fromJson((old
|
||||
..explorerLayout = 'auto'
|
||||
..explorerAlwaysMarks = true
|
||||
..explorerRetapCollapse = false)
|
||||
..explorer.layout = 'auto'
|
||||
..explorer.alwaysMarks = true
|
||||
..explorer.retapCollapse = false)
|
||||
.toJson());
|
||||
expect([again.explorerLayout, again.explorerAlwaysMarks, again.explorerRetapCollapse], ['auto', true, false]);
|
||||
expect([again.explorer.layout, again.explorer.alwaysMarks, again.explorer.retapCollapse], ['auto', true, false]);
|
||||
expect(again.migrated, isEmpty);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
await tester.runAsync(() => c.updateSettings((x) => x
|
||||
..sftpServers = [SftpServer(id: 's1', name: 'NAS', host: '127.0.0.1', port: s!.port, user: 'jj', password: 'pw')]
|
||||
..explorerPaths = ['sftp://s1/', local.path]));
|
||||
..explorer.paths = ['sftp://s1/', local.path]));
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
|
|||
|
|
@ -113,8 +113,8 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..shizukuEnabled = on
|
||||
..explorerPaths = [tmp.path, tmp.path]
|
||||
..explorerLayout = 'single';
|
||||
..explorer.paths = [tmp.path, tmp.path]
|
||||
..explorer.layout = 'single';
|
||||
return c;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ void useExplorerFixture() {
|
|||
_device = TestExplorerDevice();
|
||||
_c = AppController(PlatformServices(
|
||||
mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), shell: shell));
|
||||
c.settings.explorerPaths = [left, right];
|
||||
c.settings.explorer.paths = [left, right];
|
||||
});
|
||||
// Windows: 방금 닫은 파일 (65 의 잠금 시험) 이나 색인 · 백신이 잠깐 잡고 있으면 지우기가 실패한다 - 될 때까지 기다린다
|
||||
tearDown(() async {
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ void main() {
|
|||
..createSync(recursive: true)
|
||||
..writeAsStringSync('r');
|
||||
c.settings
|
||||
..explorerLayout = 'single'
|
||||
..explorer.layout = 'single'
|
||||
..rsyncOptions = '-avPog --delete'
|
||||
..rsyncPaths = [l, r];
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
|
|
@ -221,7 +221,7 @@ void main() {
|
|||
old.setLastModifiedSync(DateTime.now().subtract(const Duration(hours: 1)));
|
||||
final writing = File(p.join(dir, 'now.mkv.jjpart'))..writeAsStringSync('half'); // 지금 쓰는 중 (방금 바뀜)
|
||||
expect(isPartialFile('a.jjsync'), isTrue);
|
||||
c.settings.explorerPaths = [dir, dir];
|
||||
c.settings.explorer.paths = [dir, dir];
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
|
|
|||
|
|
@ -215,8 +215,8 @@ void main() {
|
|||
for (final on in [false, true]) {
|
||||
final c = ctl()
|
||||
..settings.vaultEnabled = on
|
||||
..settings.explorerPaths = [dir.path, dir.path]
|
||||
..settings.explorerLayout = 'split';
|
||||
..settings.explorer.paths = [dir.path, dir.path]
|
||||
..settings.explorer.layout = 'split';
|
||||
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), c: c))));
|
||||
await until(t, () => find.text('a.txt').evaluate().isNotEmpty);
|
||||
await t.longPress(find.text('a.txt').last);
|
||||
|
|
|
|||
Loading…
Reference in New Issue