WIP X-plore 비교 5-2 시스템 폴더 보기 (Shizuku · 읽기 전용) (검토 대기)
관리자 결정 10/21: 1단계는 Shizuku 만 · 읽기 전용 · 처음 끔 · 경고 두 번 (켤 때 · 앱을 켠 뒤 처음 열 때). - Shizuku API 13.1.5 (MIT - 3a 지시의 "Apache-2.0" 이 아니라 실제 라이선스는 MIT, 고지에 MIT 원문) - shz://sys/<기기 경로>: 목록 · 정보 · 내용 읽기 (임시로 받아 읽고 지움) 만, 쓰기 · 지우기 · 옮기기 · 새 폴더는 막음 - 셸 명령은 고정된 셋 (목록 · stat · cat), 경로는 절대 경로만 · 따옴표 처리 - 탐색기 탭 [시스템 (읽기 전용)] → Android 폴더, 주황 띠, 꺼짐 · 허락 없음 안내 - 내 판단: Shizuku API 13 의 newProcess 는 숨겨져 있어 반사로 부름 (널리 쓰는 방법, 버전이 바뀌면 다시 확인) - 내 판단: 시스템 폴더 오류 띠에는 [서버 설정 고치기] 를 두지 않음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>xplore_p3b
parent
dc4bae5329
commit
8f6fcdd9ed
|
|
@ -110,4 +110,7 @@ dependencies {
|
|||
implementation("androidx.webkit:webkit:1.12.0")
|
||||
// 88: Android SMB (SMB2 · SMB3, SMB1 없음) - Apache-2.0. 고지: THIRD_PARTY_NOTICES_ANDROID · 라이선스 화면
|
||||
implementation("com.hierynomus:smbj:0.15.0")
|
||||
// X-plore 비교 5-2 시스템 폴더 보기 (Shizuku, 읽기 전용) - MIT (POM · 저장소 LICENSE). 고지: THIRD_PARTY_NOTICES_ANDROID · 라이선스 화면
|
||||
implementation("dev.rikka.shizuku:api:13.1.5")
|
||||
implementation("dev.rikka.shizuku:provider:13.1.5")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,6 +152,13 @@
|
|||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
<!-- X-plore 비교 5-2: Shizuku 가 이 앱에 셸 권한 연결을 건네는 곳 (Shizuku 앱이 있을 때만 쓰임, 읽기 전용으로만 씀) -->
|
||||
<provider
|
||||
android:name="rikka.shizuku.ShizukuProvider"
|
||||
android:authorities="${applicationId}.shizuku"
|
||||
android:multiprocess="false"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
|
|
|
|||
|
|
@ -166,6 +166,14 @@ class MainActivity : FlutterActivity() {
|
|||
Build.VERSION.SDK_INT < Build.VERSION_CODES.O || packageManager.canRequestPackageInstalls()
|
||||
)
|
||||
"openFolder" -> result.success(openFolder(call.argument<String>("path") ?: ""))
|
||||
// X-plore 비교 5-2 시스템 폴더 보기 (Shizuku, 읽기 전용 - ShizukuBridge 의 고정 명령 셋뿐)
|
||||
"shizukuStatus" -> result.success(ShizukuBridge.status())
|
||||
"shizukuRequest" -> shizukuRequest(result)
|
||||
"shizukuList" -> inBackground(result) { ShizukuBridge.list(call.argument<String>("path") ?: "") }
|
||||
"shizukuStat" -> inBackground(result) { ShizukuBridge.stat(call.argument<String>("path") ?: "") }
|
||||
"shizukuCopy" -> inBackground(result) {
|
||||
ShizukuBridge.copyTo(call.argument<String>("path") ?: "", call.argument<String>("dest") ?: "")
|
||||
}
|
||||
// X-plore 비교 5-2 앱 관리자 (사용자 결정 10/21, 관리자 판단: 모든 앱 목록 권한 없이 런처에 보이는 앱만)
|
||||
"launcherApps" -> inBackground(result) { launcherApps() }
|
||||
"appIcon" -> inBackground(result) { appIcon(call.argument<String>("package") ?: "", call.argument<Int>("size") ?: 96) }
|
||||
|
|
@ -615,6 +623,30 @@ class MainActivity : FlutterActivity() {
|
|||
null
|
||||
}
|
||||
|
||||
/// Shizuku 허락 창 - 답 (허락했는지) 을 돌려준다
|
||||
private var shizukuPending: MethodChannel.Result? = null
|
||||
private val shizukuListener = rikka.shizuku.Shizuku.OnRequestPermissionResultListener { code, grant ->
|
||||
if (code == 5202) {
|
||||
shizukuPending?.success(grant == PackageManager.PERMISSION_GRANTED)
|
||||
shizukuPending = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun shizukuRequest(result: MethodChannel.Result) {
|
||||
try {
|
||||
if (!rikka.shizuku.Shizuku.pingBinder()) return result.success(false)
|
||||
if (rikka.shizuku.Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) return result.success(true)
|
||||
shizukuPending?.success(false)
|
||||
shizukuPending = result
|
||||
rikka.shizuku.Shizuku.removeRequestPermissionResultListener(shizukuListener)
|
||||
rikka.shizuku.Shizuku.addRequestPermissionResultListener(shizukuListener)
|
||||
rikka.shizuku.Shizuku.requestPermission(5202)
|
||||
} catch (e: Throwable) {
|
||||
shizukuPending = null
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun inBackground(result: MethodChannel.Result, work: () -> Any?) {
|
||||
Thread {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
package com.jj.jj_mkvmaker
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import rikka.shizuku.Shizuku
|
||||
|
||||
/**
|
||||
* X-plore 비교 5-2 시스템 폴더 보기 (사용자 결정 10/21, 관리자 결정: 1단계는 Shizuku 만, **읽기 전용**).
|
||||
* Shizuku API (MIT) 로 adb 권한 (uid 2000) 의 셸을 빌려 Android 11 부터 막힌 Android/data · obb 를 본다.
|
||||
* - 부르는 명령은 아래 고정된 셋뿐 (목록 · 한 항목 정보 · 내용 읽기) - 사용자 글을 명령으로 넣지 않고, 경로는 따옴표로 감싼다
|
||||
* - 쓰기 · 지우기 · 이름 바꾸기 · 권한 바꾸기 명령은 두지 않는다
|
||||
*/
|
||||
object ShizukuBridge {
|
||||
/** Shizuku 가 켜져 있는지 · 이 앱이 허락을 받았는지 */
|
||||
fun status(): Map<String, Any> = try {
|
||||
val running = Shizuku.pingBinder()
|
||||
val granted = running && !Shizuku.isPreV11() && Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
|
||||
mapOf("running" to running, "granted" to granted)
|
||||
} catch (e: Throwable) {
|
||||
mapOf("running" to false, "granted" to false)
|
||||
}
|
||||
|
||||
private fun check(path: String) {
|
||||
require(path.startsWith("/") && !path.contains('\n') && !path.contains('\u0000')) { "bad path" }
|
||||
}
|
||||
|
||||
private fun quote(s: String) = "'" + s.replace("'", "'\\''") + "'"
|
||||
|
||||
/** Shizuku 의 셸 프로세스 (API 13 은 newProcess 를 숨겨 두어 반사로 부른다 - 널리 쓰는 방법) */
|
||||
private fun shell(script: String): Process {
|
||||
val m = Shizuku::class.java.getDeclaredMethod(
|
||||
"newProcess", Array<String>::class.java, Array<String>::class.java, String::class.java,
|
||||
)
|
||||
m.isAccessible = true
|
||||
return m.invoke(null, arrayOf("sh", "-c", script), null, null) as Process
|
||||
}
|
||||
|
||||
private fun runText(script: String): String {
|
||||
val p = shell(script)
|
||||
val out = p.inputStream.bufferedReader().readText()
|
||||
val err = p.errorStream.bufferedReader().readText()
|
||||
val code = p.waitFor()
|
||||
if (code != 0 && out.isEmpty()) throw IOException(err.trim().ifEmpty { "exit $code" })
|
||||
return out
|
||||
}
|
||||
|
||||
/** [path] 안의 항목: 한 줄에 "종류|크기|바뀐 때 (초)|이름" (toybox stat) */
|
||||
fun list(path: String): String {
|
||||
check(path)
|
||||
return runText(
|
||||
"cd ${quote(path)} && for f in * .[!.]* ..?*; do " +
|
||||
"[ -e \"\$f\" ] || [ -L \"\$f\" ] || continue; stat -c '%F|%s|%Y|%n' -- \"\$f\"; done",
|
||||
)
|
||||
}
|
||||
|
||||
/** [path] 하나의 정보 (없으면 빈 글) */
|
||||
fun stat(path: String): String {
|
||||
check(path)
|
||||
return try {
|
||||
runText("stat -c '%F|%s|%Y|%n' -- ${quote(path)}")
|
||||
} catch (e: IOException) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/** [path] 의 내용을 [dest] (앱 캐시 안) 에 복사 - 바이트 수 */
|
||||
fun copyTo(path: String, dest: String): Long {
|
||||
check(path)
|
||||
val p = shell("cat -- ${quote(path)}")
|
||||
FileOutputStream(dest).use { o -> p.inputStream.copyTo(o) }
|
||||
val err = p.errorStream.bufferedReader().readText()
|
||||
val code = p.waitFor()
|
||||
if (code != 0) {
|
||||
File(dest).delete()
|
||||
throw IOException(err.trim().ifEmpty { "exit $code" })
|
||||
}
|
||||
return File(dest).length()
|
||||
}
|
||||
}
|
||||
|
|
@ -311,6 +311,12 @@
|
|||
"SQLite 파일이 아니거나 깨졌습니다": "Not an SQLite file, or it is damaged",
|
||||
"SRT 형식이 아니거나 비어 있습니다.": "Not in SRT format or empty.",
|
||||
"SSH 서버 (NAS · 리눅스) 의 폴더를 파일 탐색기 · Rsync 화면에서 열고 복사 · 동기화합니다 (위쪽 탭). 비밀번호 · 개인 키 · 키 암호는 이 기기의 안전 저장소에만 둡니다. 처음 연결할 때 서버의 키 지문을 확인합니다.": "Open, copy and sync folders on SSH servers (NAS, Linux) in the File explorer and Rsync screens (tabs at the top). Passwords, private keys and key passphrases are kept only in this device's secure storage. The server's key fingerprint is checked on the first connection.",
|
||||
"Shizuku (adb 권한) 로 다른 앱의 폴더 (Android/data · obb) 를 볼 수 있게 됩니다. 그 안에는 다른 앱의 대화 · 로그인 정보 같은 개인 데이터가 있을 수 있습니다.": "With Shizuku (adb permission), you can view other apps' folders (Android/data · obb). They may contain personal data such as other apps' chats and login information.",
|
||||
"Shizuku 가 켜져 있지 않습니다 - Shizuku 앱을 설치하고 (무선 디버깅 · adb 로) 시작한 뒤 다시 누르세요": "Shizuku is not running - install the Shizuku app, start it (via wireless debugging or adb), then tap again",
|
||||
"Shizuku 앱이 있으면 Android/data · obb 처럼 막힌 폴더를 읽기만 합니다. 탐색기 위 탭 [시스템 (읽기 전용)]": "With the Shizuku app, reads blocked folders such as Android/data · obb (read only). Tab [System (read-only)] at the top of the file explorer",
|
||||
"Shizuku 에 연결하지 못했습니다: {0}": "Could not connect to Shizuku: {0}",
|
||||
"Shizuku 의 권한으로 Android/data · obb 처럼 막힌 폴더를 봅니다. 다른 앱의 데이터 (대화 · 로그인 정보 등) 가 있을 수 있습니다. 이 앱은 읽기만 하고 (쓰기 · 지우기 · 이름 바꾸기 없음), 꺼낸 파일은 고른 곳에만 둡니다.": "Views blocked folders such as Android/data · obb with Shizuku's permission. They may contain other apps' data (chats, login information, etc.). This app only reads (no writing, deleting or renaming), and exported files go only where you choose.",
|
||||
"Shizuku 허락이 없어 볼 수 없습니다": "Cannot view without Shizuku permission",
|
||||
"Stable Diffusion v1.5": "Stable Diffusion v1.5",
|
||||
"Stable Diffusion v1.5 는 CreativeML OpenRAIL-M 라이선스입니다. 상업적 사용을 포함해 쓸 수 있지만, 다음 용도로는 쓸 수 없습니다: 법을 어기는 일, 미성년자를 해치거나 이용하는 일, 남을 해치려는 거짓 정보, 개인 정보로 사람을 해치는 일, 차별 · 괴롭힘, 의료 · 법률 판단을 대신하는 일 등 (라이선스 부속서 A). 만든 그림에 대한 책임은 쓰는 사람에게 있습니다.": "Stable Diffusion v1.5 is licensed under CreativeML OpenRAIL-M. You may use it, including commercially, but not for: breaking the law, harming or exploiting minors, false information meant to harm others, harming people with personal information, discrimination or harassment, replacing medical or legal judgment, etc. (license Attachment A). You are responsible for the images you make.",
|
||||
"TAESD (빠른 디코더)": "TAESD (fast decoder)",
|
||||
|
|
@ -1299,6 +1305,7 @@
|
|||
"모든 작업 취소": "Cancel all jobs",
|
||||
"목": "Thu",
|
||||
"목록": "list",
|
||||
"목록 · 보기 · 꺼내기 (복사) 만 됩니다. 다른 앱의 데이터가 있을 수 있으니 꺼낸 파일을 조심해서 다루세요": "Only listing, viewing and exporting (copy) work. Other apps' data may be here, so handle exported files with care",
|
||||
"목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다": "Hides ad areas in lists · next to · on top of videos",
|
||||
"목록 글자 크기": "List text size",
|
||||
"목록 맨 아래의 [숨긴 항목 보이기] 나 환경 설정 > 파일 탐색기에서 다시 볼 수 있습니다.": "You can see them again with [Show hidden items] at the bottom of the list, or in Settings > File explorer.",
|
||||
|
|
@ -1771,11 +1778,18 @@
|
|||
"시드 {0}": "Seed {0}",
|
||||
"시리즈: file_001 · file_002, S01E01 · S01E02 처럼 번호만 다른 파일": "Series: files that differ only by number, like file_001 · file_002, S01E01 · S01E02",
|
||||
"시스템": "System",
|
||||
"시스템 (읽기 전용)": "System (read-only)",
|
||||
"시스템 DNS": "System DNS",
|
||||
"시스템 DNS (끄기)": "System DNS (off)",
|
||||
"시스템 DNS 로 바꾸기": "Switch to system DNS",
|
||||
"시스템 막대 보이기 (시계 · 알림 · 뒤로)": "Show system bars (clock, notifications, back)",
|
||||
"시스템 언어 따르기 ({0})": "Follow system language ({0})",
|
||||
"시스템 폴더 (Shizuku) - 읽기 전용": "System folder (Shizuku) - read-only",
|
||||
"시스템 폴더 (Shizuku) 는 읽기 전용입니다": "The system folder (Shizuku) is read-only",
|
||||
"시스템 폴더 (Shizuku) 는 읽기 전용입니다 (꺼내기 (복사) 만 할 수 있습니다)": "The system folder (Shizuku) is read-only (you can only export (copy))",
|
||||
"시스템 폴더 보기 (Shizuku · 읽기 전용)": "View system folders (Shizuku · read-only)",
|
||||
"시스템 폴더 보기 (읽기 전용)": "View system folders (read-only)",
|
||||
"시스템 폴더 보기를 켤까요?": "Turn on viewing system folders?",
|
||||
"시작": "Start",
|
||||
"시작 = 현재": "Start = now",
|
||||
"시작 · 웹 브라우저": "Startup · web browser",
|
||||
|
|
@ -1856,6 +1870,7 @@
|
|||
"안쪽 파일을 확인하는 중 …": "Checking files inside …",
|
||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)": "{0} files inside are newer on the receiving side - they are skipped when merging (they stay in the source)",
|
||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "{0} files inside are newer on the receiving side - merging overwrites them with older ones",
|
||||
"알고 봅니다": "I understand, view",
|
||||
"알고 실행합니다 (지울 목록이 실제와 다를 수 있음)": "I understand - run anyway (the delete list may differ from what actually happens)",
|
||||
"알리지 않음": "Don't notify",
|
||||
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "Notifications show as JJ_MKVMaker and open the app when clicked (current user only, no admin rights)",
|
||||
|
|
@ -2119,6 +2134,7 @@
|
|||
"위 ← 아래": "Top ← bottom",
|
||||
"위 → 아래": "Top → bottom",
|
||||
"위 ⇄ 아래": "Top ⇄ bottom",
|
||||
"위 내용을 알고 켭니다": "I understand the above and turn it on",
|
||||
"위 폴더": "Parent folder",
|
||||
"위 폴더로": "Go up",
|
||||
"위 폴더로 가기": "Go to a parent folder",
|
||||
|
|
@ -2191,6 +2207,7 @@
|
|||
"이 설치본에는 rsync 가 들어 있지 않습니다 (rsync 로 복사할 때 · 오른쪽 [내려받기] 로 받습니다)": "This install has no rsync (downloaded when copying with rsync · or use [Download] on the right)",
|
||||
"이 설치본에는 rsync 가 들어 있지 않습니다. 직접 지정하세요.": "This install has no rsync. Choose one yourself.",
|
||||
"이 앱 (APK) 에는 Tor 가 들어 있지 않습니다.": "This app (APK) does not contain Tor.",
|
||||
"이 앱은 읽기만 합니다 - 쓰기 · 지우기 · 이름 바꾸기 · 권한 바꾸기는 하지 않습니다. 꺼낸 (복사한) 파일은 고른 곳에만 둡니다.": "This app only reads - it does not write, delete, rename or change permissions. Exported (copied) files go only where you choose.",
|
||||
"이 앱의 파일 탐색기 목록에서만 빠집니다. 파일은 지우거나 옮기거나 이름을 바꾸지 않고, 다른 앱 · 갤러리에서는 그대로 보입니다.": "It is removed only from this app's file explorer list. Files are not deleted, moved or renamed, and other apps and the gallery still show them.",
|
||||
"이 언어 지우기": "Remove this language",
|
||||
"이 연결은 서명도 암호화도 되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 보거나 바꿀 수 있으니, NAS 설정에서 SMB 서명이나 SMB3 암호화를 켜는 것이 좋습니다": "This connection is neither signed nor encrypted ({0}). Others on the same network could read or change the files, so it's best to turn on SMB signing or SMB3 encryption in the NAS settings",
|
||||
|
|
|
|||
|
|
@ -311,6 +311,12 @@
|
|||
"SQLite 파일이 아니거나 깨졌습니다": "SQLite ファイルではないか、壊れています",
|
||||
"SRT 형식이 아니거나 비어 있습니다.": "SRT 形式ではないか、空です。",
|
||||
"SSH 서버 (NAS · 리눅스) 의 폴더를 파일 탐색기 · Rsync 화면에서 열고 복사 · 동기화합니다 (위쪽 탭). 비밀번호 · 개인 키 · 키 암호는 이 기기의 안전 저장소에만 둡니다. 처음 연결할 때 서버의 키 지문을 확인합니다.": "SSH サーバー (NAS・Linux) のフォルダーをファイルエクスプローラー・Rsync 画面で開き、コピー・同期します (上部のタブ)。パスワード・秘密鍵・鍵のパスフレーズはこの端末の安全なストレージにのみ保存します。初回接続時にサーバーの鍵のフィンガープリントを確認します。",
|
||||
"Shizuku (adb 권한) 로 다른 앱의 폴더 (Android/data · obb) 를 볼 수 있게 됩니다. 그 안에는 다른 앱의 대화 · 로그인 정보 같은 개인 데이터가 있을 수 있습니다.": "Shizuku (adb 権限) で他のアプリのフォルダー (Android/data・obb) を見られるようになります。そこには他のアプリのチャット・ログイン情報などの個人データがある場合があります。",
|
||||
"Shizuku 가 켜져 있지 않습니다 - Shizuku 앱을 설치하고 (무선 디버깅 · adb 로) 시작한 뒤 다시 누르세요": "Shizuku が起動していません - Shizuku アプリをインストールして (ワイヤレスデバッグ・adb で) 起動してから、もう一度押してください",
|
||||
"Shizuku 앱이 있으면 Android/data · obb 처럼 막힌 폴더를 읽기만 합니다. 탐색기 위 탭 [시스템 (읽기 전용)]": "Shizuku アプリがあれば Android/data・obb のような制限されたフォルダーを読み取りだけします。エクスプローラー上部のタブ [システム (読み取り専用)]",
|
||||
"Shizuku 에 연결하지 못했습니다: {0}": "Shizuku に接続できませんでした: {0}",
|
||||
"Shizuku 의 권한으로 Android/data · obb 처럼 막힌 폴더를 봅니다. 다른 앱의 데이터 (대화 · 로그인 정보 등) 가 있을 수 있습니다. 이 앱은 읽기만 하고 (쓰기 · 지우기 · 이름 바꾸기 없음), 꺼낸 파일은 고른 곳에만 둡니다.": "Shizuku の権限で Android/data・obb のような制限されたフォルダーを見ます。他のアプリのデータ (チャット・ログイン情報など) がある場合があります。このアプリは読み取りのみで (書き込み・削除・名前変更なし)、取り出したファイルは選んだ場所にだけ置きます。",
|
||||
"Shizuku 허락이 없어 볼 수 없습니다": "Shizuku の許可がないため表示できません",
|
||||
"Stable Diffusion v1.5": "Stable Diffusion v1.5",
|
||||
"Stable Diffusion v1.5 는 CreativeML OpenRAIL-M 라이선스입니다. 상업적 사용을 포함해 쓸 수 있지만, 다음 용도로는 쓸 수 없습니다: 법을 어기는 일, 미성년자를 해치거나 이용하는 일, 남을 해치려는 거짓 정보, 개인 정보로 사람을 해치는 일, 차별 · 괴롭힘, 의료 · 법률 판단을 대신하는 일 등 (라이선스 부속서 A). 만든 그림에 대한 책임은 쓰는 사람에게 있습니다.": "Stable Diffusion v1.5 は CreativeML OpenRAIL-M ライセンスです。商用を含めて使えますが、次の用途には使えません: 法律に違反すること、未成年者を害したり利用したりすること、他人を害する目的の虚偽情報、個人情報で人を害すること、差別・嫌がらせ、医療・法律の判断の代わりにすること など (ライセンス付属書 A)。作成した画像の責任は使う人にあります。",
|
||||
"TAESD (빠른 디코더)": "TAESD (高速デコーダー)",
|
||||
|
|
@ -1299,6 +1305,7 @@
|
|||
"모든 작업 취소": "すべての作業をキャンセル",
|
||||
"목": "木",
|
||||
"목록": "一覧",
|
||||
"목록 · 보기 · 꺼내기 (복사) 만 됩니다. 다른 앱의 데이터가 있을 수 있으니 꺼낸 파일을 조심해서 다루세요": "一覧・表示・取り出し (コピー) のみできます。他のアプリのデータがある場合があるので、取り出したファイルは注意して扱ってください",
|
||||
"목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다": "リスト・動画の横・動画の上に出る広告エリアを隠します",
|
||||
"목록 글자 크기": "一覧の文字サイズ",
|
||||
"목록 맨 아래의 [숨긴 항목 보이기] 나 환경 설정 > 파일 탐색기에서 다시 볼 수 있습니다.": "リスト最下部の [隠した項目を表示] または 設定 > ファイルエクスプローラー で再び表示できます。",
|
||||
|
|
@ -1771,11 +1778,18 @@
|
|||
"시드 {0}": "シード {0}",
|
||||
"시리즈: file_001 · file_002, S01E01 · S01E02 처럼 번호만 다른 파일": "シリーズ: file_001・file_002、S01E01・S01E02 のように番号だけ違うファイル",
|
||||
"시스템": "システム",
|
||||
"시스템 (읽기 전용)": "システム (読み取り専用)",
|
||||
"시스템 DNS": "システム DNS",
|
||||
"시스템 DNS (끄기)": "システム DNS (オフ)",
|
||||
"시스템 DNS 로 바꾸기": "システム DNS に切り替える",
|
||||
"시스템 막대 보이기 (시계 · 알림 · 뒤로)": "システムバーを表示 (時計・通知・戻る)",
|
||||
"시스템 언어 따르기 ({0})": "システムの言語に従う ({0})",
|
||||
"시스템 폴더 (Shizuku) - 읽기 전용": "システムフォルダー (Shizuku) - 読み取り専用",
|
||||
"시스템 폴더 (Shizuku) 는 읽기 전용입니다": "システムフォルダー (Shizuku) は読み取り専用です",
|
||||
"시스템 폴더 (Shizuku) 는 읽기 전용입니다 (꺼내기 (복사) 만 할 수 있습니다)": "システムフォルダー (Shizuku) は読み取り専用です (取り出し (コピー) のみできます)",
|
||||
"시스템 폴더 보기 (Shizuku · 읽기 전용)": "システムフォルダーを表示 (Shizuku・読み取り専用)",
|
||||
"시스템 폴더 보기 (읽기 전용)": "システムフォルダーを表示 (読み取り専用)",
|
||||
"시스템 폴더 보기를 켤까요?": "システムフォルダーの表示をオンにしますか?",
|
||||
"시작": "開始",
|
||||
"시작 = 현재": "開始 = 現在",
|
||||
"시작 · 웹 브라우저": "起動・ウェブブラウザー",
|
||||
|
|
@ -1856,6 +1870,7 @@
|
|||
"안쪽 파일을 확인하는 중 …": "中のファイルを確認中 …",
|
||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)": "中に受け取る側の方が新しいファイルが {0} 件 - 合わせてもスキップします (元に残ります)",
|
||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "中に受け取る側の方が新しいファイルが {0} 件 - 合わせると古いもので上書きします",
|
||||
"알고 봅니다": "理解して表示",
|
||||
"알고 실행합니다 (지울 목록이 실제와 다를 수 있음)": "理解した上で実行します (削除リストが実際と異なる場合があります)",
|
||||
"알리지 않음": "知らせない",
|
||||
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "通知が JJ_MKVMaker の名前で表示され、クリックするとアプリが開きます (管理者権限なしで現在のユーザーのみ)",
|
||||
|
|
@ -2119,6 +2134,7 @@
|
|||
"위 ← 아래": "上 ← 下",
|
||||
"위 → 아래": "上 → 下",
|
||||
"위 ⇄ 아래": "上 ⇄ 下",
|
||||
"위 내용을 알고 켭니다": "上記を理解してオンにします",
|
||||
"위 폴더": "上のフォルダー",
|
||||
"위 폴더로": "上のフォルダーへ",
|
||||
"위 폴더로 가기": "上のフォルダーへ移動",
|
||||
|
|
@ -2191,6 +2207,7 @@
|
|||
"이 설치본에는 rsync 가 들어 있지 않습니다 (rsync 로 복사할 때 · 오른쪽 [내려받기] 로 받습니다)": "このインストールには rsync が含まれていません (rsync でコピーするとき · 右の [ダウンロード] で取得します)",
|
||||
"이 설치본에는 rsync 가 들어 있지 않습니다. 직접 지정하세요.": "このインストールには rsync が含まれていません。直接指定してください。",
|
||||
"이 앱 (APK) 에는 Tor 가 들어 있지 않습니다.": "このアプリ (APK) には Tor が入っていません。",
|
||||
"이 앱은 읽기만 합니다 - 쓰기 · 지우기 · 이름 바꾸기 · 권한 바꾸기는 하지 않습니다. 꺼낸 (복사한) 파일은 고른 곳에만 둡니다.": "このアプリは読み取りのみです - 書き込み・削除・名前変更・権限変更はしません。取り出した (コピーした) ファイルは選んだ場所にだけ置きます。",
|
||||
"이 앱의 파일 탐색기 목록에서만 빠집니다. 파일은 지우거나 옮기거나 이름을 바꾸지 않고, 다른 앱 · 갤러리에서는 그대로 보입니다.": "このアプリのファイルエクスプローラーの一覧から外れるだけです。ファイルは削除・移動・名前変更されず、他のアプリやギャラリーではそのまま表示されます。",
|
||||
"이 언어 지우기": "この言語を削除",
|
||||
"이 연결은 서명도 암호화도 되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 보거나 바꿀 수 있으니, NAS 설정에서 SMB 서명이나 SMB3 암호화를 켜는 것이 좋습니다": "この接続は署名も暗号化もされません ({0})。同じネットワークの他の人がファイルの内容を見たり変えたりできるため、NAS の設定で SMB 署名か SMB3 暗号化をオンにすることをおすすめします",
|
||||
|
|
|
|||
|
|
@ -311,6 +311,12 @@
|
|||
"SQLite 파일이 아니거나 깨졌습니다": "不是 SQLite 文件或已损坏",
|
||||
"SRT 형식이 아니거나 비어 있습니다.": "不是 SRT 格式或为空。",
|
||||
"SSH 서버 (NAS · 리눅스) 의 폴더를 파일 탐색기 · Rsync 화면에서 열고 복사 · 동기화합니다 (위쪽 탭). 비밀번호 · 개인 키 · 키 암호는 이 기기의 안전 저장소에만 둡니다. 처음 연결할 때 서버의 키 지문을 확인합니다.": "在文件资源管理器和 Rsync 界面中打开、复制和同步 SSH 服务器 (NAS · Linux) 上的文件夹 (顶部标签)。密码、私钥和私钥密码仅保存在本设备的安全存储中。首次连接时会确认服务器的密钥指纹。",
|
||||
"Shizuku (adb 권한) 로 다른 앱의 폴더 (Android/data · obb) 를 볼 수 있게 됩니다. 그 안에는 다른 앱의 대화 · 로그인 정보 같은 개인 데이터가 있을 수 있습니다.": "通过 Shizuku (adb 权限) 可以查看其他应用的文件夹 (Android/data · obb)。其中可能包含其他应用的聊天、登录信息等个人数据。",
|
||||
"Shizuku 가 켜져 있지 않습니다 - Shizuku 앱을 설치하고 (무선 디버깅 · adb 로) 시작한 뒤 다시 누르세요": "Shizuku 未运行 - 请安装 Shizuku 应用并 (通过无线调试或 adb) 启动后再点一次",
|
||||
"Shizuku 앱이 있으면 Android/data · obb 처럼 막힌 폴더를 읽기만 합니다. 탐색기 위 탭 [시스템 (읽기 전용)]": "如有 Shizuku 应用,可只读查看 Android/data · obb 等受限文件夹。文件管理器顶部标签 [系统 (只读)]",
|
||||
"Shizuku 에 연결하지 못했습니다: {0}": "无法连接到 Shizuku: {0}",
|
||||
"Shizuku 의 권한으로 Android/data · obb 처럼 막힌 폴더를 봅니다. 다른 앱의 데이터 (대화 · 로그인 정보 등) 가 있을 수 있습니다. 이 앱은 읽기만 하고 (쓰기 · 지우기 · 이름 바꾸기 없음), 꺼낸 파일은 고른 곳에만 둡니다.": "借助 Shizuku 的权限查看 Android/data · obb 等受限文件夹。其中可能有其他应用的数据 (聊天、登录信息等)。本应用只读取 (不写入、不删除、不重命名),导出的文件只放在您选择的位置。",
|
||||
"Shizuku 허락이 없어 볼 수 없습니다": "没有 Shizuku 授权,无法查看",
|
||||
"Stable Diffusion v1.5": "Stable Diffusion v1.5",
|
||||
"Stable Diffusion v1.5 는 CreativeML OpenRAIL-M 라이선스입니다. 상업적 사용을 포함해 쓸 수 있지만, 다음 용도로는 쓸 수 없습니다: 법을 어기는 일, 미성년자를 해치거나 이용하는 일, 남을 해치려는 거짓 정보, 개인 정보로 사람을 해치는 일, 차별 · 괴롭힘, 의료 · 법률 판단을 대신하는 일 등 (라이선스 부속서 A). 만든 그림에 대한 책임은 쓰는 사람에게 있습니다.": "Stable Diffusion v1.5 采用 CreativeML OpenRAIL-M 许可证。可用于包括商业用途在内的场合,但不得用于: 违法行为、伤害或利用未成年人、意在伤害他人的虚假信息、利用个人信息伤害他人、歧视 · 骚扰、代替医疗 · 法律判断等 (许可证附件 A)。生成图片的责任由使用者承担。",
|
||||
"TAESD (빠른 디코더)": "TAESD (快速解码器)",
|
||||
|
|
@ -1299,6 +1305,7 @@
|
|||
"모든 작업 취소": "取消所有任务",
|
||||
"목": "四",
|
||||
"목록": "列表",
|
||||
"목록 · 보기 · 꺼내기 (복사) 만 됩니다. 다른 앱의 데이터가 있을 수 있으니 꺼낸 파일을 조심해서 다루세요": "只能列出、查看和导出 (复制)。这里可能有其他应用的数据,请谨慎处理导出的文件",
|
||||
"목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다": "隐藏列表 · 视频旁 · 视频上方出现的广告区域",
|
||||
"목록 글자 크기": "列表文字大小",
|
||||
"목록 맨 아래의 [숨긴 항목 보이기] 나 환경 설정 > 파일 탐색기에서 다시 볼 수 있습니다.": "可通过列表底部的 [显示隐藏项] 或 设置 > 文件管理器 重新查看。",
|
||||
|
|
@ -1771,11 +1778,18 @@
|
|||
"시드 {0}": "种子 {0}",
|
||||
"시리즈: file_001 · file_002, S01E01 · S01E02 처럼 번호만 다른 파일": "系列: 像 file_001 · file_002、S01E01 · S01E02 这样仅编号不同的文件",
|
||||
"시스템": "系统",
|
||||
"시스템 (읽기 전용)": "系统 (只读)",
|
||||
"시스템 DNS": "系统 DNS",
|
||||
"시스템 DNS (끄기)": "系统 DNS (关闭)",
|
||||
"시스템 DNS 로 바꾸기": "切换为系统 DNS",
|
||||
"시스템 막대 보이기 (시계 · 알림 · 뒤로)": "显示系统栏 (时钟 · 通知 · 返回)",
|
||||
"시스템 언어 따르기 ({0})": "跟随系统语言 ({0})",
|
||||
"시스템 폴더 (Shizuku) - 읽기 전용": "系统文件夹 (Shizuku) - 只读",
|
||||
"시스템 폴더 (Shizuku) 는 읽기 전용입니다": "系统文件夹 (Shizuku) 为只读",
|
||||
"시스템 폴더 (Shizuku) 는 읽기 전용입니다 (꺼내기 (복사) 만 할 수 있습니다)": "系统文件夹 (Shizuku) 为只读 (只能导出 (复制))",
|
||||
"시스템 폴더 보기 (Shizuku · 읽기 전용)": "查看系统文件夹 (Shizuku · 只读)",
|
||||
"시스템 폴더 보기 (읽기 전용)": "查看系统文件夹 (只读)",
|
||||
"시스템 폴더 보기를 켤까요?": "要开启查看系统文件夹吗?",
|
||||
"시작": "开始",
|
||||
"시작 = 현재": "开始 = 当前",
|
||||
"시작 · 웹 브라우저": "启动 · 网页浏览器",
|
||||
|
|
@ -1856,6 +1870,7 @@
|
|||
"안쪽 파일을 확인하는 중 …": "正在检查里面的文件 …",
|
||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)": "里面有 {0} 个文件在接收端较新 - 合并时会跳过(保留在源中)",
|
||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "里面有 {0} 个文件在接收端较新 - 合并会用旧文件覆盖",
|
||||
"알고 봅니다": "我已了解,查看",
|
||||
"알고 실행합니다 (지울 목록이 실제와 다를 수 있음)": "我已了解并执行 (删除列表可能与实际不同)",
|
||||
"알리지 않음": "不提醒",
|
||||
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "通知以 JJ_MKVMaker 名义显示,点击即可打开应用 (仅当前用户,无需管理员权限)",
|
||||
|
|
@ -2119,6 +2134,7 @@
|
|||
"위 ← 아래": "上 ← 下",
|
||||
"위 → 아래": "上 → 下",
|
||||
"위 ⇄ 아래": "上 ⇄ 下",
|
||||
"위 내용을 알고 켭니다": "我已了解以上内容并开启",
|
||||
"위 폴더": "上级文件夹",
|
||||
"위 폴더로": "返回上级文件夹",
|
||||
"위 폴더로 가기": "转到上级文件夹",
|
||||
|
|
@ -2191,6 +2207,7 @@
|
|||
"이 설치본에는 rsync 가 들어 있지 않습니다 (rsync 로 복사할 때 · 오른쪽 [내려받기] 로 받습니다)": "此安装中没有 rsync (使用 rsync 复制时 · 或用右侧 [下载] 获取)",
|
||||
"이 설치본에는 rsync 가 들어 있지 않습니다. 직접 지정하세요.": "此安装中没有 rsync。请自行指定。",
|
||||
"이 앱 (APK) 에는 Tor 가 들어 있지 않습니다.": "此应用 (APK) 未包含 Tor。",
|
||||
"이 앱은 읽기만 합니다 - 쓰기 · 지우기 · 이름 바꾸기 · 권한 바꾸기는 하지 않습니다. 꺼낸 (복사한) 파일은 고른 곳에만 둡니다.": "本应用只读取 - 不写入、不删除、不重命名、不更改权限。导出 (复制) 的文件只放在您选择的位置。",
|
||||
"이 앱의 파일 탐색기 목록에서만 빠집니다. 파일은 지우거나 옮기거나 이름을 바꾸지 않고, 다른 앱 · 갤러리에서는 그대로 보입니다.": "仅从本应用的文件管理器列表中移除。文件不会被删除、移动或重命名,其他应用和图库中仍然可见。",
|
||||
"이 언어 지우기": "删除此语言",
|
||||
"이 연결은 서명도 암호화도 되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 보거나 바꿀 수 있으니, NAS 설정에서 SMB 서명이나 SMB3 암호화를 켜는 것이 좋습니다": "此连接既未签名也未加密 ({0})。同一网络中的其他人可能查看或篡改文件内容,建议在 NAS 设置中开启 SMB 签名或 SMB3 加密",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
시스템 폴더 보기 (Android, 읽기 전용) - Shizuku API 13.1.5 (api · provider · aidl · shared). Android 판에만 들어 있습니다.
|
||||
https://github.com/RikkaApps/Shizuku-API (Maven: dev.rikka.shizuku, 저장소 · POM 모두 MIT License)
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 RikkaW
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -35,6 +35,8 @@ JJ_MKVMaker (Android) - 포함된 오픈 소스 구성 요소 및 라이선스
|
|||
- smbj 0.15.0 (SMB, SMB2 · SMB3) Apache-2.0 https://github.com/hierynomus/smbj
|
||||
함께: asn-one 0.6.0 (Apache-2.0) · Bouncy Castle bcprov 1.85.2 (MIT) · SLF4J API 2.0.18 (MIT) · MBassador 1.3.2 (MIT).
|
||||
원문: 앱 안 "오픈 소스 라이선스" 의 smbj (SMBJ_LICENSES.txt - Apache License 2.0 전문 포함)
|
||||
- Shizuku API 13.1.5 (시스템 폴더 보기, 읽기 전용) MIT https://github.com/RikkaApps/Shizuku-API
|
||||
Copyright (c) 2021 RikkaW. 원문: 앱 안 "오픈 소스 라이선스" 의 Shizuku API (SHIZUKU_LICENSE.txt)
|
||||
Copyright (c) 2022 xuty. 함께 쓰는 pointycastle (MIT, 앱의 암호 보관함 암호화 (AES-256-GCM) 에도 씀) · pinenacl (MIT) · asn1lib (BSD-3-Clause) 의 원문은 [패키지 라이선스]
|
||||
- qr 3.0.2 (Wi-Fi 공유 접속 QR) BSD-3-Clause https://pub.dev/packages/qr
|
||||
Copyright 2014, the Dart QR project authors.
|
||||
|
|
|
|||
|
|
@ -489,6 +489,9 @@ class AppSettings {
|
|||
/// 설정을 처음 상태로 돌려도 보관함은 그대로 (끄기만 됨)
|
||||
bool vaultEnabled = false;
|
||||
|
||||
/// X-plore 비교 5-2 시스템 폴더 보기 (Shizuku, 읽기 전용, 사용자 결정 10/21 · 관리자 결정: 처음 끔 · 켤 때마다 경고)
|
||||
bool shizukuEnabled = false;
|
||||
|
||||
/// [마스터로도 열기] - 보관함 열쇠 한 벌을 안전 저장소 (마스터 비밀번호로 막힘) 에 둔다. 처음 끔. 마스터를 초기화하면 그 한 벌은 지워진다
|
||||
bool vaultMasterCopy = false;
|
||||
|
||||
|
|
@ -897,6 +900,7 @@ class AppSettings {
|
|||
'explorerRemoteThumbCacheMb': explorerRemoteThumbCacheMb,
|
||||
'explorerKeyOverrides': explorerKeyOverrides,
|
||||
'vaultEnabled': vaultEnabled,
|
||||
'shizukuEnabled': shizukuEnabled,
|
||||
'vaultMasterCopy': vaultMasterCopy,
|
||||
'vaultAutoLockSeconds': vaultAutoLockSeconds,
|
||||
'explorerFolderSorts': explorerFolderSorts,
|
||||
|
|
@ -1144,6 +1148,7 @@ class AppSettings {
|
|||
..explorerSortByKind = j['explorerSortByKind'] == true
|
||||
..explorerRemoteThumbs = const {'off', 'wifi', 'always'}.contains(j['explorerRemoteThumbs']) ? j['explorerRemoteThumbs'] as String : 'wifi'
|
||||
..vaultEnabled = j['vaultEnabled'] == true
|
||||
..shizukuEnabled = j['shizukuEnabled'] == true
|
||||
..vaultMasterCopy = j['vaultMasterCopy'] == true
|
||||
..vaultAutoLockSeconds = j['vaultAutoLockSeconds'] is num ? (j['vaultAutoLockSeconds'] as num).round().clamp(0, 3600) : 60
|
||||
..explorerKeyOverrides = j['explorerKeyOverrides'] is Map
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../l10n/tr.dart';
|
||||
import 'remote_fs.dart';
|
||||
import 'webdav.dart' show DavItem;
|
||||
|
||||
/// X-plore 비교 5-2 시스템 폴더 보기 (사용자 결정 10/21, 관리자 결정: 1단계는 Shizuku 만 · **읽기 전용** · 처음 끔).
|
||||
/// 경로: `shz://sys/<기기 절대 경로>` (예 shz://sys/storage/emulated/0/Android/data). 목록 · 정보 · 내용 읽기만 하고,
|
||||
/// 쓰기 · 지우기 · 옮기기는 [ShizukuReadOnlyException] (탐색기도 그 전에 막는다 - 압축 안처럼)
|
||||
const shizukuScheme = 'shz://';
|
||||
const shizukuServer = 'sys';
|
||||
|
||||
bool isShizuku(String path) => path.startsWith(shizukuScheme);
|
||||
|
||||
/// 시스템 폴더 보기를 쓰는 기기 (Android - 시험에서 바꿈)
|
||||
bool Function() shizukuSupported = () => Platform.isAndroid;
|
||||
|
||||
/// 탐색기 탭이 처음 여는 곳 (Android 11 부터 막힌 Android/data · obb 가 있는 폴더)
|
||||
String shizukuHome(String storageRoot) => '$shizukuScheme$shizukuServer${storageRoot.replaceAll('\\', '/')}/Android';
|
||||
|
||||
/// 기기와 이야기하는 쪽 (시험에서 바꿔 끼움)
|
||||
abstract class ShizukuBackend {
|
||||
/// Shizuku 가 켜져 있는지 · 이 앱이 허락을 받았는지
|
||||
Future<({bool running, bool granted})> status();
|
||||
|
||||
/// Shizuku 허락 창 (허락하면 true)
|
||||
Future<bool> request();
|
||||
|
||||
/// "종류|크기|바뀐 때 (초)|이름" 줄들
|
||||
Future<String> list(String path);
|
||||
|
||||
/// 한 항목의 "종류|크기|바뀐 때|이름" (없으면 빈 글)
|
||||
Future<String> stat(String path);
|
||||
|
||||
/// 내용을 [dest] 에 복사 (바이트 수)
|
||||
Future<int> copyTo(String path, String dest);
|
||||
}
|
||||
|
||||
class AndroidShizuku implements ShizukuBackend {
|
||||
static const _ch = MethodChannel('jj_mkvmaker/android');
|
||||
|
||||
@override
|
||||
Future<({bool running, bool granted})> status() async {
|
||||
final m = await _ch.invokeMethod<Map<Object?, Object?>>('shizukuStatus') ?? const {};
|
||||
return (running: m['running'] == true, granted: m['granted'] == true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> request() async => await _ch.invokeMethod<bool>('shizukuRequest') ?? false;
|
||||
|
||||
@override
|
||||
Future<String> list(String path) async => await _ch.invokeMethod<String>('shizukuList', {'path': path}) ?? '';
|
||||
|
||||
@override
|
||||
Future<String> stat(String path) async => await _ch.invokeMethod<String>('shizukuStat', {'path': path}) ?? '';
|
||||
|
||||
@override
|
||||
Future<int> copyTo(String path, String dest) async =>
|
||||
(await _ch.invokeMethod<num>('shizukuCopy', {'path': path, 'dest': dest}))?.toInt() ?? 0;
|
||||
}
|
||||
|
||||
class ShizukuReadOnlyException extends FileSystemException {
|
||||
ShizukuReadOnlyException(String path) : super(tr('시스템 폴더 (Shizuku) 는 읽기 전용입니다'), path);
|
||||
}
|
||||
|
||||
/// toybox `stat -c '%F|%s|%Y|%n'` 한 줄 → (폴더인지, 크기, 바뀐 때, 이름). 틀리면 null
|
||||
(bool, int, DateTime, String)? parseShizukuStat(String line) {
|
||||
final parts = line.split('|');
|
||||
if (parts.length < 4) return null;
|
||||
final name = parts.sublist(3).join('|'); // 이름에 | 가 있어도
|
||||
final size = int.tryParse(parts[1].trim());
|
||||
final secs = int.tryParse(parts[2].trim());
|
||||
if (size == null || secs == null || name.isEmpty) return null;
|
||||
return (parts[0].trim() == 'directory', size, DateTime.fromMillisecondsSinceEpoch(secs * 1000), name);
|
||||
}
|
||||
|
||||
/// [RemoteClient] 로 탐색기 · 복사 (꺼내기) · 보기가 그대로 쓰게. rel = 기기 절대 경로
|
||||
class ShizukuRemote implements RemoteClient {
|
||||
static ShizukuBackend backend = AndroidShizuku();
|
||||
|
||||
/// 내용을 잠깐 받아 둘 앱 전용 폴더 (시험에서 바꿈)
|
||||
static Future<String> Function() tempDir = () async => Directory.systemTemp.path;
|
||||
|
||||
static final instance = ShizukuRemote();
|
||||
|
||||
String _abs(String rel) => rel.isEmpty ? '/' : rel;
|
||||
|
||||
@override
|
||||
Future<List<DavItem>> list(String rel) async {
|
||||
final out = <DavItem>[];
|
||||
for (final line in (await backend.list(_abs(rel))).split('\n')) {
|
||||
final s = parseShizukuStat(line.trimRight());
|
||||
if (s == null) continue;
|
||||
final (dir, size, modified, name) = s;
|
||||
if (name == '.' || name == '..') continue;
|
||||
out.add(DavItem(rel == '/' ? '/$name' : '$rel/$name', isDir: dir, size: dir ? 0 : size, modified: modified));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DavItem?> stat(String rel) async {
|
||||
final s = parseShizukuStat((await backend.stat(_abs(rel))).trim());
|
||||
if (s == null) return null;
|
||||
return DavItem(rel, isDir: s.$1, size: s.$1 ? 0 : s.$2, modified: s.$3);
|
||||
}
|
||||
|
||||
/// 앱 전용 임시 폴더에 받아 두고 읽은 뒤 지운다
|
||||
@override
|
||||
Future<Stream<List<int>>> openRead(String rel) async {
|
||||
final dir = Directory(p.join(await tempDir(), 'jj_shz_${DateTime.now().microsecondsSinceEpoch}'));
|
||||
await dir.create(recursive: true);
|
||||
final f = File(p.join(dir.path, 'f'));
|
||||
try {
|
||||
await backend.copyTo(_abs(rel), f.path);
|
||||
} catch (_) {
|
||||
await dir.delete(recursive: true).catchError((_) => dir);
|
||||
rethrow;
|
||||
}
|
||||
final ctl = StreamController<List<int>>();
|
||||
ctl.addStream(f.openRead()).whenComplete(() async {
|
||||
await ctl.close();
|
||||
await dir.delete(recursive: true).catchError((_) => dir);
|
||||
});
|
||||
return ctl.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> mkdirs(String rel) async => throw ShizukuReadOnlyException(rel);
|
||||
@override
|
||||
Future<void> delete(String rel) async => throw ShizukuReadOnlyException(rel);
|
||||
@override
|
||||
Future<void> move(String from, String to, {bool overwrite = false}) async => throw ShizukuReadOnlyException(from);
|
||||
@override
|
||||
Future<void> write(String rel, Stream<List<int>> data, {int? length}) async => throw ShizukuReadOnlyException(rel);
|
||||
@override
|
||||
Future<(int?, int?)> quota() async => (null, null);
|
||||
@override
|
||||
void close() {}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
|||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'ftp.dart';
|
||||
import 'shizuku.dart';
|
||||
import 'remote_fs.dart';
|
||||
import 'sftp.dart';
|
||||
import 'smb.dart';
|
||||
|
|
@ -24,7 +25,12 @@ const ftpScheme = 'ftp://';
|
|||
|
||||
/// 원격 (WebDAV · SFTP · SMB) 경로인지. 이름은 예전 그대로 (원격이면 같은 길 - 복사 · 동기화 · 임시로 받아 열기)
|
||||
bool isDav(String path) =>
|
||||
path.startsWith(davScheme) || path.startsWith(sftpScheme) || path.startsWith(smbScheme) || path.startsWith(ftpScheme);
|
||||
path.startsWith(davScheme) ||
|
||||
path.startsWith(sftpScheme) ||
|
||||
path.startsWith(smbScheme) ||
|
||||
path.startsWith(ftpScheme) ||
|
||||
// X-plore 비교 5-2: 시스템 폴더 (Shizuku, 읽기 전용) 도 같은 길 (원격처럼 목록 · 임시로 받아 열기 · 꺼내기)
|
||||
path.startsWith(shizukuScheme);
|
||||
|
||||
/// WebDAV 만 (HTTP 로 바로 재생 · 서버 고치기 창 등 WebDAV 에만 있는 것)
|
||||
bool isWebDav(String path) => path.startsWith(davScheme);
|
||||
|
|
@ -33,7 +39,7 @@ bool isSmb(String path) => path.startsWith(smbScheme);
|
|||
bool isFtp(String path) => path.startsWith(ftpScheme);
|
||||
|
||||
/// 스트리밍 주소가 없는 원격 (SFTP · SMB) - 재생 · 다른 앱으로 열기는 받아서
|
||||
bool noStreamUrl(String path) => isSftp(path) || isSmb(path) || isFtp(path);
|
||||
bool noStreamUrl(String path) => isSftp(path) || isSmb(path) || isFtp(path) || isShizuku(path);
|
||||
|
||||
/// 원격 경로: 서버 종류 ([scheme]) · 서버 id · 서버 기준 경로 ("/" 로 시작, 끝 / 없음, 맨 위는 "/")
|
||||
class DavPath {
|
||||
|
|
@ -50,6 +56,8 @@ class DavPath {
|
|||
? smbScheme
|
||||
: path.startsWith(ftpScheme)
|
||||
? ftpScheme
|
||||
: path.startsWith(shizukuScheme)
|
||||
? shizukuScheme
|
||||
: davScheme;
|
||||
var rest = path.substring(scheme.length).replaceAll('\\', '/');
|
||||
final slash = rest.indexOf('/');
|
||||
|
|
@ -67,13 +75,16 @@ class DavPath {
|
|||
bool get isSftp => scheme == sftpScheme;
|
||||
bool get isSmb => scheme == smbScheme;
|
||||
bool get isFtp => scheme == ftpScheme;
|
||||
bool get isShizuku => scheme == shizukuScheme;
|
||||
String get full => isRoot ? '$scheme$server/' : '$scheme$server$rel';
|
||||
String get name => isRoot ? '' : rel.substring(rel.lastIndexOf('/') + 1);
|
||||
DavPath get parent => isRoot ? this : DavPath(server, rel.lastIndexOf('/') <= 0 ? '/' : rel.substring(0, rel.lastIndexOf('/')), scheme);
|
||||
DavPath child(String n) => DavPath(server, isRoot ? '/$n' : '$rel/$n', scheme);
|
||||
|
||||
/// 이 서버의 클라이언트 (WebDAV · SFTP · SMB)
|
||||
RemoteClient get client => isSftp
|
||||
RemoteClient get client => isShizuku
|
||||
? ShizukuRemote.instance
|
||||
: isSftp
|
||||
? SftpRegistry.client(server)
|
||||
: isSmb
|
||||
? SmbRegistry.client(server)
|
||||
|
|
@ -82,7 +93,9 @@ class DavPath {
|
|||
: DavRegistry.client(server);
|
||||
|
||||
/// 서버 이름 (화면 표시)
|
||||
String get serverLabel => isSftp
|
||||
String get serverLabel => isShizuku
|
||||
? tr('시스템 (읽기 전용)')
|
||||
: isSftp
|
||||
? SftpRegistry.server(server)?.label ?? 'SFTP'
|
||||
: isSmb
|
||||
? SmbRegistry.server(server)?.label ?? 'SMB'
|
||||
|
|
@ -91,7 +104,9 @@ class DavPath {
|
|||
: DavRegistry.server(server)?.label ?? 'WebDAV';
|
||||
|
||||
/// 설정에 이 서버가 있는지 (지운 서버의 경로 · 즐겨찾기 등)
|
||||
bool get serverKnown => isSftp
|
||||
bool get serverKnown => isShizuku
|
||||
? true
|
||||
: isSftp
|
||||
? SftpRegistry.server(server) != null
|
||||
: isSmb
|
||||
? SmbRegistry.server(server) != null
|
||||
|
|
@ -101,7 +116,9 @@ class DavPath {
|
|||
|
||||
/// 화면 표시 아이콘 글자: WebDAV ☁ · SFTP 🔑 · SMB 🗄
|
||||
/// l: FTP 📂 (평문 FTP 는 ⚠ - 암호화 안 됨)
|
||||
String get mark => isSftp
|
||||
String get mark => isShizuku
|
||||
? '🛡'
|
||||
: isSftp
|
||||
? '🔑'
|
||||
: isSmb
|
||||
// 보안 검토 6: 서명 · 암호화가 안 된 연결이면 ⚠
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import '../l10n/tr.dart';
|
|||
import 'explorer_bookmarks.dart';
|
||||
import 'explorer_search.dart';
|
||||
import 'remote_thumb_view.dart';
|
||||
import '../core/shizuku.dart';
|
||||
import 'vault_page.dart' show openVault;
|
||||
import 'app_manager_page.dart' show openAppManager;
|
||||
import 'dlna_page.dart' show openDlna;
|
||||
|
|
@ -126,6 +127,9 @@ class ExplorerPage extends StatefulWidget {
|
|||
@visibleForTesting
|
||||
static bool? debugNomedia;
|
||||
|
||||
/// 이번 실행에서 시스템 폴더 경고를 보였는지 (관리자 결정 10/21: 앱을 켠 뒤 처음 열 때 한 번)
|
||||
static bool shizukuWarnedThisRun = false;
|
||||
|
||||
/// 시험: 경로 입력 창이 넣은 경로를 고쳐 쓰고 확인하는 것 (갈 경로, 칸 아래 이유)
|
||||
@visibleForTesting
|
||||
static Future<(String?, String?)> checkPathForTest(String input) => _ExplorerPageState.checkEnteredPath(input);
|
||||
|
|
@ -390,6 +394,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
for (final s in c.settings.smbServers) ('$smbScheme${s.id}/', s.label),
|
||||
// l: FTP · FTPS 서버 (평문 FTP 는 이름 앞에 ⚠)
|
||||
for (final s in c.settings.ftpServers) ('$ftpScheme${s.id}/', s.tls ? s.label : '⚠ ${s.label}'),
|
||||
// X-plore 비교 5-2: 시스템 폴더 (Shizuku, 읽기 전용) - 환경 설정에서 켰을 때 (Android)
|
||||
if (c.settings.shizukuEnabled && shizukuSupported())
|
||||
('$shizukuScheme$shizukuServer/', tr('시스템 (읽기 전용)')),
|
||||
];
|
||||
late final List<_Pane> _panes = [_Pane(''), _Pane('')];
|
||||
int _active = 0;
|
||||
|
|
@ -589,6 +596,57 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
}
|
||||
|
||||
/// 권한이 없을 때 목록 위 안내 (이유 · 할 일 · 허용 화면 열기 · 다시 확인)
|
||||
/// X-plore 비교 5-2: 시스템 폴더를 보는 동안 늘 주황 띠
|
||||
Widget? _shizukuNotice(_Pane pane) {
|
||||
if (!isShizuku(pane.current)) return null;
|
||||
return _notice(
|
||||
icon: Icons.admin_panel_settings_outlined,
|
||||
title: tr('시스템 폴더 (Shizuku) - 읽기 전용'),
|
||||
body: tr('목록 · 보기 · 꺼내기 (복사) 만 됩니다. 다른 앱의 데이터가 있을 수 있으니 꺼낸 파일을 조심해서 다루세요'),
|
||||
actions: const [],
|
||||
color: Colors.orangeAccent,
|
||||
);
|
||||
}
|
||||
|
||||
/// X-plore 비교 5-2: 시스템 폴더 탭 - 이번 실행에서 처음이면 경고, Shizuku 가 켜져 있고 허락을 받았을 때만 Android 폴더로
|
||||
Future<void> _openShizuku(_Pane pane) async {
|
||||
if (!ExplorerPage.shizukuWarnedThisRun) {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
scrollable: true,
|
||||
icon: const Icon(Icons.admin_panel_settings_outlined, color: Colors.orangeAccent),
|
||||
title: Text(tr('시스템 폴더 보기 (읽기 전용)')),
|
||||
content: Text(tr('Shizuku 의 권한으로 Android/data · obb 처럼 막힌 폴더를 봅니다. 다른 앱의 데이터 (대화 · 로그인 정보 등) 가 '
|
||||
'있을 수 있습니다. 이 앱은 읽기만 하고 (쓰기 · 지우기 · 이름 바꾸기 없음), 꺼낸 파일은 고른 곳에만 둡니다.')),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('알고 봅니다'))),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok != true || !mounted) return;
|
||||
ExplorerPage.shizukuWarnedThisRun = true;
|
||||
}
|
||||
try {
|
||||
final st = await ShizukuRemote.backend.status();
|
||||
if (!st.running) {
|
||||
_snack(tr('Shizuku 가 켜져 있지 않습니다 - Shizuku 앱을 설치하고 (무선 디버깅 · adb 로) 시작한 뒤 다시 누르세요'));
|
||||
return;
|
||||
}
|
||||
if (!st.granted && !await ShizukuRemote.backend.request()) {
|
||||
_snack(tr('Shizuku 허락이 없어 볼 수 없습니다'));
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
_snack(trf('Shizuku 에 연결하지 못했습니다: {0}', [e]));
|
||||
return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() => _active = _panes.indexOf(pane));
|
||||
await _goTo(pane, shizukuHome(_local.first.$1));
|
||||
}
|
||||
|
||||
Widget? _accessNotice(_Pane pane) {
|
||||
if (!Platform.isAndroid || _allFiles != false || isDav(pane.root)) return null;
|
||||
return _notice(
|
||||
|
|
@ -804,7 +862,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
},
|
||||
child: Text(locked ? tr('마스터 비밀번호 넣기') : tr('다시 시도')),
|
||||
),
|
||||
if (dav && !locked)
|
||||
// 시스템 폴더 (Shizuku) 는 고칠 서버 설정이 없다
|
||||
if (dav && !locked && !isShizuku(where))
|
||||
TextButton(
|
||||
onPressed: () => sftp != null
|
||||
? _editSftp(pane, pane.root)
|
||||
|
|
@ -2485,6 +2544,11 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
/// 압축 안을 바꾸려 하면 막고 알린다 (읽기 전용). 막았으면 true
|
||||
/// [self] 면 압축 파일 자신도 (그 안으로 넣는 곳일 때)
|
||||
bool _blockArchive(List<String> paths, {bool self = false}) {
|
||||
// X-plore 비교 5-2: 시스템 폴더 (Shizuku) 도 읽기 전용 - 쓰기 · 지우기 · 이름 바꾸기 · 옮기기 · 새 폴더 · 넣기를 막는다 (꺼내기만)
|
||||
if (paths.any(isShizuku)) {
|
||||
_snack(tr('시스템 폴더 (Shizuku) 는 읽기 전용입니다 (꺼내기 (복사) 만 할 수 있습니다)'));
|
||||
return true;
|
||||
}
|
||||
if (!paths.any((x) => _insideArchive(x) || (self && _archives.containsKey(x)))) return false;
|
||||
_snack(tr('압축 안은 읽기 전용입니다 (꺼내기만 할 수 있습니다)'));
|
||||
return true;
|
||||
|
|
@ -5007,7 +5071,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
if (pane.filterOpen)
|
||||
_filterBar(pane, rows.where((r) => !r.isRoot && isSameOrInside(r.entry.path, pane.current) && !samePath(r.entry.path, pane.current) && quickFilterMatch(vBasename(r.entry.path), pane.filter)).length),
|
||||
// 38: 안내가 길어도 목록을 덮지 않게 (창 높이의 35% 까지, 넘치면 넘겨 봄)
|
||||
?_noticeArea([_accessNotice(pane), _errorNotice(pane, pane.current), _partialNotice(pane, pane.current)], box.maxHeight),
|
||||
?_noticeArea([_shizukuNotice(pane), _accessNotice(pane), _errorNotice(pane, pane.current), _partialNotice(pane, pane.current)], box.maxHeight),
|
||||
if (look.columns && !foldersOnly)
|
||||
ExplorerColumnsHeader(
|
||||
style: look,
|
||||
|
|
@ -5079,7 +5143,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
),
|
||||
const Divider(height: 1),
|
||||
if (pane.filterOpen) _filterBar(pane, shown.length),
|
||||
?_noticeArea([_accessNotice(pane), _errorNotice(pane, dir), _partialNotice(pane, dir)], box.maxHeight),
|
||||
?_noticeArea([_shizukuNotice(pane), _accessNotice(pane), _errorNotice(pane, dir), _partialNotice(pane, dir)], box.maxHeight),
|
||||
if (look.columns)
|
||||
ExplorerColumnsHeader(
|
||||
style: look,
|
||||
|
|
@ -5428,7 +5492,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
foregroundColor: samePath(pane.root, path) ? JjColors.accent : null,
|
||||
),
|
||||
icon: Icon(
|
||||
isSftp(path)
|
||||
isShizuku(path)
|
||||
? Icons.admin_panel_settings_outlined
|
||||
: isSftp(path)
|
||||
? Icons.vpn_key_outlined
|
||||
: isFtp(path)
|
||||
? Icons.folder_shared_outlined
|
||||
|
|
@ -5449,7 +5515,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
: Text(label),
|
||||
onPressed: () {
|
||||
setState(() => _active = _panes.indexOf(pane));
|
||||
if (_driveOk[path] == false) {
|
||||
if (isShizuku(path)) {
|
||||
unawaited(_openShizuku(pane));
|
||||
} else if (_driveOk[path] == false) {
|
||||
unawaited(_reconnect(pane, path));
|
||||
} else {
|
||||
_goTo(pane, path);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ List<(List<String>, String)> get _licenseAssets => [
|
|||
(['PDFium'], 'assets/licenses/PDFIUM_LICENSES.txt'),
|
||||
// 88: Android SMB - smbj (Apache-2.0) · asn-one · Bouncy Castle · SLF4J · MBassador
|
||||
(['smbj 0.15.0 · asn-one · Bouncy Castle · SLF4J · MBassador (Android SMB)'], 'assets/licenses/SMBJ_LICENSES.txt'),
|
||||
// X-plore 비교 5-2: 시스템 폴더 보기 (Shizuku API, MIT) - Android 판에만
|
||||
if (Platform.isAndroid) (['Shizuku API 13.1.5'], 'assets/licenses/SHIZUKU_LICENSE.txt'),
|
||||
// Tor 브라우저: tor 와 함께 묶인 Libevent · OpenSSL · zlib · zstd (공식 번들의 docs)
|
||||
(['Tor 0.4.9.13 (Libevent · OpenSSL · zlib · Zstandard)'], 'assets/licenses/TOR_LICENSES.txt'),
|
||||
// 관리자 지시 (10/20): Android 의 HWP · HWPX 보기 (libjj_rhwp.so - rhwp MIT 와 함께 묶인 Rust crate 들)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import 'smb_settings.dart';
|
|||
import 'webdav_settings.dart';
|
||||
import 'explorer_look.dart' show ExplorerStyle;
|
||||
import 'explorer_keys_page.dart';
|
||||
import '../core/shizuku.dart' show shizukuSupported;
|
||||
import 'theme.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'setting_tile.dart';
|
||||
|
|
@ -838,6 +839,51 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
onChanged: (v) => c.updateSettings((x) => x.explorerSortPerFolder = v),
|
||||
),
|
||||
),
|
||||
// X-plore 비교 5-2 (사용자 결정 10/21): 시스템 폴더 보기 (Shizuku, 읽기 전용) - Android, 켤 때마다 경고
|
||||
if (shizukuSupported())
|
||||
SettingTile(
|
||||
title: Text(tr('시스템 폴더 보기 (Shizuku · 읽기 전용)')),
|
||||
subtitle: Text(tr('Shizuku 앱이 있으면 Android/data · obb 처럼 막힌 폴더를 읽기만 합니다. 탐색기 위 탭 [시스템 (읽기 전용)]')),
|
||||
trailing: Switch(
|
||||
value: s.shizukuEnabled,
|
||||
onChanged: (v) async {
|
||||
if (v) {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
var sure = false;
|
||||
return StatefulBuilder(
|
||||
builder: (ctx, set) => AlertDialog(
|
||||
scrollable: true,
|
||||
icon: const Icon(Icons.admin_panel_settings_outlined, color: Colors.orangeAccent),
|
||||
title: Text(tr('시스템 폴더 보기를 켤까요?')),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(tr('Shizuku (adb 권한) 로 다른 앱의 폴더 (Android/data · obb) 를 볼 수 있게 됩니다. 그 안에는 다른 앱의 '
|
||||
'대화 · 로그인 정보 같은 개인 데이터가 있을 수 있습니다.')),
|
||||
const SizedBox(height: 6),
|
||||
Text(tr('이 앱은 읽기만 합니다 - 쓰기 · 지우기 · 이름 바꾸기 · 권한 바꾸기는 하지 않습니다. 꺼낸 (복사한) 파일은 고른 곳에만 둡니다.')),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: sure,
|
||||
onChanged: (x) => set(() => sure = x ?? false),
|
||||
title: Text(tr('위 내용을 알고 켭니다')),
|
||||
),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||
FilledButton(onPressed: sure ? () => Navigator.pop(ctx, true) : null, child: Text(tr('켜기'))),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (ok != true) return;
|
||||
}
|
||||
await c.updateSettings((x) => x.shizukuEnabled = v);
|
||||
},
|
||||
),
|
||||
),
|
||||
// X-plore 비교 5-1 ① (관리자 지시 10/21): 종류별 기본 정렬 (처음: 끔)
|
||||
SettingTile(
|
||||
title: Text(tr('종류별 기본 정렬')),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,236 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/core/shizuku.dart';
|
||||
import 'package:jj_mkvmaker/core/vfs.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/settings_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// X-plore 비교 5-2 시스템 폴더 보기 (Shizuku, 읽기 전용 · 처음 끔 · 경고 두 번 - 관리자 결정 10/21)
|
||||
class _Fake implements ShizukuBackend {
|
||||
bool running = true, granted = false, allow = true;
|
||||
final dirs = <String, String>{};
|
||||
final files = <String, String>{};
|
||||
final calls = <String>[];
|
||||
@override
|
||||
Future<({bool running, bool granted})> status() async => (running: running, granted: granted);
|
||||
@override
|
||||
Future<bool> request() async {
|
||||
calls.add('request');
|
||||
granted = allow;
|
||||
return allow;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> list(String path) async {
|
||||
calls.add('list:$path');
|
||||
final d = dirs[path];
|
||||
if (d == null) throw Exception('Permission denied');
|
||||
return d;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> stat(String path) async => files.containsKey(path) ? 'regular file|${files[path]!.length}|1700000000|${p.basename(path)}' : '';
|
||||
@override
|
||||
Future<int> copyTo(String path, String dest) async {
|
||||
calls.add('copy:$path');
|
||||
File(dest).writeAsStringSync(files[path]!);
|
||||
return files[path]!.length;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late Directory tmp;
|
||||
late _Fake fake;
|
||||
final wasBackend = ShizukuRemote.backend;
|
||||
final wasTemp = ShizukuRemote.tempDir;
|
||||
final wasSupported = shizukuSupported;
|
||||
setUp(() {
|
||||
tmp = Directory.systemTemp.createTempSync('jj_shz_');
|
||||
fake = _Fake();
|
||||
ShizukuRemote.backend = fake;
|
||||
ShizukuRemote.tempDir = () async => tmp.path;
|
||||
shizukuSupported = () => true;
|
||||
ExplorerPage.shizukuWarnedThisRun = false;
|
||||
});
|
||||
tearDown(() {
|
||||
ShizukuRemote.backend = wasBackend;
|
||||
ShizukuRemote.tempDir = wasTemp;
|
||||
shizukuSupported = wasSupported;
|
||||
ExplorerPage.debugVolumes = null;
|
||||
tmp.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
test('stat 한 줄 읽기: 폴더 · 파일 · 이름에 | · 틀린 줄', () {
|
||||
expect(parseShizukuStat('directory|3452|1700000000|com.app'), (true, 3452, DateTime.fromMillisecondsSinceEpoch(1700000000000), 'com.app'));
|
||||
expect(parseShizukuStat('regular file|12|1700000001|a|b.txt')?.$4, 'a|b.txt');
|
||||
expect(parseShizukuStat('regular empty file|0|1700000001|e'), isNotNull);
|
||||
expect(parseShizukuStat('stat: x: Permission denied'), isNull);
|
||||
expect(parseShizukuStat(''), isNull);
|
||||
});
|
||||
|
||||
test('경로 · 클라이언트: shz://sys/<절대 경로> → 읽기 전용 클라이언트, 목록 · 정보 · 내용 읽기 (임시는 지움) · 쓰기 · 지우기 · 옮기기 · 새 폴더는 막힘', () async {
|
||||
const dir = '/storage/emulated/0/Android/data';
|
||||
fake.dirs[dir] = 'directory|4096|1700000000|com.game\nregular file|5|1700000000|note.txt\nbad line\n';
|
||||
fake.files['$dir/note.txt'] = 'hello';
|
||||
final path = '$shizukuScheme$shizukuServer$dir';
|
||||
expect(isDav(path) && isShizuku(path) && noStreamUrl(path), isTrue);
|
||||
final d = DavPath.parse(path);
|
||||
expect((d.rel, d.mark, d.serverKnown), (dir, '🛡', true));
|
||||
final c = d.client;
|
||||
expect(c, isA<ShizukuRemote>());
|
||||
final items = await c.list(dir);
|
||||
expect(items.map((x) => (x.rel, x.isDir, x.size)), [('$dir/com.game', true, 0), ('$dir/note.txt', false, 5)]);
|
||||
expect((await c.stat('$dir/note.txt'))?.size, 5);
|
||||
expect(await c.stat('$dir/none'), isNull);
|
||||
final bytes = (await (await c.openRead('$dir/note.txt')).toList()).expand((x) => x).toList();
|
||||
expect(String.fromCharCodes(bytes), 'hello');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
expect(tmp.listSync().where((e) => p.basename(e.path).startsWith('jj_shz_')), isEmpty, reason: '받아 둔 임시는 지움');
|
||||
for (final f in <Future<void> Function()>[
|
||||
() => c.write('$dir/x', const Stream.empty()),
|
||||
() => c.delete('$dir/note.txt'),
|
||||
() => c.move('$dir/note.txt', '$dir/y'),
|
||||
() => c.mkdirs('$dir/new'),
|
||||
]) {
|
||||
await expectLater(f(), throwsA(isA<ShizukuReadOnlyException>()));
|
||||
}
|
||||
// 탐색기가 쓰는 vfs 길로도
|
||||
final listed = await vList(path);
|
||||
expect(listed.map((e) => p.posix.basename(e.path)), ['com.game', 'note.txt']);
|
||||
expect(shizukuHome('/storage/emulated/0'), 'shz://sys/storage/emulated/0/Android');
|
||||
});
|
||||
|
||||
AppController ctl({bool on = true}) {
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..shizukuEnabled = on
|
||||
..explorerPaths = [tmp.path, tmp.path]
|
||||
..explorerLayout = 'single';
|
||||
return c;
|
||||
}
|
||||
|
||||
Future<void> wait(WidgetTester t, bool Function() ok) async {
|
||||
for (var i = 0; i < 200 && !ok(); i++) {
|
||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
|
||||
await t.pump(const Duration(milliseconds: 20));
|
||||
}
|
||||
for (var i = 0; i < 8; i++) {
|
||||
await t.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
}
|
||||
|
||||
testWidgets('탐색기: 켰을 때만 [시스템 (읽기 전용)] 탭 · 처음 열 때 경고 · 허락 창 · Android 폴더 · 주황 띠 · 지우기는 막힘 · 다시 열 땐 경고 없음', (t) async {
|
||||
t.view.physicalSize = const Size(1400, 900);
|
||||
t.view.devicePixelRatio = 1;
|
||||
addTearDown(t.view.reset);
|
||||
// 탭이 여는 곳은 첫 저장 장치의 Android (기기 경로) - 그 저장 장치는 이 PC 에서 열리지 않아도 됨
|
||||
ExplorerPage.debugVolumes = () => [('/storage/emulated/0', 'T:')];
|
||||
const home = '/storage/emulated/0/Android';
|
||||
// 트리는 위 폴더부터 읽는다 (기기에서는 adb 셸이 / · /storage 를 읽을 수 있음)
|
||||
fake.dirs['/'] = 'directory|0|1700000000|storage\n';
|
||||
fake.dirs['/storage'] = 'directory|0|1700000000|emulated\n';
|
||||
fake.dirs['/storage/emulated'] = 'directory|0|1700000000|0\n';
|
||||
fake.dirs['/storage/emulated/0'] = 'directory|0|1700000000|Android\n';
|
||||
fake.dirs[home] = 'directory|4096|1700000000|data\ndirectory|4096|1700000000|obb\n';
|
||||
fake.dirs['$home/data'] = 'directory|4096|1700000000|com.game\n';
|
||||
// 꺼져 있으면 탭 없음
|
||||
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), c: ctl(on: false)))));
|
||||
await wait(t, () => false);
|
||||
expect(find.text('시스템 (읽기 전용)'), findsNothing);
|
||||
// 켬
|
||||
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), c: ctl()))));
|
||||
await wait(t, () => find.text('시스템 (읽기 전용)').evaluate().isNotEmpty);
|
||||
await t.tap(find.text('시스템 (읽기 전용)').first);
|
||||
await wait(t, () => find.text('알고 봅니다').evaluate().isNotEmpty);
|
||||
expect(find.textContaining('다른 앱의 데이터'), findsOneWidget);
|
||||
await t.tap(find.text('알고 봅니다'));
|
||||
await wait(t, () => find.text('obb').evaluate().isNotEmpty);
|
||||
expect(fake.calls, contains('request'), reason: '허락을 받지 않았으면 Shizuku 허락 창');
|
||||
expect(find.text('data'), findsWidgets);
|
||||
expect(find.text('시스템 폴더 (Shizuku) - 읽기 전용'), findsWidgets);
|
||||
// 지우기는 막힘 (꺼내기만)
|
||||
await t.tap(find.text('obb').last);
|
||||
await wait(t, () => false);
|
||||
await t.tap(find.text('삭제').first);
|
||||
await wait(t, () => find.textContaining('읽기 전용입니다').evaluate().isNotEmpty);
|
||||
expect(find.textContaining('시스템 폴더 (Shizuku) 는 읽기 전용입니다'), findsOneWidget);
|
||||
expect(find.text('obb'), findsWidgets, reason: '지워지지 않음');
|
||||
await t.tap(find.text('이름 변경').first);
|
||||
await wait(t, () => false);
|
||||
expect(find.byType(TextField), findsNothing, reason: '이름 바꾸기 창도 열리지 않음');
|
||||
// 다시 열기: 경고 없이 (이번 실행에서 이미 봄)
|
||||
await t.pumpWidget(const SizedBox());
|
||||
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), c: ctl()))));
|
||||
await wait(t, () => find.text('시스템 (읽기 전용)').evaluate().isNotEmpty);
|
||||
await t.tap(find.text('시스템 (읽기 전용)').first);
|
||||
await wait(t, () => find.text('obb').evaluate().isNotEmpty);
|
||||
expect(find.text('알고 봅니다'), findsNothing);
|
||||
await t.pumpWidget(const SizedBox());
|
||||
await t.pump(const Duration(seconds: 1));
|
||||
});
|
||||
|
||||
testWidgets('탐색기: Shizuku 가 꺼져 있거나 허락하지 않으면 알리고 들어가지 않음', (t) async {
|
||||
t.view.physicalSize = const Size(1400, 900);
|
||||
t.view.devicePixelRatio = 1;
|
||||
addTearDown(t.view.reset);
|
||||
ExplorerPage.debugVolumes = () => [(tmp.path, 'T:')];
|
||||
ExplorerPage.shizukuWarnedThisRun = true;
|
||||
fake.running = false;
|
||||
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), c: ctl()))));
|
||||
await wait(t, () => find.text('시스템 (읽기 전용)').evaluate().isNotEmpty);
|
||||
await t.tap(find.text('시스템 (읽기 전용)').first);
|
||||
await wait(t, () => find.textContaining('Shizuku 가 켜져 있지 않습니다').evaluate().isNotEmpty);
|
||||
expect(find.textContaining('Shizuku 가 켜져 있지 않습니다'), findsOneWidget);
|
||||
fake
|
||||
..running = true
|
||||
..allow = false;
|
||||
await t.tap(find.text('시스템 (읽기 전용)').first);
|
||||
await wait(t, () => find.text('Shizuku 허락이 없어 볼 수 없습니다').evaluate().isNotEmpty);
|
||||
expect(find.text('Shizuku 허락이 없어 볼 수 없습니다'), findsOneWidget);
|
||||
expect(fake.calls.where((x) => x.startsWith('list:')), isEmpty, reason: '들어가지 않음');
|
||||
await t.pumpWidget(const SizedBox());
|
||||
await t.pump(const Duration(seconds: 1));
|
||||
});
|
||||
|
||||
testWidgets('설정: 켤 때 경고 창 (체크해야 [켜기]) · 취소면 그대로 · 끄기는 바로', (t) async {
|
||||
t.view.physicalSize = const Size(1200, 4000);
|
||||
t.view.devicePixelRatio = 1;
|
||||
addTearDown(t.view.reset);
|
||||
final c = ctl(on: false);
|
||||
await t.pumpWidget(MaterialApp(home: SettingsPage(c: c)));
|
||||
await t.pump();
|
||||
final tile = find.text('시스템 폴더 보기 (Shizuku · 읽기 전용)');
|
||||
await t.scrollUntilVisible(tile, 300, scrollable: find.byType(Scrollable).first);
|
||||
final sw = find.descendant(of: find.ancestor(of: tile, matching: find.byType(ListTile)).first, matching: find.byType(Switch));
|
||||
Future<void> settle() async {
|
||||
for (var i = 0; i < 10; i++) {
|
||||
await t.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
}
|
||||
|
||||
await t.tap(sw.first);
|
||||
await settle();
|
||||
expect(find.text('시스템 폴더 보기를 켤까요?'), findsOneWidget);
|
||||
expect(t.widget<FilledButton>(find.widgetWithText(FilledButton, '켜기')).onPressed, isNull, reason: '체크해야 켜짐');
|
||||
await t.tap(find.text('취소'));
|
||||
await settle();
|
||||
expect(c.settings.shizukuEnabled, isFalse);
|
||||
await t.tap(sw.first);
|
||||
await settle();
|
||||
await t.tap(find.text('위 내용을 알고 켭니다'));
|
||||
await t.pump();
|
||||
await t.tap(find.widgetWithText(FilledButton, '켜기'));
|
||||
await settle();
|
||||
expect(c.settings.shizukuEnabled, isTrue);
|
||||
await t.tap(sw.first);
|
||||
await settle();
|
||||
expect(c.settings.shizukuEnabled, isFalse, reason: '끄기는 묻지 않음');
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue