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 'explorer_select.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 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 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 fromSettings(List 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; /// 다음 지우기는 휴지통을 거치지 않음 (Shift+Delete · [삭제] 길게 누르기 - 창에서 한 번 더 확인) abstract bool shiftDelete; /// 버튼이 다룰 것 (표시한 것, 없으면 지금 줄) List targetsOf(ExplorerPane pane); Future runRsync({required bool right, required bool left}); Future play(List paths, {bool internal = false, bool keepOrder = false}); Future playFolder(String dir); Future addMkv(List paths); /// 압축 안이면 막고 알림 (막았으면 true) bool blockArchive(List paths, {bool self = false}); Future askName(String title, String initial, {String? hint, bool selectAll = false}); Future renameItem(ExplorerPane pane, String path); Future batchRename(ExplorerPane pane, List paths); Future transfer(ExplorerPane pane, List sources, {required bool move}); Future paste(ExplorerPane pane, String dest); Future deleteItems(ExplorerPane pane, List paths); Future openSearch(ExplorerPane pane); Future openStorage(ExplorerPane pane, String dir); Future openWifiShare(String dir); Future openTrash(); Future sortDialog(); Future historyDialog(ExplorerPane pane); Future openBookmarks(ExplorerPane pane); Future toggleOrientation(); Future layoutDialog(); Future buttonsDialog(); } /// 약속 (E5d-4): 기능 버튼 하나 → 할 일 (가운데 줄 · 단축키 · 위 메뉴가 같이 부른다). 지금 창 ([ExplorerController.pane]) 에 한다. /// 창 · 고르기 · 다시 읽기 · 새 폴더 · 숨은 항목은 여기서, 나머지는 [ExplorerCommands]. 못 하면 까닭을 알림 (Host) class ExplorerButtons { ExplorerButtons(this.ctl, this.sel, this.cmd); final ExplorerController ctl; final ExplorerSelect sel; final ExplorerCommands cmd; /// X-plore b: 길게 누르면 하는 짝 동작의 이름 (없으면 null) static String? longPressLabel(ExplorerButton b) => switch (b) { ExplorerButton.copy => '이동', ExplorerButton.move => '복사', ExplorerButton.delete => '영구 삭제', _ => null, }; /// X-plore b: 길게 누르기 = 짝 동작 (복사 ↔ 이동, 지우기 → 영구 삭제). 짝이 없으면 아무것도 안 함 Future longPress(ExplorerButton b) async { switch (b) { case ExplorerButton.copy: return press(ExplorerButton.move); case ExplorerButton.move: return press(ExplorerButton.copy); case ExplorerButton.delete: cmd.shiftDelete = true; return press(ExplorerButton.delete); default: return; } } Future 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 cmd.runRsync(right: true, left: false); case ExplorerButton.toLeft: await cmd.runRsync(right: false, left: true); case ExplorerButton.both: await cmd.runRsync(right: true, left: true); case ExplorerButton.refresh: // 183: 두 창 모두 (밖에서 지운 파일이 다른 창에 남지 않게) await ctl.reloadOpen(); case ExplorerButton.play: final t = cmd.targetsOf(pane); // 181: 강조한 것이 mp3 등 소리 파일이어도 그것을 (예전에는 동영상이 아니라고 빼고 폴더의 동영상을 틀었다) final videos = t.where((x) => !vIsDirSync(x) && (isVideoFile(x) || isAudioFile(x))).toList(); if (videos.isNotEmpty) { await cmd.play(videos, keepOrder: !pane.sel.isEmpty); } else { await cmd.playFolder(t.length == 1 && vIsDirSync(t.first) ? t.first : pane.current); } case ExplorerButton.addMkv: final t = cmd.targetsOf(pane); await cmd.addMkv(t.isEmpty ? [pane.current] : t); case ExplorerButton.newFolder: // X-plore 비교 f: 펼친 압축 안 (지금 폴더가 압축 안의 폴더) 에는 만들지 않는다 if (cmd.blockArchive([pane.current], self: true)) return; final name = await cmd.askName(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 = cmd.targetsOf(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 cmd.transfer(pane, cmd.targetsOf(pane), move: false); case ExplorerButton.move: await cmd.transfer(pane, cmd.targetsOf(pane), move: true); case ExplorerButton.paste: await cmd.paste(pane, pane.current); case ExplorerButton.delete: await cmd.deleteItems(pane, cmd.targetsOf(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 cmd.openTrash(); 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(); } } }