WIP 80 보충 (15 지적 · 관리자 판단 ① ②): 폴더 크기에서 링크 · junction 은 따라가지 않음 · 화면에서 사라진 자동 계산은 멈춤 (검토 대기)
① 링크 · junction 을 따라가 세던 것 (같은 것을 여러 번 세어 크게 나오고, 위 폴더를 가리키는 고리면 끝나지 않을 수 있었다 -
실제 예: wt_*\third_party (junction) · .plugin_symlinks):
- 크기를 셀 때 로컬 폴더는 list(followLinks: false) 로 직접 읽고, 심볼릭 링크 · junction 은 0 으로 세고 수만 센다.
"1.2GB · 링크 3개 제외" 로 보인다 (214 의 링크 처리와 같은 판단).
- 안전장치: 이미 센 폴더 (실제 경로) 는 다시 세지 않는다.
② 다른 폴더로 가도 기다리던 계산이 끝까지 돌던 것:
- 화면을 위해 걸어 둔 자동 계산은 그 폴더가 어느 창에도 보이지 않으면 대기 줄에서 빼고, 세는 중이면 멈춘다 (결과는 남기지 않음).
- 길게 누르기 [폴더 크기 계산] 으로 직접 시킨 것만 끝까지 - 아래 알림 [취소] · 같은 메뉴 "폴더 크기 계산 멈추기".
- WebDAV 는 자동으로 하지 않는다. 직접 시키면 "서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다" 를 알린다 (관리자 판단).
- 시험: folder_size_test (Windows 에 실제 junction · 심볼릭 링크 · 위 폴더 고리 junction, 화면이 바뀌면 자동 계산 멈춤 · 사용자가 시킨 것은 끝까지 · 취소).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음6
parent
1b988cd84c
commit
5007d30afa
|
|
@ -40,6 +40,8 @@
|
|||
"\"{0}\" 이 비어 있어, 맞추면 대상 \"{1}\" 의 아래 항목이 모두 지워집니다 (되돌릴 수 없음). 원본을 일부러 비운 경우에만 누르세요.": "\"{0}\" is empty, so syncing deletes all the items below from the target \"{1}\" (cannot be undone). Press only if you emptied the source on purpose.",
|
||||
"\"{0}\" 이동": "Move \"{0}\"",
|
||||
"\"{0}\" 찾기 · {1}개": "Search \"{0}\" · {1}",
|
||||
"\"{0}\" 크기를 계산합니다.": "Calculating the size of \"{0}\".",
|
||||
"\"{0}\" 크기를 계산합니다. 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다.": "Calculating the size of \"{0}\". Everything inside is read from the server, so this can take a while.",
|
||||
"\"동영상 추가\" 로 파일을 선택하세요": "Choose files with \"Add videos\"",
|
||||
"\"모든 파일에 대한 접근\" 권한이 없어 읽을 수 없습니다. 허용하면 저절로 다시 분석합니다.": "Can't read this without the \"All files access\" permission. It will be analyzed again automatically once you allow it.",
|
||||
"\"백그라운드로 실행\" 이 꺼져 있어 앱을 끝내면 작업이 멈춥니다.": "\"Run in background\" is off, so closing the app stops these tasks.",
|
||||
|
|
@ -420,6 +422,7 @@
|
|||
"{0} / {1}장": "{0} / {1} images",
|
||||
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2} (change at the top of the main screen)",
|
||||
"{0} · {1}배 · {2}": "{0} · {1}× · {2}",
|
||||
"{0} · 링크 {1}개 제외": "{0} · {1} links excluded",
|
||||
"{0} → {1} 번역 {2}%": "{0} → {1} translation {2}%",
|
||||
"{0} 는 이미 끝났습니다": "{0} has already finished",
|
||||
"{0} 라이선스": "{0} license",
|
||||
|
|
@ -2274,6 +2277,7 @@
|
|||
"폴더 이름에 쓸 수 없는 글자가 있습니다 (\\ / : * ? \" < > |)": "The folder name has characters that cannot be used (\\ / : * ? \" < > |)",
|
||||
"폴더 크기": "Folder size",
|
||||
"폴더 크기 계산": "Calculate folder size",
|
||||
"폴더 크기 계산 멈추기": "Stop calculating folder size",
|
||||
"폴더: {0}": "Folder: {0}",
|
||||
"폴더가 들어 있을 때": "When folders are included",
|
||||
"폴더가 아닙니다: {0}": "Not a folder: {0}",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@
|
|||
"\"{0}\" 이 비어 있어, 맞추면 대상 \"{1}\" 의 아래 항목이 모두 지워집니다 (되돌릴 수 없음). 원본을 일부러 비운 경우에만 누르세요.": "「{0}」が空のため、同期すると対象「{1}」の以下の項目がすべて削除されます (元に戻せません)。元フォルダーを意図的に空にした場合のみ押してください。",
|
||||
"\"{0}\" 이동": "「{0}」を移動",
|
||||
"\"{0}\" 찾기 · {1}개": "「{0}」を検索 · {1}件",
|
||||
"\"{0}\" 크기를 계산합니다.": "「{0}」のサイズを計算します。",
|
||||
"\"{0}\" 크기를 계산합니다. 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다.": "「{0}」のサイズを計算します。サーバーから中身をすべて読むため、時間がかかることがあります。",
|
||||
"\"동영상 추가\" 로 파일을 선택하세요": "「動画を追加」でファイルを選んでください",
|
||||
"\"모든 파일에 대한 접근\" 권한이 없어 읽을 수 없습니다. 허용하면 저절로 다시 분석합니다.": "「すべてのファイルへのアクセス」権限がないため読み取れません。許可すると自動的に再分析します。",
|
||||
"\"백그라운드로 실행\" 이 꺼져 있어 앱을 끝내면 작업이 멈춥니다.": "「バックグラウンドで実行」がオフなので、アプリを終了すると作業が止まります。",
|
||||
|
|
@ -420,6 +422,7 @@
|
|||
"{0} / {1}장": "{0} / {1} 枚",
|
||||
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(メイン画面の上部で変更)",
|
||||
"{0} · {1}배 · {2}": "{0}・{1} 倍・{2}",
|
||||
"{0} · 링크 {1}개 제외": "{0} · リンク {1} 件を除く",
|
||||
"{0} → {1} 번역 {2}%": "{0} → {1} 翻訳 {2}%",
|
||||
"{0} 는 이미 끝났습니다": "{0} はすでに終わりました",
|
||||
"{0} 라이선스": "{0} ライセンス",
|
||||
|
|
@ -2274,6 +2277,7 @@
|
|||
"폴더 이름에 쓸 수 없는 글자가 있습니다 (\\ / : * ? \" < > |)": "フォルダー名に使えない文字があります(\\ / : * ? \" < > |)",
|
||||
"폴더 크기": "フォルダーサイズ",
|
||||
"폴더 크기 계산": "フォルダーサイズを計算",
|
||||
"폴더 크기 계산 멈추기": "フォルダーサイズの計算を止める",
|
||||
"폴더: {0}": "フォルダー: {0}",
|
||||
"폴더가 들어 있을 때": "フォルダーを含むとき",
|
||||
"폴더가 아닙니다: {0}": "フォルダーではありません: {0}",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@
|
|||
"\"{0}\" 이 비어 있어, 맞추면 대상 \"{1}\" 의 아래 항목이 모두 지워집니다 (되돌릴 수 없음). 원본을 일부러 비운 경우에만 누르세요.": "“{0}”为空,同步后会删除目标“{1}”中的以下所有项目 (无法撤销)。仅在你有意清空源文件夹时按下。",
|
||||
"\"{0}\" 이동": "移动“{0}”",
|
||||
"\"{0}\" 찾기 · {1}개": "搜索\"{0}\" · {1}个",
|
||||
"\"{0}\" 크기를 계산합니다.": "正在计算“{0}”的大小。",
|
||||
"\"{0}\" 크기를 계산합니다. 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다.": "正在计算“{0}”的大小。需要从服务器读取其中所有内容,可能需要较长时间。",
|
||||
"\"동영상 추가\" 로 파일을 선택하세요": "请用“添加视频”选择文件",
|
||||
"\"모든 파일에 대한 접근\" 권한이 없어 읽을 수 없습니다. 허용하면 저절로 다시 분석합니다.": "没有“所有文件访问权限”,无法读取。允许后会自动重新分析。",
|
||||
"\"백그라운드로 실행\" 이 꺼져 있어 앱을 끝내면 작업이 멈춥니다.": "“后台运行”已关闭,退出应用会停止这些任务。",
|
||||
|
|
@ -420,6 +422,7 @@
|
|||
"{0} / {1}장": "{0} / {1} 张",
|
||||
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(在主界面顶部更改)",
|
||||
"{0} · {1}배 · {2}": "{0} · {1} 倍 · {2}",
|
||||
"{0} · 링크 {1}개 제외": "{0} · 已排除 {1} 个链接",
|
||||
"{0} → {1} 번역 {2}%": "{0} → {1} 翻译 {2}%",
|
||||
"{0} 는 이미 끝났습니다": "{0} 已经完成",
|
||||
"{0} 라이선스": "{0} 许可证",
|
||||
|
|
@ -2274,6 +2277,7 @@
|
|||
"폴더 이름에 쓸 수 없는 글자가 있습니다 (\\ / : * ? \" < > |)": "文件夹名称中有不能使用的字符(\\ / : * ? \" < > |)",
|
||||
"폴더 크기": "文件夹大小",
|
||||
"폴더 크기 계산": "计算文件夹大小",
|
||||
"폴더 크기 계산 멈추기": "停止计算文件夹大小",
|
||||
"폴더: {0}": "文件夹: {0}",
|
||||
"폴더가 들어 있을 때": "包含文件夹时",
|
||||
"폴더가 아닙니다: {0}": "不是文件夹: {0}",
|
||||
|
|
|
|||
|
|
@ -1,27 +1,65 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../l10n/tr.dart';
|
||||
import 'file_ops.dart';
|
||||
import 'vfs.dart';
|
||||
|
||||
/// 폴더 하나를 읽은 것: 안의 항목 (링크 · junction 은 빼고) · 뺀 링크 수
|
||||
typedef FolderListing = ({List<FileEntry> entries, int links});
|
||||
|
||||
/// 80: 폴더 크기 (안의 파일 크기 합) - 필요할 때 계산하고 이번 실행 동안 기억한다.
|
||||
/// 한 번에 하나씩 (디스크 · NAS 를 몰아 읽지 않게). 바뀐 폴더는 [forget] 으로 그 폴더와 위 폴더들을 지운다.
|
||||
/// 한 번에 하나씩 (디스크 · NAS 를 몰아 읽지 않게). 바뀐 폴더는 [forget] 으로 그 폴더 · 안쪽 · 위 폴더들을 지운다.
|
||||
/// 80 보충 (15 지적 · 관리자 판단):
|
||||
/// - 심볼릭 링크 · junction 은 따라가지 않고 0 으로 센다 (같은 것을 여러 번 세거나, 고리 링크면 끝나지 않았다). 뺀 수는 [FolderSize.links].
|
||||
/// 안전장치로 이미 센 폴더 (실제 경로) 는 다시 세지 않는다.
|
||||
/// - 화면을 위해 걸어 둔 자동 계산 ([request] 의 [forDir]) 은 그 폴더가 화면에서 사라지면 ([retainAuto]) 멈추고 뺀다.
|
||||
/// 사용자가 직접 시킨 것 ([forDir] 없음) 은 끝까지 하고 [cancel] 로만 멈춘다.
|
||||
class FolderSizes extends ChangeNotifier {
|
||||
final _known = <String, FolderSize>{};
|
||||
final _waiting = <String>[];
|
||||
final _queued = <String>{};
|
||||
|
||||
/// 기다리는 것: 경로 → 화면 폴더 (자동) · null (사용자가 시킴)
|
||||
final _waiting = <String, String?>{};
|
||||
bool _running = false;
|
||||
bool _disposed = false;
|
||||
|
||||
/// 지금 세는 것 · 멈추라는 표시
|
||||
String? _current;
|
||||
String? _currentFor;
|
||||
bool _stop = false;
|
||||
|
||||
/// 읽기 (시험에서 바꿔 끼움)
|
||||
final Future<List<FileEntry>> Function(String dir) list;
|
||||
final Future<FolderListing> Function(String dir) list;
|
||||
|
||||
FolderSizes({Future<List<FileEntry>> Function(String dir)? list}) : list = list ?? _vList;
|
||||
FolderSizes({Future<FolderListing> Function(String dir)? list}) : list = list ?? listForSize;
|
||||
|
||||
static Future<List<FileEntry>> _vList(String dir) async => [
|
||||
for (final e in await vList(dir, strict: true)) FileEntry(e.path, isDir: e.isDir, size: e.size, modified: e.modified),
|
||||
];
|
||||
/// 크기를 셀 때의 폴더 읽기: 로컬은 링크 · junction 을 따라가지 않고 (세지 않고 수만), WebDAV 는 그대로
|
||||
static Future<FolderListing> listForSize(String dir) async {
|
||||
if (isDav(dir)) {
|
||||
return (
|
||||
entries: [for (final e in await vList(dir, strict: true)) FileEntry(e.path, isDir: e.isDir, size: e.size, modified: e.modified)],
|
||||
links: 0,
|
||||
);
|
||||
}
|
||||
final out = <FileEntry>[];
|
||||
var links = 0;
|
||||
await for (final e in Directory(dir).list(followLinks: false)) {
|
||||
// Windows 의 junction 도 Dart 에서는 Link 로 나온다 (214 와 같은 판단)
|
||||
if (e is Link || FileSystemEntity.isLinkSync(e.path)) {
|
||||
links++;
|
||||
} else if (e is Directory) {
|
||||
out.add(FileEntry(e.path, isDir: true, modified: DateTime(0)));
|
||||
} else if (e is File) {
|
||||
try {
|
||||
final st = await e.stat();
|
||||
out.add(FileEntry(e.path, isDir: false, size: st.size, modified: st.modified));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
return (entries: out, links: links);
|
||||
}
|
||||
|
||||
static String _key(String path) => isDav(path) ? vNorm(path) : path.toLowerCase();
|
||||
|
||||
|
|
@ -29,20 +67,51 @@ class FolderSizes extends ChangeNotifier {
|
|||
FolderSize? of(String path) => _known[_key(path)];
|
||||
|
||||
/// 계산 중이거나 기다리는 중인지
|
||||
bool busy(String path) => _queued.contains(_key(path));
|
||||
bool busy(String path) => _waiting.containsKey(_key(path)) || (_current != null && _key(_current!) == _key(path));
|
||||
|
||||
/// 계산해 두기 (이미 알거나 기다리는 중이면 그대로)
|
||||
void request(String path) {
|
||||
/// 계산해 두기 (이미 알거나 기다리는 중이면 그대로). [forDir]: 그 폴더를 보여 주려고 자동으로 건 것 (화면에서 사라지면 멈춤).
|
||||
/// 없으면 사용자가 직접 시킨 것 - 이미 자동으로 기다리던 것이면 끝까지 하도록 바꾼다
|
||||
void request(String path, {String? forDir}) {
|
||||
final k = _key(path);
|
||||
if (_known.containsKey(k) || _queued.contains(k)) return;
|
||||
_queued.add(k);
|
||||
_waiting.add(path);
|
||||
if (_known.containsKey(k)) return;
|
||||
if (_current != null && _key(_current!) == k) {
|
||||
if (forDir == null) _currentFor = null;
|
||||
return;
|
||||
}
|
||||
if (_waiting.containsKey(k)) {
|
||||
if (forDir == null) _waiting[k] = null;
|
||||
return;
|
||||
}
|
||||
_waiting[k] = forDir;
|
||||
_paths[k] = path;
|
||||
notifyListeners();
|
||||
// 화면을 그리는 중 (프레임 뒤 콜백) 에 맡겨도 그 영역에 묶이지 않게 바깥 영역에서 읽는다
|
||||
// (위젯 시험의 가짜 시간 영역에서는 실제 파일 읽기가 끝나지 않았다)
|
||||
Zone.root.run(() => unawaited(_pump()));
|
||||
}
|
||||
|
||||
final _paths = <String, String>{};
|
||||
|
||||
/// 화면에 보이는 폴더들 ([dirs]) 을 위한 것이 아닌 자동 계산은 빼고, 세는 중이면 멈춘다 (알리지 않음 - 화면을 그리는 중에 부른다)
|
||||
void retainAuto(Iterable<String> dirs) {
|
||||
final keep = {for (final d in dirs) _key(d)};
|
||||
_waiting.removeWhere((k, f) {
|
||||
final drop = f != null && !keep.contains(_key(f));
|
||||
if (drop) _paths.remove(k);
|
||||
return drop;
|
||||
});
|
||||
if (_currentFor != null && !keep.contains(_key(_currentFor!))) _stop = true;
|
||||
}
|
||||
|
||||
/// 사용자가 멈춤 (기다리는 것 · 세는 것 모두)
|
||||
void cancel(String path) {
|
||||
final k = _key(path);
|
||||
_waiting.remove(k);
|
||||
_paths.remove(k);
|
||||
if (_current != null && _key(_current!) == k) _stop = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 이 폴더 · 그 안의 폴더들 · 위 폴더들의 크기를 잊는다 (안의 것이 바뀜 - 새로 고침 · 복사 · 이동 · 지우기)
|
||||
void forget(String path) {
|
||||
final k = _key(path);
|
||||
|
|
@ -64,11 +133,17 @@ class FolderSizes extends ChangeNotifier {
|
|||
_running = true;
|
||||
try {
|
||||
while (_waiting.isNotEmpty && !_disposed) {
|
||||
final path = _waiting.removeAt(0);
|
||||
final k = _waiting.keys.first;
|
||||
final forDir = _waiting.remove(k);
|
||||
final path = _paths.remove(k) ?? k;
|
||||
_current = path;
|
||||
_currentFor = forDir;
|
||||
_stop = false;
|
||||
final size = await _measure(path);
|
||||
_current = null;
|
||||
_currentFor = null;
|
||||
if (_disposed) return;
|
||||
_queued.remove(_key(path));
|
||||
_known[_key(path)] = size;
|
||||
if (size != null) _known[k] = size;
|
||||
notifyListeners();
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -76,22 +151,36 @@ class FolderSizes extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
/// 안의 파일 크기 합. 읽지 못한 폴더가 있으면 [FolderSize.partial] (화면에 "+")
|
||||
Future<FolderSize> _measure(String root) async {
|
||||
/// 안의 파일 크기 합. 읽지 못한 폴더가 있으면 [FolderSize.partial] (화면에 "+"). 멈추면 null
|
||||
Future<FolderSize?> _measure(String root) async {
|
||||
var bytes = 0;
|
||||
var partial = false;
|
||||
var links = 0;
|
||||
final seen = <String>{};
|
||||
final pending = [root];
|
||||
while (pending.isNotEmpty && !_disposed) {
|
||||
while (pending.isNotEmpty) {
|
||||
if (_disposed || _stop) return null;
|
||||
final dir = pending.removeLast();
|
||||
// 안전장치: 이미 센 폴더 (실제 경로) 는 다시 세지 않는다
|
||||
var real = dir;
|
||||
if (!isDav(dir)) {
|
||||
try {
|
||||
real = Directory(dir).resolveSymbolicLinksSync();
|
||||
} catch (_) {}
|
||||
}
|
||||
if (!seen.add(_key(real))) continue;
|
||||
// 안쪽 폴더를 이미 알면 다시 읽지 않는다
|
||||
final known = dir == root ? null : _known[_key(dir)];
|
||||
if (known != null) {
|
||||
bytes += known.bytes;
|
||||
partial |= known.partial;
|
||||
links += known.links;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
for (final e in await list(dir)) {
|
||||
final r = await list(dir);
|
||||
links += r.links;
|
||||
for (final e in r.entries) {
|
||||
if (e.isDir) {
|
||||
pending.add(e.path);
|
||||
} else if (e.size > 0) {
|
||||
|
|
@ -102,7 +191,7 @@ class FolderSizes extends ChangeNotifier {
|
|||
partial = true;
|
||||
}
|
||||
}
|
||||
return FolderSize(bytes, partial: partial);
|
||||
return FolderSize(bytes, partial: partial, links: links);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -117,8 +206,14 @@ class FolderSize {
|
|||
|
||||
/// 읽지 못한 폴더가 있어 실제보다 작을 수 있음
|
||||
final bool partial;
|
||||
const FolderSize(this.bytes, {this.partial = false});
|
||||
|
||||
/// "1.2GB" · 읽지 못한 곳이 있으면 "1.2GB+"
|
||||
String get label => '${formatSize(bytes)}${partial ? '+' : ''}';
|
||||
/// 세지 않은 링크 · junction 수
|
||||
final int links;
|
||||
const FolderSize(this.bytes, {this.partial = false, this.links = 0});
|
||||
|
||||
/// "1.2GB" · 읽지 못한 곳이 있으면 "1.2GB+" · 링크가 있으면 "1.2GB · 링크 3개 제외"
|
||||
String get label {
|
||||
final s = '${formatSize(bytes)}${partial ? '+' : ''}';
|
||||
return links > 0 ? trf('{0} · 링크 {1}개 제외', [s, links]) : s;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -339,9 +339,13 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
if (s != null) return s.label;
|
||||
if (_sizes.busy(e.path)) return '…';
|
||||
final mode = c.settings.explorerFolderSize;
|
||||
if ((mode == 'always' || (mode == 'sort' && _sortOf(pane.current).$1 == 'size')) && samePath(vDirname(e.path), pane.current)) {
|
||||
// 80 보충 (관리자 판단): WebDAV 는 자동으로 하지 않는다 (서버에서 안의 것을 모두 읽음 - 길게 누르기로만)
|
||||
if ((mode == 'always' || (mode == 'sort' && _sortOf(pane.current).$1 == 'size')) &&
|
||||
samePath(vDirname(e.path), pane.current) &&
|
||||
!isDav(e.path)) {
|
||||
final forDir = pane.current;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _sizes.request(e.path);
|
||||
if (mounted) _sizes.request(e.path, forDir: forDir);
|
||||
});
|
||||
return '…';
|
||||
}
|
||||
|
|
@ -1390,7 +1394,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
if (_selecting && _canRange(pane, e)) ('range', Icons.unfold_more, tr('여기까지 범위 고르기')),
|
||||
if (e.isDir) ('open', Icons.folder_open, tr('열기')),
|
||||
// 80: 이 폴더 크기를 지금 계산 (설정이 "안 함" 이어도)
|
||||
if (e.isDir) ('folderSize', Icons.data_usage, tr('폴더 크기 계산')),
|
||||
if (e.isDir) ('folderSize', Icons.data_usage, _sizes.busy(e.path) ? tr('폴더 크기 계산 멈추기') : tr('폴더 크기 계산')),
|
||||
if (e.isDir && dual) ('openOther', Icons.vertical_split_outlined, tr('다른 창에서 열기')),
|
||||
if (e.isDir) ('playFolder', Icons.play_circle_outline, tr('이 폴더의 동영상 재생')),
|
||||
if (video || audio) ('playInternal', Icons.play_circle_outline, tr('내장 플레이어로 재생')),
|
||||
|
|
@ -1477,9 +1481,22 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
setState(() => _selecting = true);
|
||||
if (!_isMarked(pane, e.path)) await _toggleMark(pane, e);
|
||||
case 'folderSize':
|
||||
if (_sizes.busy(e.path)) {
|
||||
_sizes.cancel(e.path);
|
||||
break;
|
||||
}
|
||||
// 사용자가 직접 시킨 것: 다른 폴더로 가도 끝까지 (멈추기는 아래 알림 · 같은 메뉴)
|
||||
_sizes
|
||||
..forget(e.path)
|
||||
..request(e.path);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(isDav(e.path)
|
||||
? trf('"{0}" 크기를 계산합니다. 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다.', [e.name])
|
||||
: trf('"{0}" 크기를 계산합니다.', [e.name])),
|
||||
action: SnackBarAction(label: tr('취소'), onPressed: () => _sizes.cancel(e.path)),
|
||||
));
|
||||
}
|
||||
case 'rename':
|
||||
if (_isMarked(pane, e.path) && pane.marked.length > 1) {
|
||||
await _batchRename(pane, pane.marked.toList());
|
||||
|
|
@ -3192,6 +3209,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
listenable: c,
|
||||
builder: (context, _) {
|
||||
_dropRemovedServers();
|
||||
// 80 보충: 화면에서 사라진 폴더를 위해 걸어 둔 자동 크기 계산은 멈추고 뺀다
|
||||
_sizes.retainAuto([for (final x in _panes) x.current]);
|
||||
return Column(children: [
|
||||
_topBar(),
|
||||
if (_searchBack != null) _searchBackBar(),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
|
@ -5,7 +6,8 @@ import 'package:jj_mkvmaker/core/file_ops.dart';
|
|||
import 'package:jj_mkvmaker/core/folder_size.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// 80: 폴더 크기 - 필요할 때 계산 · 기억, 읽지 못한 곳은 "+", 바뀌면 그 폴더와 위 폴더를 잊음, 크기순 정렬
|
||||
/// 80: 폴더 크기 - 필요할 때 계산 · 기억, 읽지 못한 곳은 "+", 바뀌면 그 폴더와 위 폴더를 잊음, 크기순 정렬.
|
||||
/// 80 보충: 링크 · junction 은 따라가지 않음 (고리도 끝남), 화면에서 사라진 자동 계산은 멈춤, 사용자가 시킨 것은 [cancel] 로만
|
||||
void main() {
|
||||
late Directory tmp;
|
||||
setUp(() {
|
||||
|
|
@ -23,16 +25,16 @@ void main() {
|
|||
tearDown(() => tmp.deleteSync(recursive: true));
|
||||
|
||||
Future<void> idle(FolderSizes s, String path) async {
|
||||
for (var i = 0; i < 200 && s.of(path) == null; i++) {
|
||||
for (var i = 0; i < 300 && s.of(path) == null; i++) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
}
|
||||
}
|
||||
|
||||
test('안쪽 폴더까지 합 · 기억 · 바뀌면 그 폴더와 위 폴더를 잊음', () async {
|
||||
test('안쪽 폴더까지 합 · 기억 · 바뀌면 그 폴더 · 안쪽 · 위 폴더를 잊음', () async {
|
||||
var reads = 0;
|
||||
final s = FolderSizes(list: (d) {
|
||||
reads++;
|
||||
return listEntries(d, showHidden: true);
|
||||
return FolderSizes.listForSize(d);
|
||||
});
|
||||
final a = p.join(tmp.path, 'a');
|
||||
s.request(a);
|
||||
|
|
@ -64,7 +66,7 @@ void main() {
|
|||
final a = p.join(tmp.path, 'a');
|
||||
final s = FolderSizes(list: (d) {
|
||||
if (p.basename(d) == 'deep') throw const FileSystemException('권한 없음');
|
||||
return listEntries(d, showHidden: true);
|
||||
return FolderSizes.listForSize(d);
|
||||
});
|
||||
s.request(a);
|
||||
await idle(s, a);
|
||||
|
|
@ -73,6 +75,71 @@ void main() {
|
|||
s.dispose();
|
||||
});
|
||||
|
||||
group('링크 · junction (Windows)', () {
|
||||
Future<bool> junction(String link, String target) async =>
|
||||
(await Process.run('cmd', ['/c', 'mklink', '/J', link, target])).exitCode == 0;
|
||||
|
||||
test('junction · 심볼릭 링크는 따라가지 않고 0 · "링크 n개 제외", 위 폴더를 가리키는 고리도 끝난다', () async {
|
||||
final a = p.join(tmp.path, 'a');
|
||||
// b (5B) 를 가리키는 junction, 위 폴더 (tmp) 를 가리키는 고리 junction
|
||||
expect(await junction(p.join(a, 'to_b'), p.join(tmp.path, 'b')), isTrue);
|
||||
expect(await junction(p.join(a, 'deep', 'loop'), tmp.path), isTrue);
|
||||
var symlink = false;
|
||||
try {
|
||||
Link(p.join(a, 'sym_b')).createSync(p.join(tmp.path, 'b'));
|
||||
symlink = true;
|
||||
} catch (_) {} // 개발자 모드가 아니면 심볼릭 링크를 만들 수 없다 - junction 만 본다
|
||||
final s = FolderSizes();
|
||||
s.request(a);
|
||||
await idle(s, a);
|
||||
expect(s.of(a)!.bytes, 1024, reason: '링크가 가리키는 b (5B) 를 더하지 않는다');
|
||||
expect(s.of(a)!.links, symlink ? 3 : 2);
|
||||
expect(s.of(a)!.label, '1KB · 링크 ${symlink ? 3 : 2}개 제외');
|
||||
s.dispose();
|
||||
// tearDown 의 지우기가 junction 을 따라가 b · tmp 를 지우지 않게 링크부터 지운다
|
||||
for (final l in [p.join(a, 'to_b'), p.join(a, 'deep', 'loop'), if (symlink) p.join(a, 'sym_b')]) {
|
||||
Link(l).deleteSync();
|
||||
}
|
||||
expect(File(p.join(tmp.path, 'b', 'z.bin')).existsSync(), isTrue);
|
||||
});
|
||||
}, skip: !Platform.isWindows);
|
||||
|
||||
test('화면에서 사라진 폴더의 자동 계산은 멈추고 빠진다 · 사용자가 시킨 것은 남는다 · [cancel]', () async {
|
||||
final gate = Completer<void>();
|
||||
final read = <String>[];
|
||||
final s = FolderSizes(list: (d) async {
|
||||
read.add(p.basename(d));
|
||||
await gate.future;
|
||||
return FolderSizes.listForSize(d);
|
||||
});
|
||||
final a = p.join(tmp.path, 'a'), b = p.join(tmp.path, 'b');
|
||||
s.request(a, forDir: tmp.path); // 세는 중 (gate 에서 기다림)
|
||||
s.request(b, forDir: tmp.path); // 기다리는 중
|
||||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||
expect(s.busy(a) && s.busy(b), isTrue);
|
||||
// 다른 폴더로 감
|
||||
s.retainAuto([p.join(tmp.path, 'elsewhere')]);
|
||||
expect(s.busy(b), isFalse, reason: '기다리던 것은 빠진다');
|
||||
gate.complete();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
expect(s.of(a), isNull, reason: '세던 것은 멈추고 남기지 않는다');
|
||||
expect(read, isNot(contains('b')));
|
||||
|
||||
// 사용자가 시킨 것은 화면이 바뀌어도 끝까지
|
||||
s.request(b);
|
||||
s.retainAuto([p.join(tmp.path, 'elsewhere')]);
|
||||
await idle(s, b);
|
||||
expect(s.of(b)!.bytes, 5);
|
||||
|
||||
// [cancel]
|
||||
s.request(a);
|
||||
s.cancel(a);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
expect(s.of(a), isNull);
|
||||
expect(s.busy(a), isFalse);
|
||||
s.dispose();
|
||||
});
|
||||
|
||||
test('크기순 정렬: 아는 폴더는 그 크기로, 모르는 폴더는 가장 작게 (폴더는 늘 위)', () {
|
||||
final now = DateTime(2026);
|
||||
final list = [
|
||||
|
|
|
|||
Loading…
Reference in New Issue