WIP 77 길게 누르기 메뉴: 위 아이콘 줄 (복사 · 이동 · 이름 변경 · 삭제 · 공유), 여럿이면 "n개 항목" · 삭제도 표시한 것 모두, 경로 복사 · 즐겨찾기에 추가, Android 공유 (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>backup_묶음6_d46e089
parent
1cc4cb1b4b
commit
7d9cfc191e
|
|
@ -179,6 +179,7 @@ class MainActivity : FlutterActivity() {
|
||||||
"openWith" -> result.success(
|
"openWith" -> result.success(
|
||||||
openWith(call.argument<String>("path") ?: "", call.argument<Boolean>("choose") ?: false)
|
openWith(call.argument<String>("path") ?: "", call.argument<Boolean>("choose") ?: false)
|
||||||
)
|
)
|
||||||
|
"shareFiles" -> result.success(shareFiles(call.argument<List<String>>("paths") ?: emptyList()))
|
||||||
"setAppIcon" -> result.success(setAppIcon(call.argument<String>("id") ?: ""))
|
"setAppIcon" -> result.success(setAppIcon(call.argument<String>("id") ?: ""))
|
||||||
"keepAlive" -> {
|
"keepAlive" -> {
|
||||||
keepAlive(call.argument<String>("text") ?: "", call.argument<Int>("progress") ?: -1,
|
keepAlive(call.argument<String>("text") ?: "", call.argument<Int>("progress") ?: -1,
|
||||||
|
|
@ -421,6 +422,49 @@ class MainActivity : FlutterActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 77: 파일 탐색기의 [공유] - 파일 하나는 ACTION_SEND, 여럿은 ACTION_SEND_MULTIPLE (공유 창, 이 앱은 뺌)
|
||||||
|
private fun shareFiles(paths: List<String>): Boolean {
|
||||||
|
val uris = ArrayList<android.net.Uri>()
|
||||||
|
val mimes = mutableSetOf<String>()
|
||||||
|
for (p in paths) {
|
||||||
|
val f = File(p)
|
||||||
|
if (!f.isFile) continue
|
||||||
|
uris.add(
|
||||||
|
try {
|
||||||
|
androidx.core.content.FileProvider.getUriForFile(this, "$packageName.files", f)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
mimes.add(android.webkit.MimeTypeMap.getSingleton().getMimeTypeFromExtension(f.extension.lowercase()) ?: "*/*")
|
||||||
|
}
|
||||||
|
if (uris.isEmpty()) return false
|
||||||
|
// 종류가 하나면 그것, 앞부분 (video/ 등) 만 같으면 video/*, 아니면 */*
|
||||||
|
val tops = mimes.map { it.substringBefore('/') }.toSet()
|
||||||
|
val mime = if (mimes.size == 1) mimes.first() else if (tops.size == 1 && tops.first() != "*") "${tops.first()}/*" else "*/*"
|
||||||
|
val send = if (uris.size == 1) {
|
||||||
|
Intent(Intent.ACTION_SEND).putExtra(Intent.EXTRA_STREAM, uris[0])
|
||||||
|
} else {
|
||||||
|
Intent(Intent.ACTION_SEND_MULTIPLE).putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
|
||||||
|
}
|
||||||
|
send.type = mime
|
||||||
|
// 받는 앱이 모든 파일을 읽을 수 있게 (EXTRA_STREAM 만으로는 첫 것만 허락되는 앱이 있다)
|
||||||
|
val clip = android.content.ClipData.newRawUri(null, uris[0])
|
||||||
|
for (i in 1 until uris.size) clip.addItem(android.content.ClipData.Item(uris[i]))
|
||||||
|
send.clipData = clip
|
||||||
|
send.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
return try {
|
||||||
|
startActivity(
|
||||||
|
Intent.createChooser(send, null)
|
||||||
|
.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(android.content.ComponentName(this, MainActivity::class.java)))
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun openFolder(path: String): Boolean {
|
private fun openFolder(path: String): Boolean {
|
||||||
// 내장 저장소 (primary) 또는 SD 카드 · USB (/storage/XXXX-XXXX → 문서 ID "XXXX-XXXX:...")
|
// 내장 저장소 (primary) 또는 SD 카드 · USB (/storage/XXXX-XXXX → 문서 ID "XXXX-XXXX:...")
|
||||||
val root = Environment.getExternalStorageDirectory().path
|
val root = Environment.getExternalStorageDirectory().path
|
||||||
|
|
|
||||||
|
|
@ -466,6 +466,7 @@
|
||||||
"{0}개 · 주소 {1}개": "{0} · {1} links",
|
"{0}개 · 주소 {1}개": "{0} · {1} links",
|
||||||
"{0}개 맞춤": "{0} synced",
|
"{0}개 맞춤": "{0} synced",
|
||||||
"{0}개 보임": "{0} shown",
|
"{0}개 보임": "{0} shown",
|
||||||
|
"{0}개 삭제": "Delete {0} items",
|
||||||
"{0}개 선택": "{0} selected",
|
"{0}개 선택": "{0} selected",
|
||||||
"{0}개 중 {1}개를 지우지 못했습니다": "Couldn't delete {1} of {0}",
|
"{0}개 중 {1}개를 지우지 못했습니다": "Couldn't delete {1} of {0}",
|
||||||
"{0}개 지우고 맞추기": "Delete {0} and sync",
|
"{0}개 지우고 맞추기": "Delete {0} and sync",
|
||||||
|
|
@ -576,11 +577,15 @@
|
||||||
"결과 폴더 열기: {0}": "Open output folder: {0}",
|
"결과 폴더 열기: {0}": "Open output folder: {0}",
|
||||||
"결과물 이동 실패: {0} ({1})": "Failed to move result: {0} ({1})",
|
"결과물 이동 실패: {0} ({1})": "Failed to move result: {0} ({1})",
|
||||||
"경로": "Path",
|
"경로": "Path",
|
||||||
|
"경로 {0}개를 복사했습니다": "Copied {0} paths",
|
||||||
|
"경로 복사": "Copy path",
|
||||||
|
"경로 복사 ({0}개)": "Copy paths ({0})",
|
||||||
"경로 입력": "Enter path",
|
"경로 입력": "Enter path",
|
||||||
"경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "Enter path (e.g. D:\\Videos · \\\\NAS\\share)",
|
"경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "Enter path (e.g. D:\\Videos · \\\\NAS\\share)",
|
||||||
"경로 입력 · WebDAV": "Enter path · WebDAV",
|
"경로 입력 · WebDAV": "Enter path · WebDAV",
|
||||||
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "The path is too long for the Recycle Bin (the Windows Recycle Bin does not accept paths over 260 characters).",
|
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "The path is too long for the Recycle Bin (the Windows Recycle Bin does not accept paths over 260 characters).",
|
||||||
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "The path is too long (the Windows Recycle Bin doesn't accept paths over 260 characters) - you can delete it permanently",
|
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "The path is too long (the Windows Recycle Bin doesn't accept paths over 260 characters) - you can delete it permanently",
|
||||||
|
"경로를 복사했습니다: {0}": "Copied path: {0}",
|
||||||
"계속": "Always",
|
"계속": "Always",
|
||||||
"계속 (앱이 켜져 있는 동안)": "Always (while the app is running)",
|
"계속 (앱이 켜져 있는 동안)": "Always (while the app is running)",
|
||||||
"계속 받기": "Keep downloading",
|
"계속 받기": "Keep downloading",
|
||||||
|
|
@ -605,6 +610,9 @@
|
||||||
"골라서 시작": "Choose and start",
|
"골라서 시작": "Choose and start",
|
||||||
"공용 저장소 (Download) 를 쓸 수 없어 되돌릴 수 없습니다.": "Cannot roll back: shared storage (Download) is not available.",
|
"공용 저장소 (Download) 를 쓸 수 없어 되돌릴 수 없습니다.": "Cannot roll back: shared storage (Download) is not available.",
|
||||||
"공용 폴더에 받은 파일 (동영상 · 그림 등)": "Files downloaded to shared folders (videos, images, etc.)",
|
"공용 폴더에 받은 파일 (동영상 · 그림 등)": "Files downloaded to shared folders (videos, images, etc.)",
|
||||||
|
"공유": "Share",
|
||||||
|
"공유 (다른 앱으로 보내기)": "Share (send to another app)",
|
||||||
|
"공유 창을 열 수 없습니다.": "Can't open the share sheet.",
|
||||||
"관리 메뉴": "Manage menu",
|
"관리 메뉴": "Manage menu",
|
||||||
"관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)": "The Chrome engine can't be used because the app is running as administrator (security)",
|
"관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)": "The Chrome engine can't be used because the app is running as administrator (security)",
|
||||||
"관리자 권한으로 실행할 때만 쓰입니다. 끄면 (기본) 그때 Chrome 엔진을 시작하지 않고 Edge 를 권합니다.": "Only used when running as administrator. When off (default), the Chrome engine isn't started then and Edge is suggested.",
|
"관리자 권한으로 실행할 때만 쓰입니다. 끄면 (기본) 그때 Chrome 엔진을 시작하지 않고 Edge 를 권합니다.": "Only used when running as administrator. When off (default), the Chrome engine isn't started then and Edge is suggested.",
|
||||||
|
|
@ -1954,6 +1962,8 @@
|
||||||
"즐겨찾기 추가…": "Add bookmark…",
|
"즐겨찾기 추가…": "Add bookmark…",
|
||||||
"즐겨찾기 추가됨": "Bookmark added",
|
"즐겨찾기 추가됨": "Bookmark added",
|
||||||
"즐겨찾기 표시줄": "Bookmarks bar",
|
"즐겨찾기 표시줄": "Bookmarks bar",
|
||||||
|
"즐겨찾기에 넣었습니다: {0}": "Added to favorites: {0}",
|
||||||
|
"즐겨찾기에 추가": "Add to favorites",
|
||||||
"즐겨찾기에서 빼기 (폴더는 그대로)": "Remove from favorites (the folder stays)",
|
"즐겨찾기에서 빼기 (폴더는 그대로)": "Remove from favorites (the folder stays)",
|
||||||
"지금": "Current",
|
"지금": "Current",
|
||||||
"지금 {0}%": "Now {0}%",
|
"지금 {0}%": "Now {0}%",
|
||||||
|
|
@ -2172,6 +2182,7 @@
|
||||||
"폴더": "Folder",
|
"폴더": "Folder",
|
||||||
"폴더 + 파일 목록": "Folders + file list",
|
"폴더 + 파일 목록": "Folders + file list",
|
||||||
"폴더 {0}개 (크기 빼고)": "{0} folders (size not counted)",
|
"폴더 {0}개 (크기 빼고)": "{0} folders (size not counted)",
|
||||||
|
"폴더 {0}개는 빼고 파일만 보냈습니다.": "Sent files only ({0} folders left out).",
|
||||||
"폴더 고르기": "Choose folder",
|
"폴더 고르기": "Choose folder",
|
||||||
"폴더 만들기": "New folder",
|
"폴더 만들기": "New folder",
|
||||||
"폴더 복사 · 이동 방법": "Folder copy · move method",
|
"폴더 복사 · 이동 방법": "Folder copy · move method",
|
||||||
|
|
@ -2190,6 +2201,7 @@
|
||||||
"폴더가 없거나 열 수 없습니다. Windows 의 네트워크 공유는 \\\\서버\\공유 형식입니다 (바꾼 경로: {0})": "The folder does not exist or cannot be opened. On Windows, network shares use the \\\\server\\share form (converted path: {0})",
|
"폴더가 없거나 열 수 없습니다. Windows 의 네트워크 공유는 \\\\서버\\공유 형식입니다 (바꾼 경로: {0})": "The folder does not exist or cannot be opened. On Windows, network shares use the \\\\server\\share form (converted path: {0})",
|
||||||
"폴더가 없거나 열 수 없습니다: {0}": "The folder doesn't exist or can't be opened: {0}",
|
"폴더가 없거나 열 수 없습니다: {0}": "The folder doesn't exist or can't be opened: {0}",
|
||||||
"폴더가 없습니다": "The folder does not exist",
|
"폴더가 없습니다": "The folder does not exist",
|
||||||
|
"폴더는 공유할 수 없습니다. 파일을 고르세요.": "Folders can't be shared. Choose files.",
|
||||||
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "Folders are shown, but the videos and files inside need this permission. Turn on JJ_MKVMaker on the permission screen and come back.",
|
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "Folders are shown, but the videos and files inside need this permission. Turn on JJ_MKVMaker on the permission screen and come back.",
|
||||||
"폴더를 만들 수 없습니다: {0}": "Cannot create the folder: {0}",
|
"폴더를 만들 수 없습니다: {0}": "Cannot create the folder: {0}",
|
||||||
"폴더를 자기 안으로 복사 · 이동할 수 없습니다": "Cannot copy or move a folder into itself",
|
"폴더를 자기 안으로 복사 · 이동할 수 없습니다": "Cannot copy or move a folder into itself",
|
||||||
|
|
|
||||||
|
|
@ -466,6 +466,7 @@
|
||||||
"{0}개 · 주소 {1}개": "{0}個・URL {1}個",
|
"{0}개 · 주소 {1}개": "{0}個・URL {1}個",
|
||||||
"{0}개 맞춤": "{0} 件を同期",
|
"{0}개 맞춤": "{0} 件を同期",
|
||||||
"{0}개 보임": "{0} 件表示",
|
"{0}개 보임": "{0} 件表示",
|
||||||
|
"{0}개 삭제": "{0} 件を削除",
|
||||||
"{0}개 선택": "{0}個選択",
|
"{0}개 선택": "{0}個選択",
|
||||||
"{0}개 중 {1}개를 지우지 못했습니다": "{0} 個中 {1} 個を削除できませんでした",
|
"{0}개 중 {1}개를 지우지 못했습니다": "{0} 個中 {1} 個を削除できませんでした",
|
||||||
"{0}개 지우고 맞추기": "{0} 個を削除して同期",
|
"{0}개 지우고 맞추기": "{0} 個を削除して同期",
|
||||||
|
|
@ -576,11 +577,15 @@
|
||||||
"결과 폴더 열기: {0}": "出力フォルダーを開く: {0}",
|
"결과 폴더 열기: {0}": "出力フォルダーを開く: {0}",
|
||||||
"결과물 이동 실패: {0} ({1})": "成果物の移動失敗: {0}({1})",
|
"결과물 이동 실패: {0} ({1})": "成果物の移動失敗: {0}({1})",
|
||||||
"경로": "パス",
|
"경로": "パス",
|
||||||
|
"경로 {0}개를 복사했습니다": "{0} 件のパスをコピーしました",
|
||||||
|
"경로 복사": "パスをコピー",
|
||||||
|
"경로 복사 ({0}개)": "パスをコピー ({0} 件)",
|
||||||
"경로 입력": "パスを入力",
|
"경로 입력": "パスを入力",
|
||||||
"경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "パスを入力 (例: D:\\動画・\\\\NAS\\共有)",
|
"경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "パスを入力 (例: D:\\動画・\\\\NAS\\共有)",
|
||||||
"경로 입력 · WebDAV": "パス入力 · WebDAV",
|
"경로 입력 · WebDAV": "パス入力 · WebDAV",
|
||||||
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "パスが長すぎてごみ箱に入れられません (Windows のごみ箱は 260 文字を超えるパスを受け付けません)。",
|
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "パスが長すぎてごみ箱に入れられません (Windows のごみ箱は 260 文字を超えるパスを受け付けません)。",
|
||||||
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "パスが長すぎます (Windows のごみ箱は 260 文字を超えるパスを受け付けません) - 完全に削除はできます",
|
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "パスが長すぎます (Windows のごみ箱は 260 文字を超えるパスを受け付けません) - 完全に削除はできます",
|
||||||
|
"경로를 복사했습니다: {0}": "パスをコピーしました: {0}",
|
||||||
"계속": "常時",
|
"계속": "常時",
|
||||||
"계속 (앱이 켜져 있는 동안)": "常時 (アプリの起動中)",
|
"계속 (앱이 켜져 있는 동안)": "常時 (アプリの起動中)",
|
||||||
"계속 받기": "ダウンロードを続ける",
|
"계속 받기": "ダウンロードを続ける",
|
||||||
|
|
@ -605,6 +610,9 @@
|
||||||
"골라서 시작": "選んで開始",
|
"골라서 시작": "選んで開始",
|
||||||
"공용 저장소 (Download) 를 쓸 수 없어 되돌릴 수 없습니다.": "共有ストレージ (Download) を使えないため戻せません。",
|
"공용 저장소 (Download) 를 쓸 수 없어 되돌릴 수 없습니다.": "共有ストレージ (Download) を使えないため戻せません。",
|
||||||
"공용 폴더에 받은 파일 (동영상 · 그림 등)": "共有フォルダーにダウンロードしたファイル (動画・画像など)",
|
"공용 폴더에 받은 파일 (동영상 · 그림 등)": "共有フォルダーにダウンロードしたファイル (動画・画像など)",
|
||||||
|
"공유": "共有",
|
||||||
|
"공유 (다른 앱으로 보내기)": "共有 (他のアプリに送る)",
|
||||||
|
"공유 창을 열 수 없습니다.": "共有画面を開けません。",
|
||||||
"관리 메뉴": "管理メニュー",
|
"관리 메뉴": "管理メニュー",
|
||||||
"관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)": "管理者権限で実行中のため Chrome エンジンを使えません (セキュリティ)",
|
"관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)": "管理者権限で実行中のため Chrome エンジンを使えません (セキュリティ)",
|
||||||
"관리자 권한으로 실행할 때만 쓰입니다. 끄면 (기본) 그때 Chrome 엔진을 시작하지 않고 Edge 를 권합니다.": "管理者権限で実行するときだけ使われます。オフ (既定) の場合、そのときは Chrome エンジンを起動せず Edge をおすすめします。",
|
"관리자 권한으로 실행할 때만 쓰입니다. 끄면 (기본) 그때 Chrome 엔진을 시작하지 않고 Edge 를 권합니다.": "管理者権限で実行するときだけ使われます。オフ (既定) の場合、そのときは Chrome エンジンを起動せず Edge をおすすめします。",
|
||||||
|
|
@ -1954,6 +1962,8 @@
|
||||||
"즐겨찾기 추가…": "ブックマークを追加…",
|
"즐겨찾기 추가…": "ブックマークを追加…",
|
||||||
"즐겨찾기 추가됨": "ブックマークを追加しました",
|
"즐겨찾기 추가됨": "ブックマークを追加しました",
|
||||||
"즐겨찾기 표시줄": "ブックマークバー",
|
"즐겨찾기 표시줄": "ブックマークバー",
|
||||||
|
"즐겨찾기에 넣었습니다: {0}": "お気に入りに追加しました: {0}",
|
||||||
|
"즐겨찾기에 추가": "お気に入りに追加",
|
||||||
"즐겨찾기에서 빼기 (폴더는 그대로)": "お気に入りから外す (フォルダーはそのまま)",
|
"즐겨찾기에서 빼기 (폴더는 그대로)": "お気に入りから外す (フォルダーはそのまま)",
|
||||||
"지금": "現在",
|
"지금": "現在",
|
||||||
"지금 {0}%": "現在 {0}%",
|
"지금 {0}%": "現在 {0}%",
|
||||||
|
|
@ -2172,6 +2182,7 @@
|
||||||
"폴더": "フォルダー",
|
"폴더": "フォルダー",
|
||||||
"폴더 + 파일 목록": "フォルダー + ファイル一覧",
|
"폴더 + 파일 목록": "フォルダー + ファイル一覧",
|
||||||
"폴더 {0}개 (크기 빼고)": "フォルダー {0} 件 (サイズ除く)",
|
"폴더 {0}개 (크기 빼고)": "フォルダー {0} 件 (サイズ除く)",
|
||||||
|
"폴더 {0}개는 빼고 파일만 보냈습니다.": "ファイルだけ送りました (フォルダー {0} 件は除外)。",
|
||||||
"폴더 고르기": "フォルダーを選択",
|
"폴더 고르기": "フォルダーを選択",
|
||||||
"폴더 만들기": "フォルダーを作成",
|
"폴더 만들기": "フォルダーを作成",
|
||||||
"폴더 복사 · 이동 방법": "フォルダーのコピー · 移動方法",
|
"폴더 복사 · 이동 방법": "フォルダーのコピー · 移動方法",
|
||||||
|
|
@ -2190,6 +2201,7 @@
|
||||||
"폴더가 없거나 열 수 없습니다. Windows 의 네트워크 공유는 \\\\서버\\공유 형식입니다 (바꾼 경로: {0})": "フォルダーが存在しないか開けません。Windows のネットワーク共有は \\\\サーバー\\共有 の形式です (変換したパス: {0})",
|
"폴더가 없거나 열 수 없습니다. Windows 의 네트워크 공유는 \\\\서버\\공유 형식입니다 (바꾼 경로: {0})": "フォルダーが存在しないか開けません。Windows のネットワーク共有は \\\\サーバー\\共有 の形式です (変換したパス: {0})",
|
||||||
"폴더가 없거나 열 수 없습니다: {0}": "フォルダーがないか、開けません: {0}",
|
"폴더가 없거나 열 수 없습니다: {0}": "フォルダーがないか、開けません: {0}",
|
||||||
"폴더가 없습니다": "フォルダーがありません",
|
"폴더가 없습니다": "フォルダーがありません",
|
||||||
|
"폴더는 공유할 수 없습니다. 파일을 고르세요.": "フォルダーは共有できません。ファイルを選んでください。",
|
||||||
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "フォルダーは見えても、中の動画・ファイルはこの権限が必要です。許可画面で JJ_MKVMaker をオンにして戻ってください。",
|
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "フォルダーは見えても、中の動画・ファイルはこの権限が必要です。許可画面で JJ_MKVMaker をオンにして戻ってください。",
|
||||||
"폴더를 만들 수 없습니다: {0}": "フォルダーを作成できません: {0}",
|
"폴더를 만들 수 없습니다: {0}": "フォルダーを作成できません: {0}",
|
||||||
"폴더를 자기 안으로 복사 · 이동할 수 없습니다": "フォルダーを自分自身の中にコピー · 移動できません",
|
"폴더를 자기 안으로 복사 · 이동할 수 없습니다": "フォルダーを自分自身の中にコピー · 移動できません",
|
||||||
|
|
|
||||||
|
|
@ -466,6 +466,7 @@
|
||||||
"{0}개 · 주소 {1}개": "{0} 个 · 地址 {1} 个",
|
"{0}개 · 주소 {1}개": "{0} 个 · 地址 {1} 个",
|
||||||
"{0}개 맞춤": "已同步 {0} 个",
|
"{0}개 맞춤": "已同步 {0} 个",
|
||||||
"{0}개 보임": "显示 {0} 项",
|
"{0}개 보임": "显示 {0} 项",
|
||||||
|
"{0}개 삭제": "删除 {0} 项",
|
||||||
"{0}개 선택": "已选 {0} 个",
|
"{0}개 선택": "已选 {0} 个",
|
||||||
"{0}개 중 {1}개를 지우지 못했습니다": "{0} 个中有 {1} 个无法删除",
|
"{0}개 중 {1}개를 지우지 못했습니다": "{0} 个中有 {1} 个无法删除",
|
||||||
"{0}개 지우고 맞추기": "删除 {0} 个并同步",
|
"{0}개 지우고 맞추기": "删除 {0} 个并同步",
|
||||||
|
|
@ -576,11 +577,15 @@
|
||||||
"결과 폴더 열기: {0}": "打开输出文件夹: {0}",
|
"결과 폴더 열기: {0}": "打开输出文件夹: {0}",
|
||||||
"결과물 이동 실패: {0} ({1})": "成品移动失败: {0}({1})",
|
"결과물 이동 실패: {0} ({1})": "成品移动失败: {0}({1})",
|
||||||
"경로": "路径",
|
"경로": "路径",
|
||||||
|
"경로 {0}개를 복사했습니다": "已复制 {0} 个路径",
|
||||||
|
"경로 복사": "复制路径",
|
||||||
|
"경로 복사 ({0}개)": "复制路径 ({0} 个)",
|
||||||
"경로 입력": "输入路径",
|
"경로 입력": "输入路径",
|
||||||
"경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "输入路径 (例: D:\\视频 · \\\\NAS\\共享)",
|
"경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "输入路径 (例: D:\\视频 · \\\\NAS\\共享)",
|
||||||
"경로 입력 · WebDAV": "输入路径 · WebDAV",
|
"경로 입력 · WebDAV": "输入路径 · WebDAV",
|
||||||
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "路径过长,无法放入回收站 (Windows 回收站不接受超过 260 个字符的路径)。",
|
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "路径过长,无法放入回收站 (Windows 回收站不接受超过 260 个字符的路径)。",
|
||||||
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "路径过长 (Windows 回收站不接受超过 260 个字符的路径) - 可以永久删除",
|
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "路径过长 (Windows 回收站不接受超过 260 个字符的路径) - 可以永久删除",
|
||||||
|
"경로를 복사했습니다: {0}": "已复制路径: {0}",
|
||||||
"계속": "持续",
|
"계속": "持续",
|
||||||
"계속 (앱이 켜져 있는 동안)": "持续 (应用运行期间)",
|
"계속 (앱이 켜져 있는 동안)": "持续 (应用运行期间)",
|
||||||
"계속 받기": "继续下载",
|
"계속 받기": "继续下载",
|
||||||
|
|
@ -605,6 +610,9 @@
|
||||||
"골라서 시작": "选择后开始",
|
"골라서 시작": "选择后开始",
|
||||||
"공용 저장소 (Download) 를 쓸 수 없어 되돌릴 수 없습니다.": "无法使用公共存储 (Download),因此无法回退。",
|
"공용 저장소 (Download) 를 쓸 수 없어 되돌릴 수 없습니다.": "无法使用公共存储 (Download),因此无法回退。",
|
||||||
"공용 폴더에 받은 파일 (동영상 · 그림 등)": "下载到公共文件夹的文件 (视频、图片等)",
|
"공용 폴더에 받은 파일 (동영상 · 그림 등)": "下载到公共文件夹的文件 (视频、图片等)",
|
||||||
|
"공유": "分享",
|
||||||
|
"공유 (다른 앱으로 보내기)": "分享 (发送到其他应用)",
|
||||||
|
"공유 창을 열 수 없습니다.": "无法打开分享窗口。",
|
||||||
"관리 메뉴": "管理菜单",
|
"관리 메뉴": "管理菜单",
|
||||||
"관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)": "应用正以管理员权限运行,无法使用 Chrome 引擎(安全原因)",
|
"관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)": "应用正以管理员权限运行,无法使用 Chrome 引擎(安全原因)",
|
||||||
"관리자 권한으로 실행할 때만 쓰입니다. 끄면 (기본) 그때 Chrome 엔진을 시작하지 않고 Edge 를 권합니다.": "仅在以管理员权限运行时使用。关闭时(默认)届时不启动 Chrome 引擎,并建议使用 Edge。",
|
"관리자 권한으로 실행할 때만 쓰입니다. 끄면 (기본) 그때 Chrome 엔진을 시작하지 않고 Edge 를 권합니다.": "仅在以管理员权限运行时使用。关闭时(默认)届时不启动 Chrome 引擎,并建议使用 Edge。",
|
||||||
|
|
@ -1954,6 +1962,8 @@
|
||||||
"즐겨찾기 추가…": "添加书签…",
|
"즐겨찾기 추가…": "添加书签…",
|
||||||
"즐겨찾기 추가됨": "已添加书签",
|
"즐겨찾기 추가됨": "已添加书签",
|
||||||
"즐겨찾기 표시줄": "书签栏",
|
"즐겨찾기 표시줄": "书签栏",
|
||||||
|
"즐겨찾기에 넣었습니다: {0}": "已添加到收藏: {0}",
|
||||||
|
"즐겨찾기에 추가": "添加到收藏",
|
||||||
"즐겨찾기에서 빼기 (폴더는 그대로)": "从收藏中移除(文件夹保留)",
|
"즐겨찾기에서 빼기 (폴더는 그대로)": "从收藏中移除(文件夹保留)",
|
||||||
"지금": "当前",
|
"지금": "当前",
|
||||||
"지금 {0}%": "当前 {0}%",
|
"지금 {0}%": "当前 {0}%",
|
||||||
|
|
@ -2172,6 +2182,7 @@
|
||||||
"폴더": "文件夹",
|
"폴더": "文件夹",
|
||||||
"폴더 + 파일 목록": "文件夹 + 文件列表",
|
"폴더 + 파일 목록": "文件夹 + 文件列表",
|
||||||
"폴더 {0}개 (크기 빼고)": "{0} 个文件夹(不计大小)",
|
"폴더 {0}개 (크기 빼고)": "{0} 个文件夹(不计大小)",
|
||||||
|
"폴더 {0}개는 빼고 파일만 보냈습니다.": "只发送了文件 (排除了 {0} 个文件夹)。",
|
||||||
"폴더 고르기": "选择文件夹",
|
"폴더 고르기": "选择文件夹",
|
||||||
"폴더 만들기": "新建文件夹",
|
"폴더 만들기": "新建文件夹",
|
||||||
"폴더 복사 · 이동 방법": "文件夹复制 · 移动方式",
|
"폴더 복사 · 이동 방법": "文件夹复制 · 移动方式",
|
||||||
|
|
@ -2190,6 +2201,7 @@
|
||||||
"폴더가 없거나 열 수 없습니다. Windows 의 네트워크 공유는 \\\\서버\\공유 형식입니다 (바꾼 경로: {0})": "文件夹不存在或无法打开。Windows 的网络共享格式为 \\\\服务器\\共享(转换后的路径:{0})",
|
"폴더가 없거나 열 수 없습니다. Windows 의 네트워크 공유는 \\\\서버\\공유 형식입니다 (바꾼 경로: {0})": "文件夹不存在或无法打开。Windows 的网络共享格式为 \\\\服务器\\共享(转换后的路径:{0})",
|
||||||
"폴더가 없거나 열 수 없습니다: {0}": "文件夹不存在或无法打开: {0}",
|
"폴더가 없거나 열 수 없습니다: {0}": "文件夹不存在或无法打开: {0}",
|
||||||
"폴더가 없습니다": "文件夹不存在",
|
"폴더가 없습니다": "文件夹不存在",
|
||||||
|
"폴더는 공유할 수 없습니다. 파일을 고르세요.": "文件夹无法分享。请选择文件。",
|
||||||
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "虽然能看到文件夹,但其中的视频和文件需要此权限。请在权限界面开启 JJ_MKVMaker 后返回。",
|
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "虽然能看到文件夹,但其中的视频和文件需要此权限。请在权限界面开启 JJ_MKVMaker 后返回。",
|
||||||
"폴더를 만들 수 없습니다: {0}": "无法创建文件夹: {0}",
|
"폴더를 만들 수 없습니다: {0}": "无法创建文件夹: {0}",
|
||||||
"폴더를 자기 안으로 복사 · 이동할 수 없습니다": "不能将文件夹复制或移动到其自身内部",
|
"폴더를 자기 안으로 복사 · 이동할 수 없습니다": "不能将文件夹复制或移动到其自身内部",
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,19 @@ class AndroidShell extends NoopShell {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 77: Android 공유 창 (MainActivity "shareFiles")
|
||||||
|
@override
|
||||||
|
bool get canShare => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> shareFiles(List<String> paths) async {
|
||||||
|
try {
|
||||||
|
return await _ch.invokeMethod<bool>('shareFiles', {'paths': paths}) ?? false;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 환경 설정의 확장자별 재생 프로그램: Android 는 'system' (기본 앱) 만 있다
|
/// 환경 설정의 확장자별 재생 프로그램: Android 는 'system' (기본 앱) 만 있다
|
||||||
@override
|
@override
|
||||||
Future<void> openExternal(String program, List<String> files) async {
|
Future<void> openExternal(String program, List<String> files) async {
|
||||||
|
|
|
||||||
|
|
@ -369,6 +369,13 @@ class DesktopShell with WindowListener implements AppShell {
|
||||||
Future<void> openUrl(String url, {String browser = 'system'}) => openInBrowser(url, browser);
|
Future<void> openUrl(String url, {String browser = 'system'}) => openInBrowser(url, browser);
|
||||||
|
|
||||||
/// 탐색기로 폴더를 열고 파일을 선택. 파일이 없으면 폴더만 (그것도 없으면 가장 가까운 위 폴더).
|
/// 탐색기로 폴더를 열고 파일을 선택. 파일이 없으면 폴더만 (그것도 없으면 가장 가까운 위 폴더).
|
||||||
|
/// 77: Windows 판은 공유 창을 아직 쓰지 않는다 (메뉴에 [공유] 를 두지 않음)
|
||||||
|
@override
|
||||||
|
bool get canShare => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> shareFiles(List<String> paths) async => false;
|
||||||
|
|
||||||
/// 기본 연결 프로그램 (탐색기에서 두 번 누른 것과 같음) 또는 "연결 프로그램" 고르기 창
|
/// 기본 연결 프로그램 (탐색기에서 두 번 누른 것과 같음) 또는 "연결 프로그램" 고르기 창
|
||||||
@override
|
@override
|
||||||
Future<bool> openWith(String path, {bool choose = false}) async {
|
Future<bool> openWith(String path, {bool choose = false}) async {
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,10 @@ abstract class AppShell {
|
||||||
/// 열지 못하면 false.
|
/// 열지 못하면 false.
|
||||||
Future<bool> openWith(String path, {bool choose = false});
|
Future<bool> openWith(String path, {bool choose = false});
|
||||||
|
|
||||||
|
/// 77: 파일을 다른 앱으로 보내기 (공유 창). 이 기기에서 못 하면 [canShare] 가 false
|
||||||
|
bool get canShare;
|
||||||
|
Future<bool> shareFiles(List<String> paths);
|
||||||
|
|
||||||
/// 설치된 외부 브라우저 ('chrome', 'firefox', 'edge', 'whale' 중)
|
/// 설치된 외부 브라우저 ('chrome', 'firefox', 'edge', 'whale' 중)
|
||||||
List<String> installedBrowsers();
|
List<String> installedBrowsers();
|
||||||
|
|
||||||
|
|
@ -139,6 +143,10 @@ class NoopShell implements AppShell {
|
||||||
@override
|
@override
|
||||||
Future<bool> openWith(String path, {bool choose = false}) async => false;
|
Future<bool> openWith(String path, {bool choose = false}) async => false;
|
||||||
@override
|
@override
|
||||||
|
bool get canShare => false;
|
||||||
|
@override
|
||||||
|
Future<bool> shareFiles(List<String> paths) async => false;
|
||||||
|
@override
|
||||||
List<String> installedBrowsers() => const [];
|
List<String> installedBrowsers() => const [];
|
||||||
@override
|
@override
|
||||||
Future<List<String>> missingTools() async => const [];
|
Future<List<String>> missingTools() async => const [];
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import 'package:path/path.dart' as p;
|
||||||
import '../app/app_controller.dart';
|
import '../app/app_controller.dart';
|
||||||
import '../app/copy_center.dart';
|
import '../app/copy_center.dart';
|
||||||
import '../app/live_sync.dart';
|
import '../app/live_sync.dart';
|
||||||
import '../app/settings.dart' show CopyTask;
|
import '../app/settings.dart' show CopyTask, FolderBookmark;
|
||||||
import '../app/transfer_job.dart';
|
import '../app/transfer_job.dart';
|
||||||
import '../core/secret_gate.dart';
|
import '../core/secret_gate.dart';
|
||||||
import '../core/sync_tools.dart';
|
import '../core/sync_tools.dart';
|
||||||
|
|
@ -628,6 +628,17 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 77: [경로 복사] 에 넣을 것 - 실제 경로 (Android 도 "SD 카드 ›" 가 아닌 /storage/…), WebDAV 는 서버 주소 (아이디 · 비밀번호는 빼고)
|
||||||
|
String _copyablePath(String path) {
|
||||||
|
if (!isDav(path)) return path;
|
||||||
|
final d = DavPath.parse(path);
|
||||||
|
try {
|
||||||
|
return DavRegistry.client(d.server).uriOf(d.rel).replace(userInfo: '').toString();
|
||||||
|
} catch (_) {
|
||||||
|
return _displayPath(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 화면에 보일 경로 (5 · 14): Android 는 "SD 카드 › JJ_sdtest › jj_mkv", WebDAV 는 "☁ 서버 › 폴더", Windows 는 그대로
|
/// 화면에 보일 경로 (5 · 14): Android 는 "SD 카드 › JJ_sdtest › jj_mkv", WebDAV 는 "☁ 서버 › 폴더", Windows 는 그대로
|
||||||
String _displayPath(String path) {
|
String _displayPath(String path) {
|
||||||
if (isDav(path)) {
|
if (isDav(path)) {
|
||||||
|
|
@ -1276,6 +1287,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
final viewable = viewer && !e.isDir &&
|
final viewable = viewer && !e.isDir &&
|
||||||
(c.settings.imageExts.contains(ext) || ext == 'pdf' || zipExtensions.contains(ext));
|
(c.settings.imageExts.contains(ext) || ext == 'pdf' || zipExtensions.contains(ext));
|
||||||
final dual = _dual;
|
final dual = _dual;
|
||||||
|
// 77: 누른 것이 표시한 것 중 하나면 표시한 것 모두에 (복사 · 이동 · 삭제 · 공유 · 경로 복사)
|
||||||
|
final targets = _isMarked(pane, e.path) && pane.marked.length > 1 ? pane.marked.toList() : [e.path];
|
||||||
|
final many = targets.length > 1;
|
||||||
// 25: Rsync 화면은 폴더만 보이므로 폴더에 맞는 것만 (고르기 · 열기 · 이름 변경 · 삭제 · 정보)
|
// 25: Rsync 화면은 폴더만 보이므로 폴더에 맞는 것만 (고르기 · 열기 · 이름 변경 · 삭제 · 정보)
|
||||||
final items = widget.rsync
|
final items = widget.rsync
|
||||||
? <(String, IconData, String)>[
|
? <(String, IconData, String)>[
|
||||||
|
|
@ -1289,6 +1303,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
('info', Icons.info_outline, tr('정보')),
|
('info', Icons.info_outline, tr('정보')),
|
||||||
]
|
]
|
||||||
: <(String, IconData, String)>[
|
: <(String, IconData, String)>[
|
||||||
|
// 77: 복사 · 이동 · 이름 변경 · 삭제 · 공유는 위의 아이콘 줄 ([_menuIcons]). 아래는 상황별 항목
|
||||||
// 74: 폰에서 범위 고르기 - 첫 항목을 표시한 뒤 마지막 항목을 길게 눌러
|
// 74: 폰에서 범위 고르기 - 첫 항목을 표시한 뒤 마지막 항목을 길게 눌러
|
||||||
if (_selecting && _canRange(pane, e)) ('range', Icons.unfold_more, tr('여기까지 범위 고르기')),
|
if (_selecting && _canRange(pane, e)) ('range', Icons.unfold_more, tr('여기까지 범위 고르기')),
|
||||||
if (e.isDir) ('open', Icons.folder_open, tr('열기')),
|
if (e.isDir) ('open', Icons.folder_open, tr('열기')),
|
||||||
|
|
@ -1303,23 +1318,41 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
('toPdf', Icons.picture_as_pdf_outlined,
|
('toPdf', Icons.picture_as_pdf_outlined,
|
||||||
images.length == 1 ? tr('PDF 로 만들기') : trf('그림 {0}장을 PDF 로 만들기', [images.length])),
|
images.length == 1 ? tr('PDF 로 만들기') : trf('그림 {0}장을 PDF 로 만들기', [images.length])),
|
||||||
('select', Icons.check_circle_outline, tr('선택')),
|
('select', Icons.check_circle_outline, tr('선택')),
|
||||||
('rename', Icons.drive_file_rename_outline, tr('이름 변경')),
|
// 두 창이면 아이콘 줄의 복사 · 이동은 다른 창으로 - 담아 두었다가 원하는 폴더에 붙여넣기는 여기
|
||||||
if (dual) ('copy', Icons.copy_outlined, tr('다른 창으로 복사')),
|
if (dual) ('clipCopy', Icons.content_copy, tr('복사 (붙여넣기로)')),
|
||||||
if (dual) ('move', Icons.drive_file_move_outline, tr('다른 창으로 이동')),
|
if (dual) ('clipMove', Icons.content_cut, tr('이동 (붙여넣기로)')),
|
||||||
// 어느 배치에서나: 담아 두었다가 원하는 폴더에서 붙여넣기
|
|
||||||
('clipCopy', Icons.content_copy, dual ? tr('복사 (붙여넣기로)') : tr('복사')),
|
|
||||||
('clipMove', Icons.content_cut, dual ? tr('이동 (붙여넣기로)') : tr('이동')),
|
|
||||||
if (_clip.isNotEmpty)
|
if (_clip.isNotEmpty)
|
||||||
('paste', Icons.content_paste, trf('{0} 에 붙여넣기 ({1}개)', [vBasename(e.isDir ? e.path : vDirname(e.path)), _clip.length])),
|
('paste', Icons.content_paste, trf('{0} 에 붙여넣기 ({1}개)', [vBasename(e.isDir ? e.path : vDirname(e.path)), _clip.length])),
|
||||||
('delete', Icons.delete_outline, tr('삭제')),
|
('copyPath', Icons.link, many ? trf('경로 복사 ({0}개)', [targets.length]) : tr('경로 복사')),
|
||||||
|
if (e.isDir && !c.settings.folderBookmarks.any((b) => !b.isPair && samePath(b.path, e.path)))
|
||||||
|
('bookmark', Icons.star_outline, tr('즐겨찾기에 추가')),
|
||||||
if (!dav && (!Platform.isAndroid || e.isDir)) ('reveal', Icons.folder_outlined, tr('파일 관리자에서 보기')),
|
if (!dav && (!Platform.isAndroid || e.isDir)) ('reveal', Icons.folder_outlined, tr('파일 관리자에서 보기')),
|
||||||
('info', Icons.info_outline, tr('정보')),
|
('info', Icons.info_outline, tr('정보')),
|
||||||
];
|
];
|
||||||
|
// 77: 위의 아이콘 줄 (X-plore · One Commander 처럼 자주 쓰는 것을 한 줄에)
|
||||||
|
final icons = widget.rsync
|
||||||
|
? const <(String, IconData, String, String, bool)>[]
|
||||||
|
: <(String, IconData, String, String, bool)>[
|
||||||
|
(dual ? 'copy' : 'clipCopy', Icons.copy_outlined, tr('복사'), dual ? tr('다른 창으로 복사') : tr('복사 (붙여넣기로)'), true),
|
||||||
|
(dual ? 'move' : 'clipMove', Icons.drive_file_move_outline, tr('이동'), dual ? tr('다른 창으로 이동') : tr('이동 (붙여넣기로)'), true),
|
||||||
|
('rename', Icons.drive_file_rename_outline, tr('이름 변경'), tr('이름 변경'), !many),
|
||||||
|
('delete', Icons.delete_outline, tr('삭제'), many ? trf('{0}개 삭제', [targets.length]) : tr('삭제'), true),
|
||||||
|
if (c.services.shell.canShare) ('share', Icons.share_outlined, tr('공유'), tr('공유 (다른 앱으로 보내기)'), targets.any((x) => !vIsDirSync(x))),
|
||||||
|
];
|
||||||
final size = MediaQuery.sizeOf(context);
|
final size = MediaQuery.sizeOf(context);
|
||||||
final pick = await showMenu<String>(
|
final pick = await showMenu<String>(
|
||||||
context: context,
|
context: context,
|
||||||
position: RelativeRect.fromLTRB(at.dx, at.dy, size.width - at.dx, size.height - at.dy),
|
position: RelativeRect.fromLTRB(at.dx, at.dy, size.width - at.dx, size.height - at.dy),
|
||||||
items: [
|
items: [
|
||||||
|
// 77: 여러 개를 골랐으면 무엇에 하는지 먼저
|
||||||
|
if (many)
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
enabled: false,
|
||||||
|
height: 32,
|
||||||
|
child: Text(trf('{0}개 항목', [targets.length]),
|
||||||
|
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: JjColors.accent)),
|
||||||
|
),
|
||||||
|
if (icons.isNotEmpty) ...[_MenuIconRow(icons), const PopupMenuDivider(height: 1)],
|
||||||
for (final (id, icon, label) in items)
|
for (final (id, icon, label) in items)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: id,
|
value: id,
|
||||||
|
|
@ -1371,7 +1404,19 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
case 'paste':
|
case 'paste':
|
||||||
await _paste(pane, e.isDir ? e.path : vDirname(e.path));
|
await _paste(pane, e.isDir ? e.path : vDirname(e.path));
|
||||||
case 'delete':
|
case 'delete':
|
||||||
await _delete(pane, [e.path]);
|
// 77: 메뉴 머리의 "n개 항목" 과 같게 (예전에는 표시한 것이 여럿이어도 누른 것 하나만 지웠다)
|
||||||
|
await _delete(pane, targets);
|
||||||
|
case 'share':
|
||||||
|
await _share(targets);
|
||||||
|
case 'copyPath':
|
||||||
|
final paths = targets.map(_copyablePath).toList();
|
||||||
|
await Clipboard.setData(ClipboardData(text: paths.join('\n')));
|
||||||
|
_snack(many ? trf('경로 {0}개를 복사했습니다', [paths.length]) : trf('경로를 복사했습니다: {0}', [paths.single]));
|
||||||
|
case 'bookmark':
|
||||||
|
final name = vBasename(e.path).isEmpty ? e.path : vBasename(e.path);
|
||||||
|
await c.updateSettings((s) => s.folderBookmarks = [...s.folderBookmarks, FolderBookmark(name: name, path: e.path)]);
|
||||||
|
_snack(trf('즐겨찾기에 넣었습니다: {0}', [name]));
|
||||||
|
setState(() {});
|
||||||
case 'view':
|
case 'view':
|
||||||
await _open(e.path, pane: pane);
|
await _open(e.path, pane: pane);
|
||||||
case 'toPdf':
|
case 'toPdf':
|
||||||
|
|
@ -1383,6 +1428,26 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 77: 공유 - 파일만 (폴더는 보낼 수 없어 빼고 알림). WebDAV 파일은 받아서
|
||||||
|
Future<void> _share(List<String> paths) async {
|
||||||
|
final files = [for (final x in paths) if (!vIsDirSync(x)) x];
|
||||||
|
if (files.isEmpty) {
|
||||||
|
_snack(tr('폴더는 공유할 수 없습니다. 파일을 고르세요.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final local = <String>[];
|
||||||
|
for (final f in files) {
|
||||||
|
final l = await _fetch(f);
|
||||||
|
if (l == null || !mounted) return;
|
||||||
|
local.add(l);
|
||||||
|
}
|
||||||
|
if (!await c.services.shell.shareFiles(local)) {
|
||||||
|
_snack(tr('공유 창을 열 수 없습니다.'));
|
||||||
|
} else if (files.length < paths.length) {
|
||||||
|
_snack(trf('폴더 {0}개는 빼고 파일만 보냈습니다.', [paths.length - files.length]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ───────── 기능 ─────────
|
// ───────── 기능 ─────────
|
||||||
|
|
||||||
_Pane get _pane => _panes[_active];
|
_Pane get _pane => _panes[_active];
|
||||||
|
|
@ -4266,3 +4331,57 @@ class _PathDialogState extends State<_PathDialog> {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 77: 길게 누르기 메뉴 위의 아이콘 줄 - (id, 아이콘, 짧은 이름, 풍선 도움말, 누를 수 있음).
|
||||||
|
/// 폰에서도 무엇인지 알 수 있게 아이콘 아래에 짧은 이름
|
||||||
|
class _MenuIconRow extends PopupMenuEntry<String> {
|
||||||
|
final List<(String, IconData, String, String, bool)> items;
|
||||||
|
const _MenuIconRow(this.items);
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get height => 60;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool represents(String? value) => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_MenuIconRow> createState() => _MenuIconRowState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MenuIconRowState extends State<_MenuIconRow> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||||
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
for (final (id, icon, label, tip, on) in widget.items)
|
||||||
|
Tooltip(
|
||||||
|
message: tip,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
onTap: on ? () => Navigator.pop(context, id) : null,
|
||||||
|
child: Semantics(
|
||||||
|
button: true,
|
||||||
|
enabled: on,
|
||||||
|
label: tip,
|
||||||
|
excludeSemantics: true,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: on ? 1 : 0.38,
|
||||||
|
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
|
Icon(icon, size: 20),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(label, maxLines: 1, style: const TextStyle(fontSize: 11)),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,8 @@ void main() {
|
||||||
// 길게 누르기 → 메뉴 → 다른 앱으로 열기 (고르기 창)
|
// 길게 누르기 → 메뉴 → 다른 앱으로 열기 (고르기 창)
|
||||||
await tester.longPress(find.text('doc.txt'));
|
await tester.longPress(find.text('doc.txt'));
|
||||||
await calm(tester);
|
await calm(tester);
|
||||||
expect(find.text('다른 창으로 복사'), findsOneWidget);
|
// 77: 복사 · 이동 · 이름 변경 · 삭제는 메뉴 위 아이콘 줄 (두 창이면 복사는 다른 창으로)
|
||||||
|
expect(find.byTooltip('다른 창으로 복사'), findsOneWidget);
|
||||||
expect(find.text('삭제'), findsWidgets);
|
expect(find.text('삭제'), findsWidgets);
|
||||||
await act(tester, () => tester.tap(find.text('다른 앱으로 열기')));
|
await act(tester, () => tester.tap(find.text('다른 앱으로 열기')));
|
||||||
await settle(tester, () => shell.opened.length == 2);
|
await settle(tester, () => shell.opened.length == 2);
|
||||||
|
|
@ -193,7 +194,7 @@ void main() {
|
||||||
// 이 배치에도 복사 · 이동: 길게 눌러 [복사] 로 담고 → 다른 폴더를 골라 [붙여넣기]
|
// 이 배치에도 복사 · 이동: 길게 눌러 [복사] 로 담고 → 다른 폴더를 골라 [붙여넣기]
|
||||||
await tester.longPress(find.text('inner.txt'));
|
await tester.longPress(find.text('inner.txt'));
|
||||||
await calm(tester);
|
await calm(tester);
|
||||||
expect(find.text('다른 창으로 복사'), findsNothing);
|
expect(find.byTooltip('다른 창으로 복사'), findsNothing);
|
||||||
await tester.tap(find.text('복사').last);
|
await tester.tap(find.text('복사').last);
|
||||||
await calm(tester);
|
await calm(tester);
|
||||||
await act(tester, () => tester.tap(find.text('..')));
|
await act(tester, () => tester.tap(find.text('..')));
|
||||||
|
|
@ -410,6 +411,69 @@ void main() {
|
||||||
expect(find.textContaining('3개 표시함'), findsOneWidget, reason: 'b.srt · c.mkv · doc.txt');
|
expect(find.textContaining('3개 표시함'), findsOneWidget, reason: 'b.srt · c.mkv · doc.txt');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('77: 길게 누르기 메뉴 - 위 아이콘 줄 · 여럿이면 "n개 항목" (이름 변경은 하나일 때만) · 경로 복사 · 즐겨찾기에 추가 · 삭제는 표시한 것 모두', (tester) async {
|
||||||
|
File(p.join(left, 'b.txt')).writeAsStringSync('b');
|
||||||
|
String? clip;
|
||||||
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
|
||||||
|
if (call.method == 'Clipboard.setData') clip = (call.arguments as Map)['text'] as String?;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null));
|
||||||
|
await open(tester);
|
||||||
|
await settle(tester, () => find.text('b.txt').evaluate().isNotEmpty);
|
||||||
|
|
||||||
|
// 하나: 머리 없음 · 아이콘 줄 · 경로 복사 (실제 경로)
|
||||||
|
await tester.longPress(find.text('doc.txt').first);
|
||||||
|
await calm(tester);
|
||||||
|
expect(find.textContaining('개 항목'), findsNothing);
|
||||||
|
for (final t in ['다른 창으로 복사', '다른 창으로 이동', '이름 변경', '삭제']) {
|
||||||
|
expect(find.byTooltip(t), findsWidgets, reason: t); // 이름 변경 · 삭제는 아래 버튼 줄에도
|
||||||
|
}
|
||||||
|
await tester.tap(find.text('경로 복사'));
|
||||||
|
await calm(tester);
|
||||||
|
expect(clip, p.join(left, 'doc.txt'));
|
||||||
|
|
||||||
|
// 여럿: "2개 항목" · 이름 변경은 눌리지 않음 · 경로 복사는 둘 다
|
||||||
|
await act(tester, () => tester.tap(find.text('선택').first));
|
||||||
|
await act(tester, () => tester.tap(markOf('doc.txt')));
|
||||||
|
await act(tester, () => tester.tap(markOf('b.txt')));
|
||||||
|
await tester.longPress(find.text('b.txt').first);
|
||||||
|
await calm(tester);
|
||||||
|
expect(find.text('2개 항목'), findsOneWidget);
|
||||||
|
expect(find.byTooltip('2개 삭제'), findsOneWidget);
|
||||||
|
await tester.tap(find.byTooltip('이름 변경').last);
|
||||||
|
await calm(tester);
|
||||||
|
expect(find.text('2개 항목'), findsOneWidget, reason: '이름 변경은 눌리지 않아 메뉴 그대로');
|
||||||
|
await tester.tap(find.text('경로 복사 (2개)'));
|
||||||
|
await calm(tester);
|
||||||
|
expect(clip!.split('\n'), unorderedEquals([p.join(left, 'doc.txt'), p.join(left, 'b.txt')]));
|
||||||
|
|
||||||
|
// 삭제 (아이콘 줄) 는 표시한 것 모두 - 확인 창에서 취소
|
||||||
|
await tester.longPress(find.text('b.txt').first);
|
||||||
|
await calm(tester);
|
||||||
|
await tester.tap(find.byTooltip('2개 삭제'));
|
||||||
|
await calm(tester);
|
||||||
|
expect(find.textContaining('2개 항목을'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('취소').last);
|
||||||
|
await calm(tester);
|
||||||
|
expect(File(p.join(left, 'b.txt')).existsSync(), isTrue);
|
||||||
|
// 선택 끝 (아래 알림이 [선택 끝] 을 가릴 수 있어 Esc)
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||||
|
await calm(tester);
|
||||||
|
|
||||||
|
// 폴더: 즐겨찾기에 추가 (넣은 뒤에는 메뉴에서 빠짐)
|
||||||
|
await tester.longPress(find.text('sub').first);
|
||||||
|
await calm(tester);
|
||||||
|
await tester.tap(find.text('즐겨찾기에 추가'));
|
||||||
|
await calm(tester);
|
||||||
|
expect(c.settings.folderBookmarks.map((x) => x.path), contains(p.join(left, 'sub')));
|
||||||
|
await tester.longPress(find.text('sub').first);
|
||||||
|
await calm(tester);
|
||||||
|
expect(find.text('즐겨찾기에 추가'), findsNothing);
|
||||||
|
await tester.tapAt(const Offset(5, 5));
|
||||||
|
await calm(tester);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('75: 창마다 뒤로 · 앞으로 - 버튼 · Alt+← / → · 마우스 4 · 5번', (tester) async {
|
testWidgets('75: 창마다 뒤로 · 앞으로 - 버튼 · Alt+← / → · 마우스 4 · 5번', (tester) async {
|
||||||
await open(tester);
|
await open(tester);
|
||||||
final sub = p.join(left, 'sub');
|
final sub = p.join(left, 'sub');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue