WIP X-plore 비교 5-1 ① 종류별 기본 정렬 (관리자 지시 10/21): 환경 설정 > 파일 탐색기 "종류별 기본 정렬" (처음 끔 - 지금 정렬 그대로). 켜면 파일 중 반 넘게가 사진 · 동영상인 폴더는 날짜 (새것 먼저 - 내 판단: 갤러리처럼), 음악 폴더는 이름, 그 밖은 전체 정렬. 그 폴더에서 정렬을 바꾸면 그 폴더만 기억 (전체 정렬을 바꾸면 눌러도 안 바뀐 것처럼 보이므로) · 켜고 끄면 읽어 둔 목록을 다시 줄 세움 · 시험 (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>xplore_p3b
parent
a1780243cd
commit
81e5906f76
|
|
@ -1540,6 +1540,7 @@
|
|||
"사용자가 취소했습니다.": "Cancelled by the user.",
|
||||
"사용할 수 있는 자막 사이트가 없습니다.": "No subtitle site is available.",
|
||||
"사진": "Photos",
|
||||
"사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름으로. 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억합니다": "Folders that are mostly photos or videos sort by date (newest first), music folders by name. If you change the sort in such a folder, that folder remembers it",
|
||||
"사진 슬라이드쇼": "Photo slideshow",
|
||||
"사진 정보 (Exif)": "Photo info (Exif)",
|
||||
"사진에는 '사진' 모델이 더 낫지만, 이 기기 (그래픽카드 없음) 에서는 한 장에 약 20분 걸립니다.": "The 'Photos' model is better for photos, but on this device (no graphics card) it takes about 20 minutes per image.",
|
||||
|
|
@ -2411,6 +2412,7 @@
|
|||
"종료 창 닫힘": "Quit window closed",
|
||||
"종료할까요?": "Quit?",
|
||||
"종류 (확장자)": "Type (extension)",
|
||||
"종류별 기본 정렬": "Sort by content type",
|
||||
"좌 ← 우": "Left ← right",
|
||||
"좌 → 우": "Left → right",
|
||||
"좌 ⇄ 우": "Left ⇄ right",
|
||||
|
|
|
|||
|
|
@ -1540,6 +1540,7 @@
|
|||
"사용자가 취소했습니다.": "ユーザーがキャンセルしました。",
|
||||
"사용할 수 있는 자막 사이트가 없습니다.": "使用できる字幕サイトがありません。",
|
||||
"사진": "写真",
|
||||
"사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름으로. 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억합니다": "写真・動画が多いフォルダーは日付 (新しい順)、音楽が多いフォルダーは名前で並べます。そのフォルダーで並べ替えを変えると、そのフォルダーは変えた並べ替えを覚えます",
|
||||
"사진 슬라이드쇼": "写真スライドショー",
|
||||
"사진 정보 (Exif)": "写真情報 (Exif)",
|
||||
"사진에는 '사진' 모델이 더 낫지만, 이 기기 (그래픽카드 없음) 에서는 한 장에 약 20분 걸립니다.": "写真には「写真」モデルが適していますが、この端末 (グラフィックカードなし) では 1 枚に約 20 分かかります。",
|
||||
|
|
@ -2411,6 +2412,7 @@
|
|||
"종료 창 닫힘": "終了ウィンドウを閉じました",
|
||||
"종료할까요?": "終了しますか?",
|
||||
"종류 (확장자)": "種類 (拡張子)",
|
||||
"종류별 기본 정렬": "種類別の既定の並べ替え",
|
||||
"좌 ← 우": "左 ← 右",
|
||||
"좌 → 우": "左 → 右",
|
||||
"좌 ⇄ 우": "左 ⇄ 右",
|
||||
|
|
|
|||
|
|
@ -1540,6 +1540,7 @@
|
|||
"사용자가 취소했습니다.": "用户已取消。",
|
||||
"사용할 수 있는 자막 사이트가 없습니다.": "没有可用的字幕网站。",
|
||||
"사진": "照片",
|
||||
"사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름으로. 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억합니다": "照片和视频较多的文件夹按日期 (新的在前),音乐较多的文件夹按名称排序。在该文件夹中更改排序后,该文件夹会记住所选排序",
|
||||
"사진 슬라이드쇼": "照片幻灯片",
|
||||
"사진 정보 (Exif)": "照片信息 (Exif)",
|
||||
"사진에는 '사진' 모델이 더 낫지만, 이 기기 (그래픽카드 없음) 에서는 한 장에 약 20분 걸립니다.": "照片更适合使用“照片”模型,但在本设备 (无显卡) 上每张约需 20 分钟。",
|
||||
|
|
@ -2411,6 +2412,7 @@
|
|||
"종료 창 닫힘": "退出窗口已关闭",
|
||||
"종료할까요?": "要退出吗?",
|
||||
"종류 (확장자)": "类型 (扩展名)",
|
||||
"종류별 기본 정렬": "按类型默认排序",
|
||||
"좌 ← 우": "左 ← 右",
|
||||
"좌 → 우": "左 → 右",
|
||||
"좌 ⇄ 우": "左 ⇄ 右",
|
||||
|
|
|
|||
|
|
@ -469,6 +469,10 @@ class AppSettings {
|
|||
/// 84: 정렬을 폴더마다 기억 (처음: 끔 - 모든 폴더가 같은 정렬). 켜면 열 머리 · 정렬 창이 지금 폴더에만 쓰고 [explorerFolderSorts] 에 둔다
|
||||
bool explorerSortPerFolder = false;
|
||||
|
||||
/// X-plore 비교 5-1 ① (관리자 지시 10/21): 종류별 기본 정렬 - 사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름.
|
||||
/// 처음 끔 (지금 정렬 그대로). 켜도 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억한다 ([explorerFolderSorts])
|
||||
bool explorerSortByKind = false;
|
||||
|
||||
/// 84: 폴더 → "기준|desc" (예: "size|1"). 오래된 것부터 지워 300개까지
|
||||
Map<String, String> explorerFolderSorts = {};
|
||||
|
||||
|
|
@ -866,6 +870,7 @@ class AppSettings {
|
|||
'wifiShareTailscale': wifiShareTailscale,
|
||||
'wifiShareFolder': wifiShareFolder,
|
||||
'explorerSortPerFolder': explorerSortPerFolder,
|
||||
'explorerSortByKind': explorerSortByKind,
|
||||
'explorerFolderSorts': explorerFolderSorts,
|
||||
'explorerShowHidden': explorerShowHidden,
|
||||
'explorerHiddenItems': explorerHiddenItems,
|
||||
|
|
@ -1108,6 +1113,7 @@ class AppSettings {
|
|||
..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
|
||||
..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
|
||||
|
|
|
|||
|
|
@ -432,6 +432,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
}
|
||||
_resumeReload = AppLifecycleListener(onResume: () => unawaited(_reloadOpen(localOnly: true)));
|
||||
_hiddenSeen = c.settings.explorerHiddenItems;
|
||||
_kindSeen = c.settings.explorerSortByKind;
|
||||
c.addListener(_onSettings);
|
||||
_init();
|
||||
}
|
||||
|
|
@ -988,7 +989,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
|
||||
/// [dir] 의 목록을 정렬. 84: "폴더마다 기억" 이 켜져 있으면 그 폴더의 정렬 (없으면 전체 정렬)
|
||||
List<FileEntry> _sorted(List<FileEntry> list, String dir) {
|
||||
final (sortBy, desc) = _sortOf(dir);
|
||||
final (sortBy, desc) = _sortOf(dir, list: list);
|
||||
final by = SortBy.values.firstWhere((s) => s.name == sortBy, orElse: () => SortBy.name);
|
||||
return sortEntries(list, by, descending: desc, dirSize: (d) => _sizes.of(d.path)?.bytes);
|
||||
}
|
||||
|
|
@ -996,19 +997,43 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
static String _sortKey(String dir) => isDav(dir) ? vNorm(dir) : (Platform.isWindows ? dir.toLowerCase() : dir);
|
||||
|
||||
/// 84: [dir] 의 정렬 (기준, 거꾸로)
|
||||
(String, bool) _sortOf(String dir) {
|
||||
(String, bool) _sortOf(String dir, {List<FileEntry>? list}) {
|
||||
final s = c.settings;
|
||||
if (s.explorerSortPerFolder) {
|
||||
// 폴더마다 기억한 것 (종류별 기본 정렬을 켰을 때 그 폴더에서 바꾼 것도) 이 먼저
|
||||
if (s.explorerSortPerFolder || s.explorerSortByKind) {
|
||||
final v = s.explorerFolderSorts[_sortKey(dir)];
|
||||
if (v != null && v.contains('|')) return (v.split('|').first, v.endsWith('|1'));
|
||||
}
|
||||
if (s.explorerSortByKind) {
|
||||
final k = _kindSort(dir, list: list);
|
||||
if (k != null) return k;
|
||||
}
|
||||
return (s.explorerSort, s.explorerSortDesc);
|
||||
}
|
||||
|
||||
/// X-plore 비교 5-1 ① (관리자 지시 10/21): 종류별 기본 정렬 - 읽어 둔 목록의 파일 중 반 넘게가 사진 · 동영상이면 날짜 (새것 먼저 -
|
||||
/// 내 판단: 갤러리처럼), 음악이면 이름. 그 밖 · 아직 읽지 않은 폴더는 null (전체 정렬). [list] 지금 줄 세우는 목록 (없으면 읽어 둔 것)
|
||||
(String, bool)? _kindSort(String dir, {List<FileEntry>? list}) {
|
||||
if (list == null) {
|
||||
for (final p in _panes) {
|
||||
list = p.cache[dir];
|
||||
if (list != null) break;
|
||||
}
|
||||
}
|
||||
final files = [for (final e in list ?? const <FileEntry>[]) if (!e.isDir) e];
|
||||
if (files.isEmpty) return null;
|
||||
final media = files.where((e) => c.settings.imageExts.contains(e.ext) || isVideoFile(e.path)).length;
|
||||
if (media * 2 > files.length) return ('date', true);
|
||||
if (files.where((e) => isAudioFile(e.path)).length * 2 > files.length) return ('name', false);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 84: 정렬을 바꾼다 - "폴더마다 기억" 이면 [dir] 에만, 아니면 전체. 모든 창의 목록을 다시 줄 세운다
|
||||
Future<void> _setSort(String dir, String by, bool desc) async {
|
||||
// 종류별 기본 정렬이 그 폴더에 쓰이고 있으면 바꾼 정렬은 그 폴더에 (전체 정렬을 바꾸면 이 폴더는 그대로라 눌러도 안 바뀐 것처럼 보인다)
|
||||
final kindHere = c.settings.explorerSortByKind && _kindSort(dir) != null;
|
||||
await c.updateSettings((x) {
|
||||
if (x.explorerSortPerFolder) {
|
||||
if (x.explorerSortPerFolder || kindHere) {
|
||||
final m = Map<String, String>.of(x.explorerFolderSorts)..remove(_sortKey(dir));
|
||||
m[_sortKey(dir)] = '$by|${desc ? 1 : 0}';
|
||||
while (m.length > 300) {
|
||||
|
|
@ -1321,9 +1346,22 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
|
||||
/// 설정 화면에서 숨긴 목록을 바꾸면 (풀기) 바로 목록에 보이게
|
||||
List<String>? _hiddenSeen;
|
||||
/// 종류별 기본 정렬 (5-1 ①) 을 켜고 끈 것 - 바뀌면 읽어 둔 목록을 다시 줄 세운다
|
||||
bool? _kindSeen;
|
||||
|
||||
void _onSettings() {
|
||||
if (!mounted) return;
|
||||
if (_kindSeen != null && _kindSeen != c.settings.explorerSortByKind) {
|
||||
for (final pane in _panes) {
|
||||
for (final k in pane.cache.keys.toList()) {
|
||||
pane.cache[k] = _sorted(pane.cache[k]!, k);
|
||||
}
|
||||
pane.changed();
|
||||
}
|
||||
}
|
||||
_kindSeen = c.settings.explorerSortByKind;
|
||||
final now = c.settings.explorerHiddenItems;
|
||||
if (identical(now, _hiddenSeen) || !mounted) return;
|
||||
if (identical(now, _hiddenSeen)) return;
|
||||
_hiddenSeen = now;
|
||||
setState(() {});
|
||||
for (final q in _panes) {
|
||||
|
|
|
|||
|
|
@ -795,6 +795,15 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
onChanged: (v) => c.updateSettings((x) => x.explorerSortPerFolder = v),
|
||||
),
|
||||
),
|
||||
// X-plore 비교 5-1 ① (관리자 지시 10/21): 종류별 기본 정렬 (처음: 끔)
|
||||
SettingTile(
|
||||
title: Text(tr('종류별 기본 정렬')),
|
||||
subtitle: Text(tr('사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름으로. 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억합니다')),
|
||||
trailing: Switch(
|
||||
value: s.explorerSortByKind,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerSortByKind = v),
|
||||
),
|
||||
),
|
||||
// 80: 폴더 크기 (필요할 때 계산하고 이번 실행 동안 기억)
|
||||
SettingTile(
|
||||
title: Text(tr('폴더 크기')),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/settings.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// X-plore 비교 5-1 ① (관리자 지시 10/21): 종류별 기본 정렬 - 처음 끔 (지금 정렬 그대로), 켜면 사진 · 동영상 폴더는 날짜 (새것 먼저) ·
|
||||
/// 음악 폴더는 이름, 그 폴더에서 정렬을 바꾸면 그 폴더만 기억
|
||||
void main() {
|
||||
late Directory tmp;
|
||||
late String photos, music, docs;
|
||||
setUp(() {
|
||||
tmp = Directory.systemTemp.createTempSync('jj_sortkind_');
|
||||
photos = p.join(tmp.path, 'photos');
|
||||
music = p.join(tmp.path, 'music');
|
||||
docs = p.join(tmp.path, 'docs');
|
||||
// 사진: 이름 순과 날짜 순이 다르게 (c 가 가장 새것, a 가 가장 옛것)
|
||||
for (final (n, day) in [('a.jpg', 1), ('b.jpg', 3), ('c.mp4', 5), ('d.jpg', 2)]) {
|
||||
File(p.join(photos, n))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(n)
|
||||
..setLastModifiedSync(DateTime(2026, 1, day));
|
||||
}
|
||||
for (final (n, day) in [('z.mp3', 1), ('y.mp3', 5), ('x.mp3', 3)]) {
|
||||
File(p.join(music, n))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(n)
|
||||
..setLastModifiedSync(DateTime(2026, 1, day));
|
||||
}
|
||||
for (final (n, day) in [('q.txt', 1), ('p.txt', 5)]) {
|
||||
File(p.join(docs, n))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(n)
|
||||
..setLastModifiedSync(DateTime(2026, 1, day));
|
||||
}
|
||||
});
|
||||
tearDown(() => tmp.deleteSync(recursive: true));
|
||||
|
||||
Future<AppController> open(WidgetTester tester, String dir, String wait, {bool kind = false, String sort = 'name'}) async {
|
||||
tester.view.physicalSize = const Size(1400, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
const pp = MethodChannel('plugins.flutter.io/path_provider');
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, (_) async => tmp.path);
|
||||
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;
|
||||
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)));
|
||||
await tester.pump();
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
/// 오른쪽 목록의 이름을 위에서 아래 순서로 (Windows 모양은 확장자를 붙인 이름)
|
||||
List<String> order(WidgetTester tester, List<String> names) {
|
||||
final ys = {for (final n in names) n: tester.getTopLeft(find.text(n).last).dy};
|
||||
return names.toList()..sort((a, b) => ys[a]!.compareTo(ys[b]!));
|
||||
}
|
||||
|
||||
Future<void> close(WidgetTester tester) async {
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
test('설정: 처음 끔 · 저장 · 읽기 · 예전 설정 (값 없음) 은 끔', () {
|
||||
expect(AppSettings().explorerSortByKind, isFalse);
|
||||
expect(AppSettings.fromJson((AppSettings()..explorerSortByKind = true).toJson()).explorerSortByKind, isTrue);
|
||||
expect(AppSettings.fromJson({}).explorerSortByKind, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('끄면 (처음) 사진 폴더도 지금 정렬 (이름) 그대로', (tester) async {
|
||||
await open(tester, photos, 'a.jpg');
|
||||
expect(order(tester, ['a.jpg', 'b.jpg', 'c.mp4', 'd.jpg']), ['a.jpg', 'b.jpg', 'c.mp4', 'd.jpg']);
|
||||
await close(tester);
|
||||
});
|
||||
|
||||
testWidgets('켜면 사진 · 동영상 폴더는 날짜 (새것 먼저) · 그 폴더에서 [이름] 을 누르면 그 폴더만 기억 (전체 정렬은 그대로)', (tester) async {
|
||||
final c = await open(tester, photos, 'a.jpg', kind: true);
|
||||
expect(order(tester, ['a.jpg', 'b.jpg', 'c.mp4', 'd.jpg']), ['c.mp4', 'b.jpg', 'd.jpg', 'a.jpg']);
|
||||
// 열 머리 [이름] → 이 폴더는 이름 순, 전체 정렬 설정은 바뀌지 않음
|
||||
await tester.tap(find.byTooltip('이름 순으로 정렬'));
|
||||
for (var i = 0; i < 10; i++) {
|
||||
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
|
||||
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: '전체 정렬은 그대로');
|
||||
await close(tester);
|
||||
});
|
||||
|
||||
testWidgets('켜도 음악 폴더는 이름 · 그 밖의 폴더는 전체 정렬 (날짜로 정해 두었으면 날짜)', (tester) async {
|
||||
await open(tester, music, 'x.mp3', kind: true, sort: 'date');
|
||||
expect(order(tester, ['x.mp3', 'y.mp3', 'z.mp3']), ['x.mp3', 'y.mp3', 'z.mp3'], reason: '음악 = 이름');
|
||||
await close(tester);
|
||||
await open(tester, docs, 'p.txt', kind: true, sort: 'date');
|
||||
expect(order(tester, ['p.txt', 'q.txt']), ['q.txt', 'p.txt'], reason: '글 폴더는 전체 정렬 (날짜, 옛것 먼저)');
|
||||
await close(tester);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue