JJ_MKVMaker/lib/ui/explorer/explorer_buttons.dart

275 lines
12 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import '../../core/file_ops.dart' show FileOps, samePath;
import '../../core/playlist.dart' show isAudioFile, isVideoFile;
import '../../core/vfs.dart' show vDirname, vIsDirSync;
import '../../l10n/tr.dart';
import '../app_manager_page.dart' show openAppManager;
import '../monitor_page.dart';
import '../vault_page.dart' show openVault;
import 'explorer_controller.dart';
import 'explorer_pane.dart';
import 'delete_flow.dart';
import 'explorer_select.dart';
import 'name_dialog.dart';
import 'open_flow.dart';
import 'rsync_flow.dart';
import 'transfer_flow.dart';
import 'write_guard.dart';
/// 기능 버튼 (가운데 줄). 이름은 설정 (explorerButtons) 에 저장된다.
enum ExplorerButton {
up(Icons.arrow_upward, '상위 폴더'),
refresh(Icons.refresh, '새로 고침'),
select(Icons.checklist, '선택'),
// ── Rsync 화면에만 ──
toRight(Icons.arrow_forward, '좌 → 우'),
toLeft(Icons.arrow_back, '좌 ← 우'),
both(Icons.compare_arrows, '좌 ⇄ 우'),
monitor(Icons.monitor_heart_outlined, '모니터링'),
// ──
play(Icons.play_circle_outline, '재생'),
addMkv(Icons.playlist_add, 'MKV 목록에 추가'),
newFolder(Icons.create_new_folder_outlined, '새 폴더'),
rename(Icons.drive_file_rename_outline, '이름 변경'),
copy(Icons.copy_outlined, '복사'),
move(Icons.drive_file_move_outline, '이동'),
paste(Icons.content_paste, '붙여넣기'),
delete(Icons.delete_outline, '삭제'),
search(Icons.search, '찾기'),
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, '창 배치'),
buttons(Icons.tune, '버튼 구성'),
// 87: 지금 폴더 (드라이브) 의 큰 폴더 · 큰 파일
storage(Icons.pie_chart_outline, '저장 공간 분석'),
// 89: 지금 폴더를 같은 Wi-Fi 의 다른 기기 브라우저에서 보기 · 받기 (읽기 전용)
wifiShare(Icons.wifi_tethering, 'Wi-Fi 공유'),
// X-plore 비교 e: 휴지통 (Android 앱 휴지통 · Windows 는 시스템 휴지통 열기)
trash(Icons.restore_from_trash_outlined, '휴지통'),
// X-plore 비교 5-2 (사용자 결정 10/21): 앱 관리자 (Android 만)
apps(Icons.apps, '앱 관리자'),
// X-plore 비교 5-2 (사용자 결정 10/21): 암호 보관함 (환경 설정 > 보안 에서 켰을 때 열림)
vault(Icons.enhanced_encryption_outlined, '암호 보관함');
final IconData icon;
final String label;
const ExplorerButton(this.icon, this.label);
/// Rsync 화면에만 있는 버튼 (파일 탐색기 버튼 구성에서는 빼기)
bool get rsyncOnly => this == toRight || this == toLeft || this == both || this == monitor;
/// 파일 탐색기에서 고를 수 있는 버튼
static List<ExplorerButton> get explorerValues => [for (final b in values) if (!b.rsyncOnly && (b != apps || appsButtonSupported())) b];
/// 앱 관리자 버튼을 쓰는 기기 (Android - 시험에서 바꿈)
static bool Function() appsButtonSupported = () => Platform.isAndroid;
/// Rsync 화면의 버튼 (늘 이대로, 두 창 사이)
// 208: 방향 실행 (Rsync 화면의 핵심) 을 앞에 - 좁아서 [더 보기] 로 가는 것은 뒤쪽
static const rsyncButtons = [toRight, toLeft, both, monitor, up, refresh, newFolder, bookmarks];
/// 147 (관리자): 처음 값 (버튼 구성을 바꾸지 않았을 때) 은 자주 쓰는 것부터 - 좁은 화면에서 [더 보기] 로 가는 것은 뒤쪽
static List<ExplorerButton> get defaultOrder {
const first = [copy, move, delete, newFolder, rename, select, search, bookmarks, orient];
return [...first, for (final b in explorerValues) if (!first.contains(b)) b];
}
static List<ExplorerButton> fromSettings(List<String> names) {
final out = [
for (final n in names)
for (final b in explorerValues)
if (b.name == n) b,
];
// 사용자가 정한 순서는 그대로 (비어 있을 때만 처음 값)
return out.isEmpty ? defaultOrder : out;
}
}
/// 약속 (E5d-4): 버튼이 부르는 화면의 일 (지우기 · 옮기기 · 이름 바꾸기 · 창 · 재생 등). 흐름 모듈로 옮겨 갈 때마다 이 목록이 줄어든다
abstract interface class ExplorerCommands {
/// 창 · 메뉴를 띄울 곳
BuildContext get context;
/// 복사 · 이동 갈 곳 (두 창 · 숨은 다른 창의 지금 폴더, 없으면 null - 담아 두기)
String? get otherDir;
/// 두 창으로 보이는지
bool get dual;
/// 81: 빠른 보기 - [path] (없으면 지금 줄). 같은 폴더에 보이는 항목을 ← → 로
Future<void> quickLook(ExplorerPane pane, [String? path]);
Future<void> addMkv(List<String> paths);
Future<void> renameItem(ExplorerPane pane, String path);
Future<void> batchRename(ExplorerPane pane, List<String> paths);
Future<void> openSearch(ExplorerPane pane);
Future<void> openStorage(ExplorerPane pane, String dir);
Future<void> openWifiShare(String dir);
Future<void> sortDialog();
Future<void> historyDialog(ExplorerPane pane);
Future<void> openBookmarks(ExplorerPane pane);
Future<void> toggleOrientation();
Future<void> layoutDialog();
Future<void> buttonsDialog();
}
/// 약속 (E5d-4): 기능 버튼 하나 → 할 일 (가운데 줄 · 단축키 · 위 메뉴가 같이 부른다). 지금 창 ([ExplorerController.pane]) 에 한다.
/// 창 · 고르기 · 다시 읽기 · 새 폴더 · 숨은 항목은 여기서, 복사 · 이동 · 지우기 · rsync 는 흐름 모듈, 나머지는 [ExplorerCommands]. 못 하면 까닭을 알림 (Host)
class ExplorerButtons {
ExplorerButtons(this.ctl, this.sel, this.cmd,
{required this.guard, required this.transfers, required this.deletes, required this.rsync, required this.opener});
final ExplorerController ctl;
final ExplorerSelect sel;
final ExplorerCommands cmd;
final ExplorerGuard guard;
final ExplorerTransfers transfers;
final ExplorerDeleteFlow deletes;
final ExplorerRsyncFlow rsync;
final ExplorerOpener opener;
/// X-plore b: 길게 누르면 하는 짝 동작의 이름 (없으면 null)
static String? longPressLabel(ExplorerButton b) => switch (b) {
ExplorerButton.copy => '이동',
ExplorerButton.move => '복사',
ExplorerButton.delete => '영구 삭제',
_ => null,
};
/// X-plore b: 길게 누르기 = 짝 동작 (복사 ↔ 이동, 지우기 → 영구 삭제). 짝이 없으면 아무것도 안 함
Future<void> longPress(ExplorerButton b) async {
switch (b) {
case ExplorerButton.copy:
return press(ExplorerButton.move);
case ExplorerButton.move:
return press(ExplorerButton.copy);
case ExplorerButton.delete:
deletes.shiftDelete = true;
return press(ExplorerButton.delete);
default:
return;
}
}
Future<void> press(ExplorerButton b) async {
final pane = ctl.pane;
switch (b) {
case ExplorerButton.up:
if (samePath(pane.current, pane.root)) return;
final up = vDirname(pane.current);
pane.expanded.remove(pane.current);
await ctl.goTo(pane, up);
case ExplorerButton.select:
sel.setSelecting(!sel.selecting);
case ExplorerButton.monitor:
await MonitorPage.open(cmd.context, ctl.c);
case ExplorerButton.toRight:
await rsync.run(cmd.context, right: true, left: false);
case ExplorerButton.toLeft:
await rsync.run(cmd.context, right: false, left: true);
case ExplorerButton.both:
await rsync.run(cmd.context, right: true, left: true);
case ExplorerButton.refresh:
// 183: 두 창 모두 (밖에서 지운 파일이 다른 창에 남지 않게)
await ctl.reloadOpen();
case ExplorerButton.play:
final t = sel.targets(pane);
// 181: 강조한 것이 mp3 등 소리 파일이어도 그것을 (예전에는 동영상이 아니라고 빼고 폴더의 동영상을 틀었다)
final videos = t.where((x) => !vIsDirSync(x) && (isVideoFile(x) || isAudioFile(x))).toList();
if (videos.isNotEmpty) {
await opener.play(cmd.context, videos, keepOrder: !pane.sel.isEmpty);
} else {
await opener.playFolder(cmd.context, t.length == 1 && vIsDirSync(t.first) ? t.first : pane.current);
}
case ExplorerButton.addMkv:
final t = sel.targets(pane);
await cmd.addMkv(t.isEmpty ? [pane.current] : t);
case ExplorerButton.newFolder:
// X-plore 비교 f: 펼친 압축 안 (지금 폴더가 압축 안의 폴더) 에는 만들지 않는다
if (guard.blockArchive([pane.current], self: true)) return;
final name = await askName(cmd.context, tr('새 폴더'), '');
if (name == null) return;
try {
final parent = pane.current;
final made = await FileOps.makeFolder(parent, name);
// 만든 폴더가 트리에 보이게: 부모를 펼치고 다시 읽은 뒤 그 줄까지 스크롤
pane.expanded.add(parent);
await ctl.load(pane, parent, force: true);
await ctl.refreshAll([parent]);
// Rsync 화면: 만든 폴더를 이 창의 원본 · 대상으로 바로 고른다
if (ctl.rsync) {
pane.sel.clear();
await pane.sel.toggle(made, isDir: true, list: (_) async => const []);
}
pane.focused = made;
pane.changed();
ctl.host.reveal(pane, made);
} catch (e) {
ctl.host.snack(trf('폴더를 만들 수 없습니다: {0}', [e]));
}
case ExplorerButton.rename:
final t = sel.targets(pane);
if (t.isEmpty) {
ctl.host.snack(tr('이름을 바꿀 항목 하나를 고르세요.'));
return;
}
// 78: 여럿을 골랐으면 일괄 이름 바꾸기
if (t.length > 1) return cmd.batchRename(pane, t);
await cmd.renameItem(pane, t.first);
case ExplorerButton.copy:
await transfers.transfer(cmd.context, pane, sel.targets(pane), move: false, otherDir: cmd.otherDir);
case ExplorerButton.move:
await transfers.transfer(cmd.context, pane, sel.targets(pane), move: true, otherDir: cmd.otherDir);
case ExplorerButton.paste:
await transfers.paste(cmd.context, pane, pane.current);
case ExplorerButton.delete:
await deletes.run(cmd.context, pane, sel.targets(pane));
case ExplorerButton.search:
await cmd.openSearch(pane);
case ExplorerButton.storage:
await cmd.openStorage(pane, pane.current);
case ExplorerButton.wifiShare:
await cmd.openWifiShare(pane.current);
case ExplorerButton.apps:
await openAppManager(cmd.context);
case ExplorerButton.vault:
if (!ctl.c.settings.vaultEnabled) {
ctl.host.snack(tr('암호 보관함은 환경 설정 > 보안 에서 켭니다 (처음 꺼짐)'));
} else {
await openVault(cmd.context, ctl.c);
}
case ExplorerButton.trash:
await deletes.openTrash(cmd.context);
case ExplorerButton.sort:
await cmd.sortDialog();
case ExplorerButton.hidden:
await ctl.c.updateSettings((s) => s.explorerShowHidden = !s.explorerShowHidden);
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('숨은 항목을 감춥니다.'));
case ExplorerButton.history:
await cmd.historyDialog(pane);
case ExplorerButton.bookmarks:
await cmd.openBookmarks(pane);
case ExplorerButton.orient:
await cmd.toggleOrientation();
case ExplorerButton.layout:
await cmd.layoutDialog();
case ExplorerButton.buttons:
await cmd.buttonsDialog();
}
}
}