246 lines
11 KiB
Dart
246 lines
11 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../../core/file_ops.dart' show FileOps, samePath;
|
|
import '../../core/playlist.dart' show isPlayableFile;
|
|
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_flows.dart';
|
|
import 'explorer_select.dart';
|
|
import 'name_dialog.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;
|
|
|
|
/// 파일 탐색기에서 고를 수 있는 버튼. [앱 관리자] 는 Android 에서만 ([android] = c.services.platform.isAndroid - 설계 M4, 예전 전역 함수 appsButtonSupported)
|
|
static List<ExplorerButton> explorerValues({required bool android}) =>
|
|
[for (final b in values) if (!b.rsyncOnly && (b != apps || android)) b];
|
|
|
|
/// Rsync 화면의 버튼 (늘 이대로, 두 창 사이)
|
|
// 208: 방향 실행 (Rsync 화면의 핵심) 을 앞에 - 좁아서 [더 보기] 로 가는 것은 뒤쪽
|
|
static const rsyncButtons = [toRight, toLeft, both, monitor, up, refresh, newFolder, bookmarks];
|
|
|
|
/// 147 (관리자): 처음 값 (버튼 구성을 바꾸지 않았을 때) 은 자주 쓰는 것부터 - 좁은 화면에서 [더 보기] 로 가는 것은 뒤쪽
|
|
static List<ExplorerButton> defaultOrder({required bool android}) {
|
|
const first = [copy, move, delete, newFolder, rename, select, search, bookmarks, orient];
|
|
return [...first, for (final b in explorerValues(android: android)) if (!first.contains(b)) b];
|
|
}
|
|
|
|
static List<ExplorerButton> fromSettings(List<String> names, {required bool android}) {
|
|
final out = [
|
|
for (final n in names)
|
|
for (final b in explorerValues(android: android))
|
|
if (b.name == n) b,
|
|
];
|
|
// 사용자가 정한 순서는 그대로 (비어 있을 때만 처음 값)
|
|
return out.isEmpty ? defaultOrder(android: android) : out;
|
|
}
|
|
}
|
|
|
|
/// 약속 (E5d-4 · E5d-6): 버튼 · 메뉴가 화면에서 받는 것 - 창 · 메뉴를 띄울 곳 · 배치 (다른 창 · 두 창) · 빠른 보기. 일은 [ExplorerFlows]
|
|
abstract interface class ExplorerCommands {
|
|
/// 창 · 메뉴를 띄울 곳
|
|
BuildContext get context;
|
|
|
|
/// 복사 · 이동 갈 곳 (두 창 · 숨은 다른 창의 지금 폴더, 없으면 null - 담아 두기)
|
|
String? get otherDir;
|
|
|
|
/// 두 창으로 보이는지
|
|
bool get dual;
|
|
}
|
|
|
|
/// 약속 (E5d-4): 기능 버튼 하나 → 할 일 (가운데 줄 · 단축키 · 위 메뉴가 같이 부른다). 지금 창 ([ExplorerController.pane]) 에 한다.
|
|
/// 창 · 고르기 · 다시 읽기 · 새 폴더 · 숨은 항목은 여기서, 나머지는 흐름 모듈 ([ExplorerFlows]). 못 하면 까닭을 알림 (Host)
|
|
class ExplorerButtons {
|
|
ExplorerButtons(this.ctl, this.sel, this.cmd, this.flows);
|
|
|
|
final ExplorerController ctl;
|
|
final ExplorerSelect sel;
|
|
final ExplorerCommands cmd;
|
|
final ExplorerFlows flows;
|
|
|
|
/// 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:
|
|
flows.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 flows.rsync.run(cmd.context, right: true, left: false);
|
|
case ExplorerButton.toLeft:
|
|
await flows.rsync.run(cmd.context, right: false, left: true);
|
|
case ExplorerButton.both:
|
|
await flows.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) && isPlayableFile(x)).toList();
|
|
if (videos.isNotEmpty) {
|
|
await flows.opener.play(cmd.context, videos, keepOrder: !pane.sel.isEmpty);
|
|
} else {
|
|
await flows.opener.playFolder(cmd.context, t.length == 1 && vIsDirSync(t.first) ? t.first : pane.current);
|
|
}
|
|
case ExplorerButton.addMkv:
|
|
final t = sel.targets(pane);
|
|
await flows.opener.addMkv(t.isEmpty ? [pane.current] : t);
|
|
case ExplorerButton.newFolder:
|
|
// X-plore 비교 f: 펼친 압축 안 (지금 폴더가 압축 안의 폴더) 에는 만들지 않는다
|
|
if (flows.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 flows.renames.batch(cmd.context, pane, t);
|
|
await flows.renames.rename(cmd.context, pane, t.first);
|
|
case ExplorerButton.copy:
|
|
await flows.transfers.transfer(cmd.context, pane, sel.targets(pane), move: false, otherDir: cmd.otherDir);
|
|
case ExplorerButton.move:
|
|
await flows.transfers.transfer(cmd.context, pane, sel.targets(pane), move: true, otherDir: cmd.otherDir);
|
|
case ExplorerButton.paste:
|
|
await flows.transfers.paste(cmd.context, pane, pane.current);
|
|
case ExplorerButton.delete:
|
|
await flows.deletes.run(cmd.context, pane, sel.targets(pane));
|
|
case ExplorerButton.search:
|
|
await flows.search.open(cmd.context, pane);
|
|
case ExplorerButton.storage:
|
|
await flows.tools.openStorage(cmd.context, pane, pane.current);
|
|
case ExplorerButton.wifiShare:
|
|
await flows.tools.openWifiShare(cmd.context, pane.current);
|
|
case ExplorerButton.apps:
|
|
await openAppManager(cmd.context);
|
|
case ExplorerButton.vault:
|
|
if (!ctl.c.settings.vault.enabled) {
|
|
ctl.host.snack(tr('암호 보관함은 환경 설정 > 보안 에서 켭니다 (처음 꺼짐)'));
|
|
} else {
|
|
await openVault(cmd.context, ctl.c);
|
|
}
|
|
case ExplorerButton.trash:
|
|
await flows.deletes.openTrash(cmd.context);
|
|
case ExplorerButton.sort:
|
|
await flows.dialogs.sortDialog(cmd.context);
|
|
case ExplorerButton.hidden:
|
|
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.explorer.showHidden ? tr('숨은 항목을 보입니다.') : tr('숨은 항목을 감춥니다.'));
|
|
case ExplorerButton.history:
|
|
await flows.dialogs.historyDialog(cmd.context, pane);
|
|
case ExplorerButton.bookmarks:
|
|
await flows.dialogs.openBookmarks(cmd.context, pane);
|
|
case ExplorerButton.orient:
|
|
await flows.dialogs.toggleOrientation(cmd.context);
|
|
case ExplorerButton.layout:
|
|
await flows.dialogs.layoutDialog(cmd.context);
|
|
case ExplorerButton.buttons:
|
|
await flows.dialogs.buttonsDialog(cmd.context);
|
|
}
|
|
}
|
|
}
|