파일 탐색기 즐겨찾기 (73): 지금 폴더 넣기 · 창 위에서 한 번에 · Rsync 짝
- 창 맨 위 줄 맨 앞 ★ (즐겨찾기한 폴더면 채움) · 버튼 줄 [즐겨찾기] (처음 값 순서 · Rsync 버튼 줄에도). - 즐겨찾기 창: 누르면 그 창이 그 폴더로, [지금 폴더 추가] (표시 이름 고침), 이름 바꾸기 · 빼기 (폴더는 그대로), 바로 가기 (다운로드 · MKV 결과 폴더). - Rsync 화면: [지금 짝 추가] 로 "왼쪽 ⇄ 오른쪽", 누르면 두 창이 함께. 설정 folderBookmarks. - explorer_test (넣기 · 가기 · 빼기 · Rsync 짝 · 저장). 버튼이 늘어 기존 시험 두 곳 고침 (처음 값 순서 · [더 보기] 안의 [붙여넣기]). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>fix_mkv
parent
f86697c634
commit
fe4ca6adb2
|
|
@ -156,6 +156,7 @@
|
|||
"MKV · AI 작업": "MKV · AI task",
|
||||
"MKV · 자막 (jj_mkv)": "MKV · subtitles (jj_mkv)",
|
||||
"MKV · 자막 저장 위치": "Where to save MKV · subtitles",
|
||||
"MKV 결과 폴더": "MKV output folder",
|
||||
"MKV 만들기": "Make MKV",
|
||||
"MKV 만들기 (대기열에 추가)": "Make MKV (add to queue)",
|
||||
"MKV 만들기 {0}개": "Make {0} MKVs",
|
||||
|
|
@ -1174,6 +1175,7 @@
|
|||
"아이콘 · 목록 모양: X-plore (썸네일 · 두 줄) · Windows 탐색기 (컬러 아이콘 · 열) · Total Commander (촘촘한 목록 · [폴더])": "Icons · list look: X-plore (thumbnails · two lines) · Windows Explorer (color icons · columns) · Total Commander (dense list · [folders])",
|
||||
"아직 만든 MKV · 자막이 없습니다: {0}": "No MKV · subtitles made yet: {0}",
|
||||
"아직 실행 안 함": "Not run yet",
|
||||
"아직 없습니다. 아래에서 지금 폴더를 넣으세요.": "None yet. Add the current folder below.",
|
||||
"아직 없습니다. 영상을 다 받으면 자동으로 들어옵니다 (설정에서 변경).": "None yet. Videos come in automatically once downloaded (change in Settings).",
|
||||
"아직 없음 (rsync 로 복사할 때 · 아래 [내려받기] 로 받습니다)": "Not yet (downloaded when you copy with rsync · or press [Download])",
|
||||
"아직 저장하지 못했습니다. 잠시 뒤 다시 시도하세요": "Still couldn't save. Please try again in a moment",
|
||||
|
|
@ -1456,6 +1458,7 @@
|
|||
"이동할 폴더를 만들 수 없습니다: {0} ({1})": "Cannot create the move folder: {0} ({1})",
|
||||
"이름": "Name",
|
||||
"이름 (탭에 보임, 비우면 주소)": "Name (shown on the tab; address if empty)",
|
||||
"이름 바꾸기": "Rename",
|
||||
"이름 바꾸기 (예: 이름 (2))": "Keep both (e.g. name (2))",
|
||||
"이름 바꾸기 · 이동": "Rename · move",
|
||||
"이름 변경": "Rename",
|
||||
|
|
@ -1693,6 +1696,7 @@
|
|||
"즐겨찾기 추가…": "Add bookmark…",
|
||||
"즐겨찾기 추가됨": "Bookmark added",
|
||||
"즐겨찾기 표시줄": "Bookmarks bar",
|
||||
"즐겨찾기에서 빼기 (폴더는 그대로)": "Remove from favorites (the folder stays)",
|
||||
"지금": "Current",
|
||||
"지금 {0}%": "Now {0}%",
|
||||
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "The running copy uses the previous options. Stop it and run again with the new options?",
|
||||
|
|
@ -1708,6 +1712,10 @@
|
|||
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "Current version: v{0} → new version: v{1}",
|
||||
"지금 작업 취소: {0}": "Cancelled current job: {0}",
|
||||
"지금 작업만 (대기 {0}개는 계속)": "Current job only ({0} waiting continue)",
|
||||
"지금 짝 추가": "Add current pair",
|
||||
"지금 짝을 즐겨찾기에": "Add the current pair to favorites",
|
||||
"지금 폴더 추가": "Add current folder",
|
||||
"지금 폴더를 즐겨찾기에": "Add the current folder to favorites",
|
||||
"지금: {0}": "Now: {0}",
|
||||
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "This is the Dual App (cloned app). Android may not grant this permission to dual apps. If you can't turn it on, open the original JJ_MKVMaker icon without the badge.",
|
||||
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "The last run did not end normally (last check {0}:{1}). It has been noted in the job log.",
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@
|
|||
"MKV · AI 작업": "MKV・AI 作業",
|
||||
"MKV · 자막 (jj_mkv)": "MKV・字幕(jj_mkv)",
|
||||
"MKV · 자막 저장 위치": "MKV・字幕の保存先",
|
||||
"MKV 결과 폴더": "MKV 出力フォルダー",
|
||||
"MKV 만들기": "MKV 作成",
|
||||
"MKV 만들기 (대기열에 추가)": "MKV 作成(キューに追加)",
|
||||
"MKV 만들기 {0}개": "MKV 作成 {0}個",
|
||||
|
|
@ -1174,6 +1175,7 @@
|
|||
"아이콘 · 목록 모양: X-plore (썸네일 · 두 줄) · Windows 탐색기 (컬러 아이콘 · 열) · Total Commander (촘촘한 목록 · [폴더])": "アイコン · 一覧の見た目: X-plore (サムネイル · 2 行) · Windows エクスプローラー (カラーアイコン · 列) · Total Commander (詰めた一覧 · [フォルダー])",
|
||||
"아직 만든 MKV · 자막이 없습니다: {0}": "まだ作成した MKV・字幕がありません: {0}",
|
||||
"아직 실행 안 함": "まだ実行していません",
|
||||
"아직 없습니다. 아래에서 지금 폴더를 넣으세요.": "まだありません。下で今のフォルダーを追加してください。",
|
||||
"아직 없습니다. 영상을 다 받으면 자동으로 들어옵니다 (설정에서 변경).": "まだありません。動画のダウンロードが終わると自動で入ります(設定で変更)。",
|
||||
"아직 없음 (rsync 로 복사할 때 · 아래 [내려받기] 로 받습니다)": "まだありません (rsync でコピーするとき · [ダウンロード] で入手)",
|
||||
"아직 저장하지 못했습니다. 잠시 뒤 다시 시도하세요": "まだ保存できませんでした。しばらくしてからもう一度お試しください",
|
||||
|
|
@ -1456,6 +1458,7 @@
|
|||
"이동할 폴더를 만들 수 없습니다: {0} ({1})": "移動フォルダーを作成できません: {0}({1})",
|
||||
"이름": "名前",
|
||||
"이름 (탭에 보임, 비우면 주소)": "名前 (タブに表示、空ならアドレス)",
|
||||
"이름 바꾸기": "名前を変更",
|
||||
"이름 바꾸기 (예: 이름 (2))": "名前を変える (例: 名前 (2))",
|
||||
"이름 바꾸기 · 이동": "名前の変更・移動",
|
||||
"이름 변경": "名前の変更",
|
||||
|
|
@ -1693,6 +1696,7 @@
|
|||
"즐겨찾기 추가…": "ブックマークを追加…",
|
||||
"즐겨찾기 추가됨": "ブックマークを追加しました",
|
||||
"즐겨찾기 표시줄": "ブックマークバー",
|
||||
"즐겨찾기에서 빼기 (폴더는 그대로)": "お気に入りから外す (フォルダーはそのまま)",
|
||||
"지금": "現在",
|
||||
"지금 {0}%": "現在 {0}%",
|
||||
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "実行中のコピーは前のオプションで動いています。止めて新しいオプションで再実行しますか?",
|
||||
|
|
@ -1708,6 +1712,10 @@
|
|||
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "現在のバージョン: v{0} → 新しいバージョン: v{1}",
|
||||
"지금 작업 취소: {0}": "現在の作業をキャンセル: {0}",
|
||||
"지금 작업만 (대기 {0}개는 계속)": "現在の作業だけ (待機 {0} 件は続行)",
|
||||
"지금 짝 추가": "今のペアを追加",
|
||||
"지금 짝을 즐겨찾기에": "今のペアをお気に入りに",
|
||||
"지금 폴더 추가": "今のフォルダーを追加",
|
||||
"지금 폴더를 즐겨찾기에": "今のフォルダーをお気に入りに",
|
||||
"지금: {0}": "現在: {0}",
|
||||
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "これはデュアルアプリ (複製したアプリ) です。Android はデュアルアプリにこの権限を与えない場合があります。許可画面でオンにできない場合は、バッジのない元の JJ_MKVMaker アイコンから開いてください。",
|
||||
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "前回の実行が正常に終了しませんでした(最終確認 {0}:{1})。作業記録に残しました。",
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@
|
|||
"MKV · AI 작업": "MKV · AI 任务",
|
||||
"MKV · 자막 (jj_mkv)": "MKV · 字幕(jj_mkv)",
|
||||
"MKV · 자막 저장 위치": "MKV · 字幕保存位置",
|
||||
"MKV 결과 폴더": "MKV 输出文件夹",
|
||||
"MKV 만들기": "制作 MKV",
|
||||
"MKV 만들기 (대기열에 추가)": "制作 MKV(加入队列)",
|
||||
"MKV 만들기 {0}개": "制作 MKV {0} 个",
|
||||
|
|
@ -1174,6 +1175,7 @@
|
|||
"아이콘 · 목록 모양: X-plore (썸네일 · 두 줄) · Windows 탐색기 (컬러 아이콘 · 열) · Total Commander (촘촘한 목록 · [폴더])": "图标 · 列表外观: X-plore (缩略图 · 两行) · Windows 资源管理器 (彩色图标 · 列) · Total Commander (紧凑列表 · [文件夹])",
|
||||
"아직 만든 MKV · 자막이 없습니다: {0}": "尚未制作 MKV · 字幕: {0}",
|
||||
"아직 실행 안 함": "尚未运行",
|
||||
"아직 없습니다. 아래에서 지금 폴더를 넣으세요.": "还没有。请在下方添加当前文件夹。",
|
||||
"아직 없습니다. 영상을 다 받으면 자동으로 들어옵니다 (설정에서 변경).": "暂无。视频下载完成后会自动加入(可在设置中更改)。",
|
||||
"아직 없음 (rsync 로 복사할 때 · 아래 [내려받기] 로 받습니다)": "尚未安装 (用 rsync 复制时 · 或按 [下载] 获取)",
|
||||
"아직 저장하지 못했습니다. 잠시 뒤 다시 시도하세요": "仍未能保存。请稍后再试",
|
||||
|
|
@ -1456,6 +1458,7 @@
|
|||
"이동할 폴더를 만들 수 없습니다: {0} ({1})": "无法创建移动目标文件夹: {0}({1})",
|
||||
"이름": "名称",
|
||||
"이름 (탭에 보임, 비우면 주소)": "名称 (显示在标签上,留空则显示地址)",
|
||||
"이름 바꾸기": "重命名",
|
||||
"이름 바꾸기 (예: 이름 (2))": "重命名 (例: 名称 (2))",
|
||||
"이름 바꾸기 · 이동": "重命名 · 移动",
|
||||
"이름 변경": "重命名",
|
||||
|
|
@ -1693,6 +1696,7 @@
|
|||
"즐겨찾기 추가…": "添加书签…",
|
||||
"즐겨찾기 추가됨": "已添加书签",
|
||||
"즐겨찾기 표시줄": "书签栏",
|
||||
"즐겨찾기에서 빼기 (폴더는 그대로)": "从收藏中移除(文件夹保留)",
|
||||
"지금": "当前",
|
||||
"지금 {0}%": "当前 {0}%",
|
||||
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "正在运行的复制使用的是之前的选项。停止并用新选项重新运行吗?",
|
||||
|
|
@ -1708,6 +1712,10 @@
|
|||
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "当前版本: v{0} → 新版本: v{1}",
|
||||
"지금 작업 취소: {0}": "已取消当前任务: {0}",
|
||||
"지금 작업만 (대기 {0}개는 계속)": "仅当前任务 (等待中的 {0} 个继续)",
|
||||
"지금 짝 추가": "添加当前一对",
|
||||
"지금 짝을 즐겨찾기에": "将当前一对加入收藏",
|
||||
"지금 폴더 추가": "添加当前文件夹",
|
||||
"지금 폴더를 즐겨찾기에": "将当前文件夹加入收藏",
|
||||
"지금: {0}": "当前: {0}",
|
||||
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "当前是双开应用 (复制的应用)。Android 可能不会给双开应用此权限。如果无法开启,请从没有徽标的原 JJ_MKVMaker 图标打开。",
|
||||
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "上次运行未正常结束(最后检查 {0}:{1})。已记录在任务记录中。",
|
||||
|
|
|
|||
|
|
@ -36,6 +36,27 @@ class MoveTarget {
|
|||
int get hashCode => Object.hash(name, dir);
|
||||
}
|
||||
|
||||
/// 73: 파일 탐색기 즐겨찾기 (폴더). [right] 가 있으면 Rsync 화면의 "왼쪽 + 오른쪽" 짝
|
||||
class FolderBookmark {
|
||||
final String name;
|
||||
final String path;
|
||||
final String? right;
|
||||
const FolderBookmark({required this.name, required this.path, this.right});
|
||||
|
||||
bool get isPair => right != null;
|
||||
|
||||
FolderBookmark copyWith({String? name}) => FolderBookmark(name: name ?? this.name, path: path, right: right);
|
||||
|
||||
Map<String, Object?> toJson() => {'name': name, 'path': path, if (right != null) 'right': right};
|
||||
|
||||
static FolderBookmark? fromJson(Object? j) => j is Map && j['path'] is String
|
||||
? FolderBookmark(
|
||||
name: j['name'] is String && (j['name'] as String).trim().isNotEmpty ? j['name'] as String : j['path'] as String,
|
||||
path: j['path'] as String,
|
||||
right: j['right'] is String ? j['right'] as String : null)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// 실시간 동기화 한 쌍 (lsyncd 처럼: 원본 폴더를 지켜보다가 바뀌면 대상 폴더에 맞춘다)
|
||||
class LiveSyncPair {
|
||||
final String source;
|
||||
|
|
@ -211,6 +232,9 @@ class AppSettings {
|
|||
/// 비어 있으면 [이동] 을 처음 누를 때 폴더를 골라 하나 만든다.
|
||||
List<MoveTarget> moveTargets = [];
|
||||
|
||||
/// 73: 파일 탐색기 · Rsync 즐겨찾기 (폴더, Rsync 는 짝)
|
||||
List<FolderBookmark> folderBookmarks = [];
|
||||
|
||||
/// 웹 브라우저에서 YouTube 광고 자동 건너뛰기 (건너뛰기 버튼 누르기 · 건너뛸 수 없는 광고는 빨리 감기 · 소리 끄기)
|
||||
bool youtubeAdSkip = true;
|
||||
|
||||
|
|
@ -576,6 +600,7 @@ class AppSettings {
|
|||
...stripSecrets(extra),
|
||||
'mkvOutputRoot': mkvOutputRoot,
|
||||
'moveTargets': [for (final t in moveTargets) t.toJson()],
|
||||
'folderBookmarks': [for (final b in folderBookmarks) b.toJson()],
|
||||
'youtubeAdSkip': youtubeAdSkip,
|
||||
'youtubeAdHide': youtubeAdHide,
|
||||
'webPauseOnLeave': webPauseOnLeave,
|
||||
|
|
@ -749,6 +774,7 @@ class AppSettings {
|
|||
final e = (j['encode'] as Map?) ?? const {};
|
||||
final s = AppSettings()
|
||||
..mkvOutputRoot = j['mkvOutputRoot'] as String?
|
||||
..folderBookmarks = [for (final x in (j['folderBookmarks'] as List?) ?? const []) ?FolderBookmark.fromJson(x)]
|
||||
..moveTargets = [
|
||||
for (final x in (j['moveTargets'] as List?) ?? const [])
|
||||
if (x is Map && x['dir'] is String) MoveTarget.fromJson(x),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,186 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../app/app_controller.dart';
|
||||
import '../app/settings.dart';
|
||||
import '../core/file_ops.dart' show samePath;
|
||||
import '../core/vfs.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// 73: 파일 탐색기 즐겨찾기 - 지금 폴더를 넣고 창 위에서 한 번에 간다.
|
||||
/// Rsync 화면에서는 "왼쪽 A + 오른쪽 B" 짝을 넣고 한 번에 연다.
|
||||
/// [right] 가 있으면 Rsync 화면 (지금 오른쪽 폴더). [go] 는 이 창으로, [goPair] 는 두 창을 함께.
|
||||
Future<void> showFolderBookmarks(
|
||||
BuildContext context,
|
||||
AppController c, {
|
||||
required String current,
|
||||
String? right,
|
||||
required void Function(String dir) go,
|
||||
void Function(String left, String right)? goPair,
|
||||
}) async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => _BookmarksDialog(c: c, current: current, right: right, go: go, goPair: goPair),
|
||||
);
|
||||
}
|
||||
|
||||
class _BookmarksDialog extends StatefulWidget {
|
||||
final AppController c;
|
||||
final String current;
|
||||
final String? right;
|
||||
final void Function(String dir) go;
|
||||
final void Function(String left, String right)? goPair;
|
||||
const _BookmarksDialog({required this.c, required this.current, this.right, required this.go, this.goPair});
|
||||
|
||||
@override
|
||||
State<_BookmarksDialog> createState() => _BookmarksDialogState();
|
||||
}
|
||||
|
||||
class _BookmarksDialogState extends State<_BookmarksDialog> {
|
||||
AppSettings get s => widget.c.settings;
|
||||
bool get _rsync => widget.right != null && widget.goPair != null;
|
||||
|
||||
Future<void> _save(List<FolderBookmark> list) async {
|
||||
await widget.c.updateSettings((x) => x.folderBookmarks = list);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<String?> _askName(String initial, String title) async {
|
||||
final name = await showDialog<String>(context: context, builder: (_) => _NameDialog(initial: initial, title: title));
|
||||
final t = name?.trim();
|
||||
return t == null || t.isEmpty ? null : t;
|
||||
}
|
||||
|
||||
Future<void> _add({required bool pair}) async {
|
||||
final b = pair
|
||||
? FolderBookmark(name: '${vBasename(widget.current)} ⇄ ${vBasename(widget.right!)}', path: widget.current, right: widget.right)
|
||||
: FolderBookmark(name: vBasename(widget.current).isEmpty ? widget.current : vBasename(widget.current), path: widget.current);
|
||||
final name = await _askName(b.name, pair ? tr('지금 짝을 즐겨찾기에') : tr('지금 폴더를 즐겨찾기에'));
|
||||
if (name == null) return;
|
||||
await _save([...s.folderBookmarks, b.copyWith(name: name)]);
|
||||
}
|
||||
|
||||
void _open(FolderBookmark b) {
|
||||
Navigator.pop(context);
|
||||
if (b.isPair && widget.goPair != null) {
|
||||
widget.goPair!(b.path, b.right!);
|
||||
} else {
|
||||
widget.go(b.path);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mine = [for (final b in s.folderBookmarks) if (_rsync || !b.isPair) b];
|
||||
// 바로 가기: 다운로드 폴더 · MKV 결과 폴더 (넣지 않아도)
|
||||
final shortcuts = [
|
||||
(tr('다운로드'), s.resolvedDownloadRoot()),
|
||||
if (s.mkvOutputRoot?.isNotEmpty ?? false) (tr('MKV 결과 폴더'), s.mkvOutputRoot!),
|
||||
].where((x) => !mine.any((b) => !b.isPair && samePath(b.path, x.$2))).toList();
|
||||
final already = mine.any((b) => !b.isPair && samePath(b.path, widget.current));
|
||||
final pairAlready = _rsync && mine.any((b) => b.isPair && samePath(b.path, widget.current) && samePath(b.right!, widget.right!));
|
||||
return AlertDialog(
|
||||
title: Text(tr('즐겨찾기')),
|
||||
contentPadding: const EdgeInsets.fromLTRB(8, 12, 8, 0),
|
||||
content: SizedBox(
|
||||
width: 480,
|
||||
child: ListView(shrinkWrap: true, children: [
|
||||
if (mine.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(tr('아직 없습니다. 아래에서 지금 폴더를 넣으세요.'), style: const TextStyle(color: JjColors.textDim)),
|
||||
),
|
||||
for (final b in mine)
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: Icon(b.isPair ? Icons.compare_arrows : Icons.star, color: Colors.amber, size: 20),
|
||||
title: Text(b.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text(b.isPair ? '${vDisplay(b.path)}\n⇄ ${vDisplay(b.right!)}' : vDisplay(b.path),
|
||||
maxLines: b.isPair ? 2 : 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11)),
|
||||
onTap: () => _open(b),
|
||||
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
tooltip: tr('이름 바꾸기'),
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(Icons.edit_outlined, size: 18),
|
||||
onPressed: () async {
|
||||
final name = await _askName(b.name, tr('이름 바꾸기'));
|
||||
if (name != null) await _save([for (final x in s.folderBookmarks) identical(x, b) ? x.copyWith(name: name) : x]);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
tooltip: tr('즐겨찾기에서 빼기 (폴더는 그대로)'),
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: () => _save([for (final x in s.folderBookmarks) if (!identical(x, b)) x]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
if (shortcuts.isNotEmpty) ...[
|
||||
const Divider(),
|
||||
for (final (name, dir) in shortcuts)
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.folder_special_outlined, size: 20),
|
||||
title: Text(name),
|
||||
subtitle: Text(vDisplay(dir), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11)),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
widget.go(dir);
|
||||
},
|
||||
),
|
||||
],
|
||||
]),
|
||||
),
|
||||
actions: [
|
||||
if (_rsync)
|
||||
TextButton.icon(
|
||||
onPressed: pairAlready ? null : () => _add(pair: true),
|
||||
icon: const Icon(Icons.compare_arrows, size: 18),
|
||||
label: Text(tr('지금 짝 추가')),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: already || widget.current.isEmpty ? null : () => _add(pair: false),
|
||||
icon: const Icon(Icons.star_border, size: 18),
|
||||
label: Text(tr('지금 폴더 추가')),
|
||||
),
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(tr('닫기'))),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 표시 이름 넣기 (글 칸은 창이 닫히는 동안에도 그려지므로 창이 스스로 가지고 버린다)
|
||||
class _NameDialog extends StatefulWidget {
|
||||
final String initial;
|
||||
final String title;
|
||||
const _NameDialog({required this.initial, required this.title});
|
||||
|
||||
@override
|
||||
State<_NameDialog> createState() => _NameDialogState();
|
||||
}
|
||||
|
||||
class _NameDialogState extends State<_NameDialog> {
|
||||
late final _ctl = TextEditingController(text: widget.initial);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AlertDialog(
|
||||
title: Text(widget.title),
|
||||
content: TextField(
|
||||
controller: _ctl,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(labelText: tr('표시 이름')),
|
||||
onSubmitted: (v) => Navigator.pop(context, v),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(tr('취소'))),
|
||||
FilledButton(onPressed: () => Navigator.pop(context, _ctl.text), child: Text(tr('확인'))),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ import '../app/components.dart';
|
|||
import 'zip_page.dart';
|
||||
import '../core/reader_sources.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'explorer_bookmarks.dart';
|
||||
|
||||
/// 파일 탐색기 (X-plore 참고): 두 창 (트리 목록) + 가운데 기능 버튼 줄.
|
||||
///
|
||||
|
|
@ -125,6 +126,8 @@ enum ExplorerButton {
|
|||
sort(Icons.sort, '정렬 기준'),
|
||||
hidden(Icons.visibility_outlined, '숨은 항목 표시'),
|
||||
history(Icons.history, '내역'),
|
||||
// 73: 즐겨찾기 (지금 폴더 넣기 · 한 번에 가기, Rsync 는 짝)
|
||||
bookmarks(Icons.star_outline, '즐겨찾기'),
|
||||
// 118: 두 창을 좌우 ⇆ / 위아래 ⇅ 로 한 번에 바꾸기 (한 창이면 두 창 (좌우) 으로)
|
||||
orient(Icons.swap_horiz, '좌우 ⇆ / 위아래 ⇅'),
|
||||
layout(Icons.view_quilt_outlined, '창 배치'),
|
||||
|
|
@ -141,11 +144,11 @@ enum ExplorerButton {
|
|||
static List<ExplorerButton> get explorerValues => [for (final b in values) if (!b.rsyncOnly) b];
|
||||
|
||||
/// Rsync 화면의 버튼 (늘 이대로, 두 창 사이)
|
||||
static const rsyncButtons = [up, refresh, newFolder, toRight, toLeft, both, monitor];
|
||||
static const rsyncButtons = [up, refresh, newFolder, bookmarks, toRight, toLeft, both, monitor];
|
||||
|
||||
/// 147 (관리자): 처음 값 (버튼 구성을 바꾸지 않았을 때) 은 자주 쓰는 것부터 - 좁은 화면에서 [더 보기] 로 가는 것은 뒤쪽
|
||||
static List<ExplorerButton> get defaultOrder {
|
||||
const first = [copy, move, delete, newFolder, rename, select, search, orient];
|
||||
const first = [copy, move, delete, newFolder, rename, select, search, bookmarks, orient];
|
||||
return [...first, for (final b in explorerValues) if (!first.contains(b)) b];
|
||||
}
|
||||
|
||||
|
|
@ -1288,6 +1291,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
_snack(c.settings.explorerShowHidden ? tr('숨은 항목을 보입니다.') : tr('숨은 항목을 감춥니다.'));
|
||||
case ExplorerButton.history:
|
||||
await _historyDialog(pane);
|
||||
case ExplorerButton.bookmarks:
|
||||
await _bookmarks(pane);
|
||||
case ExplorerButton.orient:
|
||||
await _toggleOrientation();
|
||||
case ExplorerButton.layout:
|
||||
|
|
@ -2323,6 +2328,25 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 73: 즐겨찾기 - 이 창으로 가기, Rsync 화면이면 짝 (왼쪽 · 오른쪽) 을 한 번에
|
||||
Future<void> _bookmarks(_Pane pane) async {
|
||||
setState(() => _active = _panes.indexOf(pane));
|
||||
await showFolderBookmarks(
|
||||
context,
|
||||
c,
|
||||
current: pane.current,
|
||||
right: widget.rsync ? _panes[1].current : null,
|
||||
go: (dir) => _goTo(pane, dir, fresh: true),
|
||||
goPair: widget.rsync
|
||||
? (a, b) {
|
||||
_goTo(_panes[0], a, fresh: true);
|
||||
_goTo(_panes[1], b, fresh: true);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _historyDialog(_Pane pane) async {
|
||||
final hist = c.settings.explorerHistory.where(_reachable).toList();
|
||||
final pick = await showDialog<String>(
|
||||
|
|
@ -2773,6 +2797,16 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
children: [
|
||||
// 73: 즐겨찾기 (창 위에서 한 번에 - 드라이브가 많아도 보이게 맨 앞)
|
||||
IconButton(
|
||||
tooltip: tr('즐겨찾기'),
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: Icon(
|
||||
c.settings.folderBookmarks.any((b) => !b.isPair && samePath(b.path, pane.current)) ? Icons.star : Icons.star_outline,
|
||||
size: 16,
|
||||
color: Colors.amber),
|
||||
onPressed: () => _bookmarks(pane),
|
||||
),
|
||||
// 37: 지금 열어 둔 네트워크 공유 (\\NAS\공유) 도 탭으로
|
||||
for (final (path, label) in [
|
||||
..._volumes,
|
||||
|
|
|
|||
|
|
@ -351,8 +351,8 @@ void main() {
|
|||
expect(c.settings.explorerShowHidden, !before);
|
||||
// 창 배치 버튼은 좁아도 늘 줄에 (사용자 요청) · 처음 값은 자주 쓰는 것부터 (복사 · 이동 · 삭제 …)
|
||||
expect(find.byTooltip('창 배치: 좌우 (누르면 위아래)'), findsOneWidget);
|
||||
expect(ExplorerButton.fromSettings(const []).take(8).map((b) => b.name),
|
||||
['copy', 'move', 'delete', 'newFolder', 'rename', 'select', 'search', 'orient']);
|
||||
expect(ExplorerButton.fromSettings(const []).take(9).map((b) => b.name),
|
||||
['copy', 'move', 'delete', 'newFolder', 'rename', 'select', 'search', 'bookmarks', 'orient']);
|
||||
expect(ExplorerButton.fromSettings(const ['hidden', 'copy']), [ExplorerButton.hidden, ExplorerButton.copy],
|
||||
reason: '사용자가 정한 순서는 그대로');
|
||||
// 넉넉한 화면에서는 [더 보기] 없이 모두
|
||||
|
|
|
|||
|
|
@ -164,6 +164,14 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
await act(tester, () => tester.tap(find.text('..')));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||
// 줄에 자리가 모자라면 [더 보기] 안에 있다 (버튼이 늘면 - 73 즐겨찾기)
|
||||
if (find.text('붙여넣기').evaluate().isEmpty) {
|
||||
// 아래 버튼 줄을 가리는 "담았습니다" 알림을 먼저 닫는다
|
||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('더 보기'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
await act(tester, () => tester.tap(find.text('붙여넣기')));
|
||||
await tester.pumpAndSettle();
|
||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
||||
|
|
@ -181,6 +189,9 @@ void main() {
|
|||
expect(find.text('doc'), findsOneWidget); // 확장자를 뺀 이름
|
||||
expect(find.text('txt'), findsNWidgets(2)); // 확장자 칸 (doc.txt · 붙여넣은 inner.txt)
|
||||
expect(find.text('확장자'), findsOneWidget);
|
||||
// 알림 (복사 끝 등) 이 시험이 끝난 뒤에 닫히지 않게 정리
|
||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('선택 모드: 파일 누르기 = 선택 · 취소, 폴더 누르기 = 자기 자신과 안의 것 모두 선택 · 다시 누르면 모두 취소', (tester) async {
|
||||
|
|
@ -252,6 +263,76 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
}, skip: !Platform.isWindows);
|
||||
|
||||
testWidgets('73: 즐겨찾기 - 지금 폴더 넣기 (이름 고침) · 한 번에 가기 · 빼기 (폴더는 그대로)', (tester) async {
|
||||
await open(tester);
|
||||
// 창 위의 ★ (왼쪽 창)
|
||||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('아직 없습니다. 아래에서 지금 폴더를 넣으세요.'), findsOneWidget);
|
||||
await tester.tap(find.text('지금 폴더 추가'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField).last, '왼쪽 폴더');
|
||||
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
||||
await tester.pumpAndSettle();
|
||||
expect([for (final b in c.settings.folderBookmarks) (b.name, b.path)], [('왼쪽 폴더', left)]);
|
||||
expect(find.text('왼쪽 폴더'), findsOneWidget);
|
||||
await tester.tap(find.text('닫기'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byIcon(Icons.star), findsOneWidget, reason: '즐겨찾기한 폴더를 보는 창의 ★ 는 채움');
|
||||
|
||||
// 오른쪽 폴더를 즐겨찾기에 두고 왼쪽 창에서 한 번에 가기
|
||||
await c.updateSettings((x) => x.folderBookmarks = [...x.folderBookmarks, FolderBookmark(name: 'R', path: right)]);
|
||||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||||
await tester.pumpAndSettle();
|
||||
await act(tester, () => tester.tap(find.text('R')));
|
||||
await settle(tester, () => false, rounds: 20);
|
||||
expect(find.byIcon(Icons.star), findsNWidgets(2), reason: '두 창 모두 즐겨찾기한 폴더 (왼쪽 창이 R 로 감)');
|
||||
|
||||
// 빼기: 즐겨찾기에서만 (폴더는 그대로)
|
||||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('즐겨찾기에서 빼기 (폴더는 그대로)').first);
|
||||
await tester.pumpAndSettle();
|
||||
expect([for (final b in c.settings.folderBookmarks) b.name], ['R']);
|
||||
expect(Directory(left).existsSync(), isTrue);
|
||||
await tester.tap(find.text('닫기'));
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('73: Rsync 화면 - "왼쪽 + 오른쪽" 짝을 넣고 한 번에 연다', (tester) async {
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
c.settings.rsyncPaths = [left, right];
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c, rsync: true))));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('지금 짝 추가'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
||||
await tester.pumpAndSettle();
|
||||
final pair = c.settings.folderBookmarks.single;
|
||||
expect((pair.path, pair.right, pair.name), (left, right, 'left ⇄ right'));
|
||||
await tester.tap(find.text('닫기'));
|
||||
await tester.pumpAndSettle();
|
||||
// 반대 짝을 넣어 두고 열면 두 창이 함께 바뀐다
|
||||
await c.updateSettings((x) => x.folderBookmarks = [...x.folderBookmarks, FolderBookmark(name: '거꾸로', path: right, right: left)]);
|
||||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||||
await tester.pumpAndSettle();
|
||||
await act(tester, () => tester.tap(find.text('거꾸로')));
|
||||
await settle(tester, () => c.settings.rsyncPaths.length == 2 && c.settings.rsyncPaths[0] == right && c.settings.rsyncPaths[1] == left,
|
||||
rounds: 100);
|
||||
expect(c.settings.rsyncPaths, [right, left]);
|
||||
});
|
||||
|
||||
test('73: 즐겨찾기 저장 · 읽기 (Rsync 짝 포함)', () {
|
||||
final s = AppSettings()
|
||||
..folderBookmarks = const [FolderBookmark(name: 'NAS/영화', path: r'\\nas\movies'), FolderBookmark(name: '백업', path: r'D:\a', right: r'E:\b')];
|
||||
final b = AppSettings.fromJson(s.toJson()).folderBookmarks;
|
||||
expect([for (final x in b) (x.name, x.path, x.right, x.isPair)], [('NAS/영화', r'\\nas\movies', null, false), ('백업', r'D:\a', r'E:\b', true)]);
|
||||
});
|
||||
|
||||
testWidgets('여러 개 표시 → 삭제 · 새 폴더 · 이름 변경', (tester) async {
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('선택').first));
|
||||
|
|
|
|||
Loading…
Reference in New Issue