WIP X-plore 비교 5-2 앱 관리자 (사용자 결정 10/21, 관리자 판단, Android): 가운데 버튼 [앱 관리자] (Android 만, 버튼 끝에 - 좁으면 [더 보기]) - 모든 앱 목록 권한 (QUERY_ALL_PACKAGES) 없이 매니페스트 queries 의 LAUNCHER 로 실행할 수 있는 앱만, 화면 위에 "보이는 앱만 나옵니다 (모든 앱 목록 권한은 쓰지 않음)". 이름 · 판 · 크기 · 설치일, 이름 · 크기 · 설치일 정렬 · 찾기, [APK 백업] (Download/JJ_MKVMaker/apk, 이름_판.apk · 겹치면 (2), 나눠진 앱은 알림) · [앱 정보] (시스템 화면) · [지우기] (시스템 지우기 창, 시스템 앱은 없음). 아이콘은 그때그때 PNG 로 · 시험 (지도 읽기 · 백업 이름 · 화면 · 버튼은 Android 만) · APK 빌드로 Kotlin 확인 (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>xplore_p3b
parent
59f417552a
commit
fffb9d7dbc
|
|
@ -159,6 +159,11 @@
|
||||||
|
|
||||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
<queries>
|
<queries>
|
||||||
|
<!-- X-plore 비교 5-2 앱 관리자 (관리자 판단 10/21): 모든 앱 목록 권한 (QUERY_ALL_PACKAGES) 대신 실행할 수 있는 앱 (런처에 보이는 앱) 만 -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,24 @@ class MainActivity : FlutterActivity() {
|
||||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.O || packageManager.canRequestPackageInstalls()
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.O || packageManager.canRequestPackageInstalls()
|
||||||
)
|
)
|
||||||
"openFolder" -> result.success(openFolder(call.argument<String>("path") ?: ""))
|
"openFolder" -> result.success(openFolder(call.argument<String>("path") ?: ""))
|
||||||
|
// X-plore 비교 5-2 앱 관리자 (사용자 결정 10/21, 관리자 판단: 모든 앱 목록 권한 없이 런처에 보이는 앱만)
|
||||||
|
"launcherApps" -> inBackground(result) { launcherApps() }
|
||||||
|
"appIcon" -> inBackground(result) { appIcon(call.argument<String>("package") ?: "", call.argument<Int>("size") ?: 96) }
|
||||||
|
"appDetails" -> result.success(try {
|
||||||
|
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + (call.argument<String>("package") ?: "")))
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
})
|
||||||
|
// 지우기는 시스템의 지우기 창 (사용자가 거기서 확인)
|
||||||
|
"uninstallApp" -> result.success(try {
|
||||||
|
startActivity(Intent(Intent.ACTION_DELETE, Uri.parse("package:" + (call.argument<String>("package") ?: "")))
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
})
|
||||||
// X-plore 맞추기 (관리자 지시 10/20): .nomedia 를 넣고 지운 뒤 갤러리 · 음악 앱이 다시 읽게 (실패해도 조용히)
|
// X-plore 맞추기 (관리자 지시 10/20): .nomedia 를 넣고 지운 뒤 갤러리 · 음악 앱이 다시 읽게 (실패해도 조용히)
|
||||||
"mediaScan" -> result.success(try {
|
"mediaScan" -> result.success(try {
|
||||||
val paths = call.argument<List<String>>("paths") ?: emptyList()
|
val paths = call.argument<List<String>>("paths") ?: emptyList()
|
||||||
|
|
@ -554,6 +572,49 @@ class MainActivity : FlutterActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 오래 걸리는 일 (처음 켤 때 실행 파일 풀기 · yt-dlp 업데이트) 은 메인 스레드 밖에서
|
/// 오래 걸리는 일 (처음 켤 때 실행 파일 풀기 · yt-dlp 업데이트) 은 메인 스레드 밖에서
|
||||||
|
/// X-plore 비교 5-2: 런처에 보이는 앱 (이름 · 패키지 · 판 · 크기 · 설치일 · APK 경로 · 나눈 APK 수 · 시스템 앱인지)
|
||||||
|
private fun launcherApps(): List<Map<String, Any?>> {
|
||||||
|
val pm = packageManager
|
||||||
|
val seen = HashSet<String>()
|
||||||
|
val out = ArrayList<Map<String, Any?>>()
|
||||||
|
for (ri in pm.queryIntentActivities(Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER), 0)) {
|
||||||
|
val pkg = ri.activityInfo.packageName
|
||||||
|
if (!seen.add(pkg)) continue
|
||||||
|
try {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val pi = pm.getPackageInfo(pkg, 0)
|
||||||
|
val ai = pi.applicationInfo ?: continue
|
||||||
|
val dirs = listOfNotNull(ai.sourceDir) + (ai.splitSourceDirs?.toList() ?: emptyList())
|
||||||
|
out.add(mapOf(
|
||||||
|
"package" to pkg,
|
||||||
|
"label" to ri.loadLabel(pm).toString(),
|
||||||
|
"version" to (pi.versionName ?: ""),
|
||||||
|
"installed" to pi.firstInstallTime,
|
||||||
|
"updated" to pi.lastUpdateTime,
|
||||||
|
"size" to dirs.sumOf { java.io.File(it).length() },
|
||||||
|
"apk" to ai.sourceDir,
|
||||||
|
"splits" to (ai.splitSourceDirs?.size ?: 0),
|
||||||
|
"system" to ((ai.flags and android.content.pm.ApplicationInfo.FLAG_SYSTEM) != 0),
|
||||||
|
))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 앱 아이콘 (PNG, [size] 픽셀)
|
||||||
|
private fun appIcon(pkg: String, size: Int): ByteArray? = try {
|
||||||
|
val d = packageManager.getApplicationIcon(pkg)
|
||||||
|
val bmp = android.graphics.Bitmap.createBitmap(size, size, android.graphics.Bitmap.Config.ARGB_8888)
|
||||||
|
d.setBounds(0, 0, size, size)
|
||||||
|
d.draw(android.graphics.Canvas(bmp))
|
||||||
|
val bos = java.io.ByteArrayOutputStream()
|
||||||
|
bmp.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, bos)
|
||||||
|
bos.toByteArray()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
private fun inBackground(result: MethodChannel.Result, work: () -> Any?) {
|
private fun inBackground(result: MethodChannel.Result, work: () -> Any?) {
|
||||||
Thread {
|
Thread {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,10 @@
|
||||||
"API 키 (선택)": "API key (optional)",
|
"API 키 (선택)": "API key (optional)",
|
||||||
"API 키 (필수, 무료)": "API key (required, free)",
|
"API 키 (필수, 무료)": "API key (required, free)",
|
||||||
"API 키 또는 아이디:비밀번호 (선택)": "API key or user:password (optional)",
|
"API 키 또는 아이디:비밀번호 (선택)": "API key or user:password (optional)",
|
||||||
|
"APK 를 저장하지 못했습니다: {0}": "Could not save the APK: {0}",
|
||||||
|
"APK 를 저장했습니다: {0}": "APK saved: {0}",
|
||||||
|
"APK 를 저장했습니다: {0} - 나눠진 앱 (조각 {1}개) 이라 이 파일 하나로는 설치되지 않을 수 있습니다": "APK saved: {0} - this app is split ({1} parts), so this file alone may not install",
|
||||||
|
"APK 백업": "Back up APK",
|
||||||
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "AV1 video cannot be re-encoded on this device. Change the codec to \"Keep original\" to make the MKV. (The PC version can.)",
|
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "AV1 video cannot be re-encoded on this device. Change the codec to \"Keep original\" to make the MKV. (The PC version can.)",
|
||||||
"Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "Since Android 11, no file app can open other apps' data folders (Android/data · Android/obb). Use export or share inside that app to get the files.",
|
"Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "Since Android 11, no file app can open other apps' data folders (Android/data · Android/obb). Use export or share inside that app to get the files.",
|
||||||
"Android System WebView 에 필요한 기능이 없습니다 ({0}). Play 스토어에서 Android System WebView · Chrome 을 업데이트한 뒤 다시 여세요. Tor 를 거치지 않고 열릴 수 있으므로 이 화면은 페이지를 읽지 않습니다.": "Android System WebView lacks a required feature ({0}). Update Android System WebView / Chrome in the Play Store and open this screen again. Because pages could open without going through Tor, this screen loads nothing.",
|
"Android System WebView 에 필요한 기능이 없습니다 ({0}). Play 스토어에서 Android System WebView · Chrome 을 업데이트한 뒤 다시 여세요. Tor 를 거치지 않고 열릴 수 있으므로 이 화면은 페이지를 읽지 않습니다.": "Android System WebView lacks a required feature ({0}). Update Android System WebView / Chrome in the Play Store and open this screen again. Because pages could open without going through Tor, this screen loads nothing.",
|
||||||
|
|
@ -1458,6 +1462,7 @@
|
||||||
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "The folder you were viewing is gone, so moved to its parent folder",
|
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "The folder you were viewing is gone, so moved to its parent folder",
|
||||||
"보안": "Security",
|
"보안": "Security",
|
||||||
"보이기": "Show",
|
"보이기": "Show",
|
||||||
|
"보이는 앱만 나옵니다 (모든 앱 목록 권한은 쓰지 않음)": "Only visible apps are listed (the all-apps permission is not used)",
|
||||||
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "Choose the buttons to show and drag to reorder.",
|
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "Choose the buttons to show and drag to reorder.",
|
||||||
"보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "Adjustments require re-encoding, so the codec was changed from \"Keep original\" to H.264.",
|
"보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "Adjustments require re-encoding, so the codec was changed from \"Keep original\" to H.264.",
|
||||||
"보조 DNS (비워 둘 수 있음)": "Secondary DNS (may be empty)",
|
"보조 DNS (비워 둘 수 있음)": "Secondary DNS (may be empty)",
|
||||||
|
|
@ -1674,6 +1679,7 @@
|
||||||
"설정을 되살리지 못했습니다: {0}": "Could not restore settings: {0}",
|
"설정을 되살리지 못했습니다: {0}": "Could not restore settings: {0}",
|
||||||
"설정을 백업으로 되살렸습니다": "Settings restored from backup",
|
"설정을 백업으로 되살렸습니다": "Settings restored from backup",
|
||||||
"설정을 보관하지 못했습니다: {0}": "Could not keep settings: {0}",
|
"설정을 보관하지 못했습니다: {0}": "Could not keep settings: {0}",
|
||||||
|
"설치 {0}": "Installed {0}",
|
||||||
"설치 계속": "Continue install",
|
"설치 계속": "Continue install",
|
||||||
"설치 실패": "Install failed",
|
"설치 실패": "Install failed",
|
||||||
"설치 준비 완료": "Ready to install",
|
"설치 준비 완료": "Ready to install",
|
||||||
|
|
@ -1684,6 +1690,7 @@
|
||||||
"설치가 끝나면 프로그램이 다시 시작됩니다.": "The program restarts when the installation finishes.",
|
"설치가 끝나면 프로그램이 다시 시작됩니다.": "The program restarts when the installation finishes.",
|
||||||
"설치됨": "Installed",
|
"설치됨": "Installed",
|
||||||
"설치됨: Tor Expert Bundle {0} (Tor 공식 배포). 끄면 지웁니다.": "Installed: Tor Expert Bundle {0} (official Tor release). Turning it off deletes it.",
|
"설치됨: Tor Expert Bundle {0} (Tor 공식 배포). 끄면 지웁니다.": "Installed: Tor Expert Bundle {0} (official Tor release). Turning it off deletes it.",
|
||||||
|
"설치일": "Install date",
|
||||||
"설치하는 중": "Installing",
|
"설치하는 중": "Installing",
|
||||||
"설치하면 필요한 파일을 내려받습니다 (준비 중)": "Installing downloads the files it needs (coming soon)",
|
"설치하면 필요한 파일을 내려받습니다 (준비 중)": "Installing downloads the files it needs (coming soon)",
|
||||||
"설치하지 못했습니다.\n{0}": "Could not install.\n{0}",
|
"설치하지 못했습니다.\n{0}": "Could not install.\n{0}",
|
||||||
|
|
@ -1841,7 +1848,9 @@
|
||||||
"앞의 버튼 보기": "Previous buttons",
|
"앞의 버튼 보기": "Previous buttons",
|
||||||
"앞의 복사 · 이동이 끝난 뒤에 하세요.": "Wait until the current copy · move finishes.",
|
"앞의 복사 · 이동이 끝난 뒤에 하세요.": "Wait until the current copy · move finishes.",
|
||||||
"앨범": "Album",
|
"앨범": "Album",
|
||||||
|
"앱 관리자": "App manager",
|
||||||
"앱 목록 · 홈 화면과 앱 안 왼쪽 위 버튼의 아이콘입니다. 바꾸면 홈 화면에 둔 아이콘이 없어질 수 있어 앱 목록에서 다시 끌어다 놓아야 할 수 있습니다": "Icon in the app list, home screen and the top-left button in the app. After changing it, the icon on your home screen may disappear and you may need to drag it from the app list again",
|
"앱 목록 · 홈 화면과 앱 안 왼쪽 위 버튼의 아이콘입니다. 바꾸면 홈 화면에 둔 아이콘이 없어질 수 있어 앱 목록에서 다시 끌어다 놓아야 할 수 있습니다": "Icon in the app list, home screen and the top-left button in the app. After changing it, the icon on your home screen may disappear and you may need to drag it from the app list again",
|
||||||
|
"앱 목록을 읽지 못했습니다: {0}": "Could not read the app list: {0}",
|
||||||
"앱 목록이 열렸다면, 목록에서 JJ_MKVMaker 를 찾아 눌러 \"모든 파일에 대한 접근\" 을 켜고 돌아오세요.": "If a list of apps opened, find and tap JJ_MKVMaker in the list, turn on \"All files access\", and come back.",
|
"앱 목록이 열렸다면, 목록에서 JJ_MKVMaker 를 찾아 눌러 \"모든 파일에 대한 접근\" 을 켜고 돌아오세요.": "If a list of apps opened, find and tap JJ_MKVMaker in the list, turn on \"All files access\", and come back.",
|
||||||
"앱 설정 전체 (처음 상태로)": "All app settings (back to initial)",
|
"앱 설정 전체 (처음 상태로)": "All app settings (back to initial)",
|
||||||
"앱 설정을 처음 상태로 돌렸습니다 (모두 적용하려면 앱을 다시 켜 주세요)": "Reset app settings to the initial state (restart the app to apply everything)",
|
"앱 설정을 처음 상태로 돌렸습니다 (모두 적용하려면 앱을 다시 켜 주세요)": "Reset app settings to the initial state (restart the app to apply everything)",
|
||||||
|
|
@ -1854,6 +1863,7 @@
|
||||||
"앱 안 웹 브라우저 · 즐겨찾기 · 동영상 다운로드": "In-app web browser · bookmarks · video downloads",
|
"앱 안 웹 브라우저 · 즐겨찾기 · 동영상 다운로드": "In-app web browser · bookmarks · video downloads",
|
||||||
"앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 (넘기는 동안 화면 위쪽에 \"광고 건너뛰는 중…\")": "In-app web browser: presses [Skip] for you, and mutes and fast-forwards ads that can't be skipped (\"Skipping ad…\" shows at the top while skipping)",
|
"앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 (넘기는 동안 화면 위쪽에 \"광고 건너뛰는 중…\")": "In-app web browser: presses [Skip] for you, and mutes and fast-forwards ads that can't be skipped (\"Skipping ad…\" shows at the top while skipping)",
|
||||||
"앱 안에서 열지 못해 다른 앱으로 엽니다: {0}": "Couldn't open it in the app, opening with another app: {0}",
|
"앱 안에서 열지 못해 다른 앱으로 엽니다: {0}": "Couldn't open it in the app, opening with another app: {0}",
|
||||||
|
"앱 정보 (시스템 화면)": "App info (system screen)",
|
||||||
"앱 지우기": "Uninstall app",
|
"앱 지우기": "Uninstall app",
|
||||||
"앱 휴지통에 {0}일 두었다가 지웁니다 (탐색기 [휴지통] 에서 되살리기).": "Kept in the app trash for {0} days, then deleted (restore from [Trash] in the explorer).",
|
"앱 휴지통에 {0}일 두었다가 지웁니다 (탐색기 [휴지통] 에서 되살리기).": "Kept in the app trash for {0} days, then deleted (restore from [Trash] in the explorer).",
|
||||||
"앱에 들어 있는 것 (기본)": "Built into the app (default)",
|
"앱에 들어 있는 것 (기본)": "Built into the app (default)",
|
||||||
|
|
@ -2213,6 +2223,7 @@
|
||||||
"이름 틀": "Name pattern",
|
"이름 틀": "Name pattern",
|
||||||
"이름 틀 (확장자는 그대로)": "Name pattern (extension kept)",
|
"이름 틀 (확장자는 그대로)": "Name pattern (extension kept)",
|
||||||
"이름에 줄바꿈이 들어 있어 FTP 로 보낼 수 없습니다": "The name contains a line break and can't be sent over FTP",
|
"이름에 줄바꿈이 들어 있어 FTP 로 보낼 수 없습니다": "The name contains a line break and can't be sent over FTP",
|
||||||
|
"이름으로 찾기": "Search by name",
|
||||||
"이름을 바꿀 수 없습니다: {0}": "Cannot rename: {0}",
|
"이름을 바꿀 수 없습니다: {0}": "Cannot rename: {0}",
|
||||||
"이름을 바꿀 항목 하나를 고르세요.": "Choose one item to rename.",
|
"이름을 바꿀 항목 하나를 고르세요.": "Choose one item to rename.",
|
||||||
"이름을 바꿉니다": "renames it",
|
"이름을 바꿉니다": "renames it",
|
||||||
|
|
@ -2546,6 +2557,7 @@
|
||||||
"지우고 맞추기": "Delete and sync",
|
"지우고 맞추기": "Delete and sync",
|
||||||
"지우고 초기화": "Delete and reset",
|
"지우고 초기화": "Delete and reset",
|
||||||
"지우기": "Delete",
|
"지우기": "Delete",
|
||||||
|
"지우기 (시스템 창에서 확인)": "Uninstall (confirm in the system dialog)",
|
||||||
"지우기 (확인 · 휴지통)": "Delete (confirm · trash)",
|
"지우기 (확인 · 휴지통)": "Delete (confirm · trash)",
|
||||||
"지우기 포함": "with delete",
|
"지우기 포함": "with delete",
|
||||||
"지우기가 들어 있는 작업입니다": "This task deletes files",
|
"지우기가 들어 있는 작업입니다": "This task deletes files",
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,10 @@
|
||||||
"API 키 (선택)": "API キー (任意)",
|
"API 키 (선택)": "API キー (任意)",
|
||||||
"API 키 (필수, 무료)": "API キー(必須・無料)",
|
"API 키 (필수, 무료)": "API キー(必須・無料)",
|
||||||
"API 키 또는 아이디:비밀번호 (선택)": "API キーまたは ID:パスワード (任意)",
|
"API 키 또는 아이디:비밀번호 (선택)": "API キーまたは ID:パスワード (任意)",
|
||||||
|
"APK 를 저장하지 못했습니다: {0}": "APK を保存できませんでした: {0}",
|
||||||
|
"APK 를 저장했습니다: {0}": "APK を保存しました: {0}",
|
||||||
|
"APK 를 저장했습니다: {0} - 나눠진 앱 (조각 {1}개) 이라 이 파일 하나로는 설치되지 않을 수 있습니다": "APK を保存しました: {0} - 分割されたアプリ (分割 {1} 個) のため、このファイルだけではインストールできない場合があります",
|
||||||
|
"APK 백업": "APK をバックアップ",
|
||||||
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "AV1 動画はこの端末では再エンコードできません。コーデックを「元のまま」に変えて MKV を作成してください。(PC 版はできます)",
|
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "AV1 動画はこの端末では再エンコードできません。コーデックを「元のまま」に変えて MKV を作成してください。(PC 版はできます)",
|
||||||
"Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "Android 11 以降、他のアプリのデータフォルダー (Android/data・Android/obb) はどのファイルアプリでも開けません。そのアプリ内のエクスポート・共有でファイルを取り出してください。",
|
"Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "Android 11 以降、他のアプリのデータフォルダー (Android/data・Android/obb) はどのファイルアプリでも開けません。そのアプリ内のエクスポート・共有でファイルを取り出してください。",
|
||||||
"Android System WebView 에 필요한 기능이 없습니다 ({0}). Play 스토어에서 Android System WebView · Chrome 을 업데이트한 뒤 다시 여세요. Tor 를 거치지 않고 열릴 수 있으므로 이 화면은 페이지를 읽지 않습니다.": "Android System WebView に必要な機能がありません ({0})。Play ストアで Android System WebView・Chrome を更新してから開き直してください。Tor を通らずに開く恐れがあるため、この画面はページを読み込みません。",
|
"Android System WebView 에 필요한 기능이 없습니다 ({0}). Play 스토어에서 Android System WebView · Chrome 을 업데이트한 뒤 다시 여세요. Tor 를 거치지 않고 열릴 수 있으므로 이 화면은 페이지를 읽지 않습니다.": "Android System WebView に必要な機能がありません ({0})。Play ストアで Android System WebView・Chrome を更新してから開き直してください。Tor を通らずに開く恐れがあるため、この画面はページを読み込みません。",
|
||||||
|
|
@ -1458,6 +1462,7 @@
|
||||||
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "見ていたフォルダーがなくなったため上のフォルダーに移動しました",
|
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "見ていたフォルダーがなくなったため上のフォルダーに移動しました",
|
||||||
"보안": "セキュリティ",
|
"보안": "セキュリティ",
|
||||||
"보이기": "表示",
|
"보이기": "表示",
|
||||||
|
"보이는 앱만 나옵니다 (모든 앱 목록 권한은 쓰지 않음)": "表示されるアプリだけが出ます (すべてのアプリ一覧の権限は使いません)",
|
||||||
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "表示するボタンを選び、ドラッグで順序を変えます。",
|
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "表示するボタンを選び、ドラッグで順序を変えます。",
|
||||||
"보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "補正には再エンコードが必要なため、コーデックを「元のまま」から H.264 に変更しました。",
|
"보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "補正には再エンコードが必要なため、コーデックを「元のまま」から H.264 に変更しました。",
|
||||||
"보조 DNS (비워 둘 수 있음)": "代替 DNS (空欄可)",
|
"보조 DNS (비워 둘 수 있음)": "代替 DNS (空欄可)",
|
||||||
|
|
@ -1674,6 +1679,7 @@
|
||||||
"설정을 되살리지 못했습니다: {0}": "設定を復元できませんでした: {0}",
|
"설정을 되살리지 못했습니다: {0}": "設定を復元できませんでした: {0}",
|
||||||
"설정을 백업으로 되살렸습니다": "設定をバックアップから復元しました",
|
"설정을 백업으로 되살렸습니다": "設定をバックアップから復元しました",
|
||||||
"설정을 보관하지 못했습니다: {0}": "設定を保管できませんでした: {0}",
|
"설정을 보관하지 못했습니다: {0}": "設定を保管できませんでした: {0}",
|
||||||
|
"설치 {0}": "インストール {0}",
|
||||||
"설치 계속": "インストールを続ける",
|
"설치 계속": "インストールを続ける",
|
||||||
"설치 실패": "インストール失敗",
|
"설치 실패": "インストール失敗",
|
||||||
"설치 준비 완료": "インストール準備完了",
|
"설치 준비 완료": "インストール準備完了",
|
||||||
|
|
@ -1684,6 +1690,7 @@
|
||||||
"설치가 끝나면 프로그램이 다시 시작됩니다.": "インストールが終わるとプログラムが再起動します。",
|
"설치가 끝나면 프로그램이 다시 시작됩니다.": "インストールが終わるとプログラムが再起動します。",
|
||||||
"설치됨": "インストール済み",
|
"설치됨": "インストール済み",
|
||||||
"설치됨: Tor Expert Bundle {0} (Tor 공식 배포). 끄면 지웁니다.": "インストール済み: Tor Expert Bundle {0} (Tor 公式配布)。オフにすると削除します。",
|
"설치됨: Tor Expert Bundle {0} (Tor 공식 배포). 끄면 지웁니다.": "インストール済み: Tor Expert Bundle {0} (Tor 公式配布)。オフにすると削除します。",
|
||||||
|
"설치일": "インストール日",
|
||||||
"설치하는 중": "インストール中",
|
"설치하는 중": "インストール中",
|
||||||
"설치하면 필요한 파일을 내려받습니다 (준비 중)": "インストールすると必要なファイルをダウンロードします (準備中)",
|
"설치하면 필요한 파일을 내려받습니다 (준비 중)": "インストールすると必要なファイルをダウンロードします (準備中)",
|
||||||
"설치하지 못했습니다.\n{0}": "インストールできませんでした。\n{0}",
|
"설치하지 못했습니다.\n{0}": "インストールできませんでした。\n{0}",
|
||||||
|
|
@ -1841,7 +1848,9 @@
|
||||||
"앞의 버튼 보기": "前のボタンを表示",
|
"앞의 버튼 보기": "前のボタンを表示",
|
||||||
"앞의 복사 · 이동이 끝난 뒤에 하세요.": "前のコピー · 移動が終わってから行ってください。",
|
"앞의 복사 · 이동이 끝난 뒤에 하세요.": "前のコピー · 移動が終わってから行ってください。",
|
||||||
"앨범": "アルバム",
|
"앨범": "アルバム",
|
||||||
|
"앱 관리자": "アプリ管理",
|
||||||
"앱 목록 · 홈 화면과 앱 안 왼쪽 위 버튼의 아이콘입니다. 바꾸면 홈 화면에 둔 아이콘이 없어질 수 있어 앱 목록에서 다시 끌어다 놓아야 할 수 있습니다": "アプリ一覧・ホーム画面とアプリ内左上のボタンのアイコンです。変更するとホーム画面のアイコンが消えることがあり、アプリ一覧から再度ドラッグする必要がある場合があります",
|
"앱 목록 · 홈 화면과 앱 안 왼쪽 위 버튼의 아이콘입니다. 바꾸면 홈 화면에 둔 아이콘이 없어질 수 있어 앱 목록에서 다시 끌어다 놓아야 할 수 있습니다": "アプリ一覧・ホーム画面とアプリ内左上のボタンのアイコンです。変更するとホーム画面のアイコンが消えることがあり、アプリ一覧から再度ドラッグする必要がある場合があります",
|
||||||
|
"앱 목록을 읽지 못했습니다: {0}": "アプリ一覧を読み込めませんでした: {0}",
|
||||||
"앱 목록이 열렸다면, 목록에서 JJ_MKVMaker 를 찾아 눌러 \"모든 파일에 대한 접근\" 을 켜고 돌아오세요.": "アプリ一覧が開いた場合は、一覧から JJ_MKVMaker を探してタップし、「すべてのファイルへのアクセス」をオンにして戻ってください。",
|
"앱 목록이 열렸다면, 목록에서 JJ_MKVMaker 를 찾아 눌러 \"모든 파일에 대한 접근\" 을 켜고 돌아오세요.": "アプリ一覧が開いた場合は、一覧から JJ_MKVMaker を探してタップし、「すべてのファイルへのアクセス」をオンにして戻ってください。",
|
||||||
"앱 설정 전체 (처음 상태로)": "アプリの設定すべて (初期状態に)",
|
"앱 설정 전체 (처음 상태로)": "アプリの設定すべて (初期状態に)",
|
||||||
"앱 설정을 처음 상태로 돌렸습니다 (모두 적용하려면 앱을 다시 켜 주세요)": "アプリの設定を初期状態に戻しました (すべて反映するにはアプリを再起動してください)",
|
"앱 설정을 처음 상태로 돌렸습니다 (모두 적용하려면 앱을 다시 켜 주세요)": "アプリの設定を初期状態に戻しました (すべて反映するにはアプリを再起動してください)",
|
||||||
|
|
@ -1854,6 +1863,7 @@
|
||||||
"앱 안 웹 브라우저 · 즐겨찾기 · 동영상 다운로드": "アプリ内 Web ブラウザー・ブックマーク・動画ダウンロード",
|
"앱 안 웹 브라우저 · 즐겨찾기 · 동영상 다운로드": "アプリ内 Web ブラウザー・ブックマーク・動画ダウンロード",
|
||||||
"앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 (넘기는 동안 화면 위쪽에 \"광고 건너뛰는 중…\")": "アプリ内ウェブブラウザー:[スキップ]を代わりに押し、スキップできない広告は音を消して早送りします(スキップ中は画面上部に「広告をスキップ中…」)",
|
"앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 (넘기는 동안 화면 위쪽에 \"광고 건너뛰는 중…\")": "アプリ内ウェブブラウザー:[スキップ]を代わりに押し、スキップできない広告は音を消して早送りします(スキップ中は画面上部に「広告をスキップ中…」)",
|
||||||
"앱 안에서 열지 못해 다른 앱으로 엽니다: {0}": "アプリ内で開けないため、他のアプリで開きます: {0}",
|
"앱 안에서 열지 못해 다른 앱으로 엽니다: {0}": "アプリ内で開けないため、他のアプリで開きます: {0}",
|
||||||
|
"앱 정보 (시스템 화면)": "アプリ情報 (システム画面)",
|
||||||
"앱 지우기": "アプリを削除",
|
"앱 지우기": "アプリを削除",
|
||||||
"앱 휴지통에 {0}일 두었다가 지웁니다 (탐색기 [휴지통] 에서 되살리기).": "アプリのゴミ箱に {0} 日間置いてから削除します (エクスプローラーの [ゴミ箱] から復元できます)。",
|
"앱 휴지통에 {0}일 두었다가 지웁니다 (탐색기 [휴지통] 에서 되살리기).": "アプリのゴミ箱に {0} 日間置いてから削除します (エクスプローラーの [ゴミ箱] から復元できます)。",
|
||||||
"앱에 들어 있는 것 (기본)": "アプリに内蔵 (既定)",
|
"앱에 들어 있는 것 (기본)": "アプリに内蔵 (既定)",
|
||||||
|
|
@ -2213,6 +2223,7 @@
|
||||||
"이름 틀": "名前の形式",
|
"이름 틀": "名前の形式",
|
||||||
"이름 틀 (확장자는 그대로)": "名前の形式 (拡張子はそのまま)",
|
"이름 틀 (확장자는 그대로)": "名前の形式 (拡張子はそのまま)",
|
||||||
"이름에 줄바꿈이 들어 있어 FTP 로 보낼 수 없습니다": "名前に改行が含まれているため FTP で送れません",
|
"이름에 줄바꿈이 들어 있어 FTP 로 보낼 수 없습니다": "名前に改行が含まれているため FTP で送れません",
|
||||||
|
"이름으로 찾기": "名前で探す",
|
||||||
"이름을 바꿀 수 없습니다: {0}": "名前を変更できません: {0}",
|
"이름을 바꿀 수 없습니다: {0}": "名前を変更できません: {0}",
|
||||||
"이름을 바꿀 항목 하나를 고르세요.": "名前を変更する項目を 1 つ選んでください。",
|
"이름을 바꿀 항목 하나를 고르세요.": "名前を変更する項目を 1 つ選んでください。",
|
||||||
"이름을 바꿉니다": "名前を変えます",
|
"이름을 바꿉니다": "名前を変えます",
|
||||||
|
|
@ -2546,6 +2557,7 @@
|
||||||
"지우고 맞추기": "削除して同期",
|
"지우고 맞추기": "削除して同期",
|
||||||
"지우고 초기화": "削除して初期化",
|
"지우고 초기화": "削除して初期化",
|
||||||
"지우기": "削除",
|
"지우기": "削除",
|
||||||
|
"지우기 (시스템 창에서 확인)": "アンインストール (システム画面で確認)",
|
||||||
"지우기 (확인 · 휴지통)": "削除 (確認・ごみ箱)",
|
"지우기 (확인 · 휴지통)": "削除 (確認・ごみ箱)",
|
||||||
"지우기 포함": "削除あり",
|
"지우기 포함": "削除あり",
|
||||||
"지우기가 들어 있는 작업입니다": "削除を含む作業です",
|
"지우기가 들어 있는 작업입니다": "削除を含む作業です",
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,10 @@
|
||||||
"API 키 (선택)": "API 密钥 (可选)",
|
"API 키 (선택)": "API 密钥 (可选)",
|
||||||
"API 키 (필수, 무료)": "API 密钥(必填,免费)",
|
"API 키 (필수, 무료)": "API 密钥(必填,免费)",
|
||||||
"API 키 또는 아이디:비밀번호 (선택)": "API 密钥或 用户名:密码 (可选)",
|
"API 키 또는 아이디:비밀번호 (선택)": "API 密钥或 用户名:密码 (可选)",
|
||||||
|
"APK 를 저장하지 못했습니다: {0}": "无法保存 APK: {0}",
|
||||||
|
"APK 를 저장했습니다: {0}": "已保存 APK: {0}",
|
||||||
|
"APK 를 저장했습니다: {0} - 나눠진 앱 (조각 {1}개) 이라 이 파일 하나로는 설치되지 않을 수 있습니다": "已保存 APK: {0} - 此应用已拆分 ({1} 个部分),仅此文件可能无法安装",
|
||||||
|
"APK 백업": "备份 APK",
|
||||||
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "此设备无法重新编码 AV1 视频。请将编解码器改为“保持原样”后制作 MKV。(PC 版可以)",
|
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "此设备无法重新编码 AV1 视频。请将编解码器改为“保持原样”后制作 MKV。(PC 版可以)",
|
||||||
"Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "从 Android 11 起,任何文件应用都无法打开其他应用的数据文件夹 (Android/data · Android/obb)。请在该应用内使用导出或分享取出文件。",
|
"Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "从 Android 11 起,任何文件应用都无法打开其他应用的数据文件夹 (Android/data · Android/obb)。请在该应用内使用导出或分享取出文件。",
|
||||||
"Android System WebView 에 필요한 기능이 없습니다 ({0}). Play 스토어에서 Android System WebView · Chrome 을 업데이트한 뒤 다시 여세요. Tor 를 거치지 않고 열릴 수 있으므로 이 화면은 페이지를 읽지 않습니다.": "Android System WebView 缺少所需功能 ({0})。请在 Play 商店更新 Android System WebView · Chrome 后重新打开。由于页面可能不经过 Tor 打开,此画面不会加载任何页面。",
|
"Android System WebView 에 필요한 기능이 없습니다 ({0}). Play 스토어에서 Android System WebView · Chrome 을 업데이트한 뒤 다시 여세요. Tor 를 거치지 않고 열릴 수 있으므로 이 화면은 페이지를 읽지 않습니다.": "Android System WebView 缺少所需功能 ({0})。请在 Play 商店更新 Android System WebView · Chrome 后重新打开。由于页面可能不经过 Tor 打开,此画面不会加载任何页面。",
|
||||||
|
|
@ -1458,6 +1462,7 @@
|
||||||
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "正在查看的文件夹已不存在,已转到上级文件夹",
|
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "正在查看的文件夹已不存在,已转到上级文件夹",
|
||||||
"보안": "安全",
|
"보안": "安全",
|
||||||
"보이기": "显示",
|
"보이기": "显示",
|
||||||
|
"보이는 앱만 나옵니다 (모든 앱 목록 권한은 쓰지 않음)": "仅列出可见的应用 (不使用全部应用列表权限)",
|
||||||
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "选择要显示的按钮,拖动以调整顺序。",
|
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "选择要显示的按钮,拖动以调整顺序。",
|
||||||
"보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "调整需要重新编码,因此编解码器已从“保持原样”改为 H.264。",
|
"보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "调整需要重新编码,因此编解码器已从“保持原样”改为 H.264。",
|
||||||
"보조 DNS (비워 둘 수 있음)": "备用 DNS (可留空)",
|
"보조 DNS (비워 둘 수 있음)": "备用 DNS (可留空)",
|
||||||
|
|
@ -1674,6 +1679,7 @@
|
||||||
"설정을 되살리지 못했습니다: {0}": "无法恢复设置: {0}",
|
"설정을 되살리지 못했습니다: {0}": "无法恢复设置: {0}",
|
||||||
"설정을 백업으로 되살렸습니다": "已从备份恢复设置",
|
"설정을 백업으로 되살렸습니다": "已从备份恢复设置",
|
||||||
"설정을 보관하지 못했습니다: {0}": "无法保存设置: {0}",
|
"설정을 보관하지 못했습니다: {0}": "无法保存设置: {0}",
|
||||||
|
"설치 {0}": "安装于 {0}",
|
||||||
"설치 계속": "继续安装",
|
"설치 계속": "继续安装",
|
||||||
"설치 실패": "安装失败",
|
"설치 실패": "安装失败",
|
||||||
"설치 준비 완료": "准备安装",
|
"설치 준비 완료": "准备安装",
|
||||||
|
|
@ -1684,6 +1690,7 @@
|
||||||
"설치가 끝나면 프로그램이 다시 시작됩니다.": "安装完成后程序将重新启动。",
|
"설치가 끝나면 프로그램이 다시 시작됩니다.": "安装完成后程序将重新启动。",
|
||||||
"설치됨": "已安装",
|
"설치됨": "已安装",
|
||||||
"설치됨: Tor Expert Bundle {0} (Tor 공식 배포). 끄면 지웁니다.": "已安装: Tor Expert Bundle {0} (Tor 官方发布)。关闭时删除。",
|
"설치됨: Tor Expert Bundle {0} (Tor 공식 배포). 끄면 지웁니다.": "已安装: Tor Expert Bundle {0} (Tor 官方发布)。关闭时删除。",
|
||||||
|
"설치일": "安装日期",
|
||||||
"설치하는 중": "正在安装",
|
"설치하는 중": "正在安装",
|
||||||
"설치하면 필요한 파일을 내려받습니다 (준비 중)": "安装时会下载所需文件 (准备中)",
|
"설치하면 필요한 파일을 내려받습니다 (준비 중)": "安装时会下载所需文件 (准备中)",
|
||||||
"설치하지 못했습니다.\n{0}": "未能安装。\n{0}",
|
"설치하지 못했습니다.\n{0}": "未能安装。\n{0}",
|
||||||
|
|
@ -1841,7 +1848,9 @@
|
||||||
"앞의 버튼 보기": "前面的按钮",
|
"앞의 버튼 보기": "前面的按钮",
|
||||||
"앞의 복사 · 이동이 끝난 뒤에 하세요.": "请等待前一个复制 · 移动完成。",
|
"앞의 복사 · 이동이 끝난 뒤에 하세요.": "请等待前一个复制 · 移动完成。",
|
||||||
"앨범": "专辑",
|
"앨범": "专辑",
|
||||||
|
"앱 관리자": "应用管理",
|
||||||
"앱 목록 · 홈 화면과 앱 안 왼쪽 위 버튼의 아이콘입니다. 바꾸면 홈 화면에 둔 아이콘이 없어질 수 있어 앱 목록에서 다시 끌어다 놓아야 할 수 있습니다": "应用列表、主屏幕以及应用内左上角按钮的图标。更改后主屏幕上的图标可能会消失,可能需要从应用列表重新拖放",
|
"앱 목록 · 홈 화면과 앱 안 왼쪽 위 버튼의 아이콘입니다. 바꾸면 홈 화면에 둔 아이콘이 없어질 수 있어 앱 목록에서 다시 끌어다 놓아야 할 수 있습니다": "应用列表、主屏幕以及应用内左上角按钮的图标。更改后主屏幕上的图标可能会消失,可能需要从应用列表重新拖放",
|
||||||
|
"앱 목록을 읽지 못했습니다: {0}": "无法读取应用列表: {0}",
|
||||||
"앱 목록이 열렸다면, 목록에서 JJ_MKVMaker 를 찾아 눌러 \"모든 파일에 대한 접근\" 을 켜고 돌아오세요.": "如果打开的是应用列表,请在列表中找到并点按 JJ_MKVMaker,开启“所有文件访问权限”后返回。",
|
"앱 목록이 열렸다면, 목록에서 JJ_MKVMaker 를 찾아 눌러 \"모든 파일에 대한 접근\" 을 켜고 돌아오세요.": "如果打开的是应用列表,请在列表中找到并点按 JJ_MKVMaker,开启“所有文件访问权限”后返回。",
|
||||||
"앱 설정 전체 (처음 상태로)": "全部应用设置 (恢复初始状态)",
|
"앱 설정 전체 (처음 상태로)": "全部应用设置 (恢复初始状态)",
|
||||||
"앱 설정을 처음 상태로 돌렸습니다 (모두 적용하려면 앱을 다시 켜 주세요)": "已将应用设置恢复为初始状态 (请重新启动应用以全部生效)",
|
"앱 설정을 처음 상태로 돌렸습니다 (모두 적용하려면 앱을 다시 켜 주세요)": "已将应用设置恢复为初始状态 (请重新启动应用以全部生效)",
|
||||||
|
|
@ -1854,6 +1863,7 @@
|
||||||
"앱 안 웹 브라우저 · 즐겨찾기 · 동영상 다운로드": "应用内浏览器 · 书签 · 视频下载",
|
"앱 안 웹 브라우저 · 즐겨찾기 · 동영상 다운로드": "应用内浏览器 · 书签 · 视频下载",
|
||||||
"앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 (넘기는 동안 화면 위쪽에 \"광고 건너뛰는 중…\")": "应用内网页浏览器: 代为点击[跳过],无法跳过的广告会静音并快进(跳过时屏幕顶部显示“正在跳过广告…”)",
|
"앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 (넘기는 동안 화면 위쪽에 \"광고 건너뛰는 중…\")": "应用内网页浏览器: 代为点击[跳过],无法跳过的广告会静音并快进(跳过时屏幕顶部显示“正在跳过广告…”)",
|
||||||
"앱 안에서 열지 못해 다른 앱으로 엽니다: {0}": "无法在应用内打开,改用其他应用打开: {0}",
|
"앱 안에서 열지 못해 다른 앱으로 엽니다: {0}": "无法在应用内打开,改用其他应用打开: {0}",
|
||||||
|
"앱 정보 (시스템 화면)": "应用信息 (系统界面)",
|
||||||
"앱 지우기": "卸载应用",
|
"앱 지우기": "卸载应用",
|
||||||
"앱 휴지통에 {0}일 두었다가 지웁니다 (탐색기 [휴지통] 에서 되살리기).": "在应用回收站保留 {0} 天后删除 (可在资源管理器的 [回收站] 中还原)。",
|
"앱 휴지통에 {0}일 두었다가 지웁니다 (탐색기 [휴지통] 에서 되살리기).": "在应用回收站保留 {0} 天后删除 (可在资源管理器的 [回收站] 中还原)。",
|
||||||
"앱에 들어 있는 것 (기본)": "应用内置 (默认)",
|
"앱에 들어 있는 것 (기본)": "应用内置 (默认)",
|
||||||
|
|
@ -2213,6 +2223,7 @@
|
||||||
"이름 틀": "名称模式",
|
"이름 틀": "名称模式",
|
||||||
"이름 틀 (확장자는 그대로)": "名称模式 (保留扩展名)",
|
"이름 틀 (확장자는 그대로)": "名称模式 (保留扩展名)",
|
||||||
"이름에 줄바꿈이 들어 있어 FTP 로 보낼 수 없습니다": "名称中含有换行,无法通过 FTP 发送",
|
"이름에 줄바꿈이 들어 있어 FTP 로 보낼 수 없습니다": "名称中含有换行,无法通过 FTP 发送",
|
||||||
|
"이름으로 찾기": "按名称查找",
|
||||||
"이름을 바꿀 수 없습니다: {0}": "无法重命名: {0}",
|
"이름을 바꿀 수 없습니다: {0}": "无法重命名: {0}",
|
||||||
"이름을 바꿀 항목 하나를 고르세요.": "请选择一个要重命名的项目。",
|
"이름을 바꿀 항목 하나를 고르세요.": "请选择一个要重命名的项目。",
|
||||||
"이름을 바꿉니다": "重命名",
|
"이름을 바꿉니다": "重命名",
|
||||||
|
|
@ -2546,6 +2557,7 @@
|
||||||
"지우고 맞추기": "删除并同步",
|
"지우고 맞추기": "删除并同步",
|
||||||
"지우고 초기화": "删除并重置",
|
"지우고 초기화": "删除并重置",
|
||||||
"지우기": "删除",
|
"지우기": "删除",
|
||||||
|
"지우기 (시스템 창에서 확인)": "卸载 (在系统对话框中确认)",
|
||||||
"지우기 (확인 · 휴지통)": "删除 (确认 · 回收站)",
|
"지우기 (확인 · 휴지통)": "删除 (确认 · 回收站)",
|
||||||
"지우기 포함": "含删除",
|
"지우기 포함": "含删除",
|
||||||
"지우기가 들어 있는 작업입니다": "此任务包含删除",
|
"지우기가 들어 있는 작업입니다": "此任务包含删除",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
/// X-plore 비교 5-2 앱 관리자 (사용자 결정 10/21, Android). 관리자 판단: 모든 앱 목록 권한 (QUERY_ALL_PACKAGES) 은 쓰지 않고
|
||||||
|
/// 매니페스트 `<queries>` 의 LAUNCHER 로 "실행할 수 있는 앱" (런처에 보이는 앱) 만 본다.
|
||||||
|
class InstalledApp {
|
||||||
|
final String package, label, version, apk;
|
||||||
|
final DateTime installed, updated;
|
||||||
|
final int size, splits;
|
||||||
|
final bool system;
|
||||||
|
const InstalledApp({
|
||||||
|
required this.package,
|
||||||
|
required this.label,
|
||||||
|
required this.version,
|
||||||
|
required this.apk,
|
||||||
|
required this.installed,
|
||||||
|
required this.updated,
|
||||||
|
required this.size,
|
||||||
|
this.splits = 0,
|
||||||
|
this.system = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory InstalledApp.fromMap(Map<Object?, Object?> m) {
|
||||||
|
int n(String k) => (m[k] as num?)?.toInt() ?? 0;
|
||||||
|
return InstalledApp(
|
||||||
|
package: '${m['package'] ?? ''}',
|
||||||
|
label: '${m['label'] ?? m['package'] ?? ''}',
|
||||||
|
version: '${m['version'] ?? ''}',
|
||||||
|
apk: '${m['apk'] ?? ''}',
|
||||||
|
installed: DateTime.fromMillisecondsSinceEpoch(n('installed')),
|
||||||
|
updated: DateTime.fromMillisecondsSinceEpoch(n('updated')),
|
||||||
|
size: n('size'),
|
||||||
|
splits: n('splits'),
|
||||||
|
system: m['system'] == true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 기기와 이야기하는 쪽 (시험에서 바꿔 끼움)
|
||||||
|
abstract class AppManagerBackend {
|
||||||
|
Future<List<InstalledApp>> list();
|
||||||
|
Future<Uint8List?> icon(String package);
|
||||||
|
Future<bool> details(String package);
|
||||||
|
Future<bool> uninstall(String package);
|
||||||
|
|
||||||
|
/// APK 백업 폴더 (Download/JJ_MKVMaker/apk)
|
||||||
|
Future<String> backupDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AndroidAppManager implements AppManagerBackend {
|
||||||
|
static const _ch = MethodChannel('jj_mkvmaker/android');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<InstalledApp>> list() async {
|
||||||
|
final l = await _ch.invokeMethod<List<Object?>>('launcherApps') ?? const [];
|
||||||
|
return [for (final m in l) if (m is Map) InstalledApp.fromMap(m)];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Uint8List?> icon(String package) => _ch.invokeMethod<Uint8List>('appIcon', {'package': package, 'size': 96});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> details(String package) async => await _ch.invokeMethod<bool>('appDetails', {'package': package}) ?? false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> uninstall(String package) async => await _ch.invokeMethod<bool>('uninstallApp', {'package': package}) ?? false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> backupDir() async {
|
||||||
|
final root = await _ch.invokeMethod<String>('storageRoot') ?? '/storage/emulated/0';
|
||||||
|
return p.join(root, 'Download', 'JJ_MKVMaker', 'apk');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 파일 이름에 못 쓰는 글자를 _ 로
|
||||||
|
String _safe(String s) => s.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim();
|
||||||
|
|
||||||
|
/// APK 를 [dir] 에 복사한다 ("이름_판.apk", 이미 있으면 " (2)" …). 만든 파일 경로
|
||||||
|
Future<String> backupApk(InstalledApp app, String dir) async {
|
||||||
|
await Directory(dir).create(recursive: true);
|
||||||
|
final base = '${_safe(app.label)}_${_safe(app.version.isEmpty ? app.package : app.version)}';
|
||||||
|
var out = p.join(dir, '$base.apk');
|
||||||
|
for (var i = 2; await File(out).exists(); i++) {
|
||||||
|
out = p.join(dir, '$base ($i).apk');
|
||||||
|
}
|
||||||
|
await File(app.apk).copy(out);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../core/app_manager.dart';
|
||||||
|
import '../core/file_ops.dart' show formatSize;
|
||||||
|
import '../core/vfs.dart' show vDisplay;
|
||||||
|
import '../l10n/tr.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// X-plore 비교 5-2 앱 관리자 (사용자 결정 10/21, Android): 실행할 수 있는 앱 목록 - 이름 · 판 · 크기 · 설치일,
|
||||||
|
/// [APK 백업] (Download/JJ_MKVMaker/apk) · [앱 정보] (시스템 화면) · [지우기] (시스템 지우기 창)
|
||||||
|
Future<void> openAppManager(BuildContext context, {AppManagerBackend? backend}) => Navigator.of(context)
|
||||||
|
.push(MaterialPageRoute<void>(builder: (_) => AppManagerPage(backend: backend ?? AndroidAppManager())));
|
||||||
|
|
||||||
|
class AppManagerPage extends StatefulWidget {
|
||||||
|
final AppManagerBackend backend;
|
||||||
|
const AppManagerPage({super.key, required this.backend});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppManagerPage> createState() => _AppManagerPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppManagerPageState extends State<AppManagerPage> {
|
||||||
|
List<InstalledApp>? _apps;
|
||||||
|
String? _error;
|
||||||
|
String _sort = 'name';
|
||||||
|
String _filter = '';
|
||||||
|
final _icons = <String, Future<Uint8List?>>{};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
unawaited(_load());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
try {
|
||||||
|
final l = await widget.backend.list();
|
||||||
|
if (mounted) setState(() => _apps = l);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) setState(() => _error = trf('앱 목록을 읽지 못했습니다: {0}', [e]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<InstalledApp> get _shown {
|
||||||
|
final f = _filter.toLowerCase();
|
||||||
|
final l = [for (final a in _apps ?? const <InstalledApp>[]) if (f.isEmpty || a.label.toLowerCase().contains(f) || a.package.contains(f)) a];
|
||||||
|
switch (_sort) {
|
||||||
|
case 'size':
|
||||||
|
l.sort((a, b) => b.size.compareTo(a.size));
|
||||||
|
case 'installed':
|
||||||
|
l.sort((a, b) => b.installed.compareTo(a.installed));
|
||||||
|
default:
|
||||||
|
l.sort((a, b) => a.label.toLowerCase().compareTo(b.label.toLowerCase()));
|
||||||
|
}
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _snack(String t) => ScaffoldMessenger.maybeOf(context)?.showSnackBar(SnackBar(content: Text(t)));
|
||||||
|
|
||||||
|
String _day(DateTime d) => '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
Future<void> _backup(InstalledApp a) async {
|
||||||
|
try {
|
||||||
|
final out = await backupApk(a, await widget.backend.backupDir());
|
||||||
|
_snack(a.splits > 0
|
||||||
|
? trf('APK 를 저장했습니다: {0} - 나눠진 앱 (조각 {1}개) 이라 이 파일 하나로는 설치되지 않을 수 있습니다', [vDisplay(out), a.splits])
|
||||||
|
: trf('APK 를 저장했습니다: {0}', [vDisplay(out)]));
|
||||||
|
} catch (e) {
|
||||||
|
_snack(trf('APK 를 저장하지 못했습니다: {0}', [e]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _menu(InstalledApp a) async {
|
||||||
|
final w = await showModalBottomSheet<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => SafeArea(
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
ListTile(title: Text(a.label), subtitle: Text(a.package)),
|
||||||
|
ListTile(leading: const Icon(Icons.save_alt), title: Text(tr('APK 백업')), onTap: () => Navigator.pop(ctx, 'backup')),
|
||||||
|
ListTile(leading: const Icon(Icons.info_outline), title: Text(tr('앱 정보 (시스템 화면)')), onTap: () => Navigator.pop(ctx, 'details')),
|
||||||
|
if (!a.system)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.delete_outline, color: JjColors.danger),
|
||||||
|
title: Text(tr('지우기 (시스템 창에서 확인)')),
|
||||||
|
onTap: () => Navigator.pop(ctx, 'uninstall')),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
switch (w) {
|
||||||
|
case 'backup':
|
||||||
|
await _backup(a);
|
||||||
|
case 'details':
|
||||||
|
await widget.backend.details(a.package);
|
||||||
|
case 'uninstall':
|
||||||
|
await widget.backend.uninstall(a.package);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final apps = _apps;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(tr('앱 관리자')),
|
||||||
|
actions: [IconButton(tooltip: tr('새로 고침'), icon: const Icon(Icons.refresh), onPressed: _load)],
|
||||||
|
),
|
||||||
|
body: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||||
|
child: Text(tr('보이는 앱만 나옵니다 (모든 앱 목록 권한은 쓰지 않음)'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: Row(children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(isDense: true, prefixIcon: const Icon(Icons.search, size: 18), hintText: tr('이름으로 찾기')),
|
||||||
|
onChanged: (v) => setState(() => _filter = v.trim()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
DropdownButton<String>(
|
||||||
|
value: _sort,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(value: 'name', child: Text(tr('이름'))),
|
||||||
|
DropdownMenuItem(value: 'size', child: Text(tr('크기'))),
|
||||||
|
DropdownMenuItem(value: 'installed', child: Text(tr('설치일'))),
|
||||||
|
],
|
||||||
|
onChanged: (v) => setState(() => _sort = v!),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
if (_error != null) Padding(padding: const EdgeInsets.all(16), child: Text(_error!, style: const TextStyle(color: JjColors.danger))),
|
||||||
|
if (apps == null && _error == null) const Expanded(child: Center(child: CircularProgressIndicator())),
|
||||||
|
if (apps != null)
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _shown.length,
|
||||||
|
itemBuilder: (_, i) {
|
||||||
|
final a = _shown[i];
|
||||||
|
return ListTile(
|
||||||
|
leading: SizedBox(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: FutureBuilder<Uint8List?>(
|
||||||
|
future: _icons[a.package] ??= widget.backend.icon(a.package).catchError((_) => null),
|
||||||
|
builder: (_, s) => s.data == null ? const Icon(Icons.android) : Image.memory(s.data!, width: 40, height: 40),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(a.label, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
|
subtitle: Text([if (a.version.isNotEmpty) a.version, formatSize(a.size), trf('설치 {0}', [_day(a.installed)])].join(' · '),
|
||||||
|
maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
|
trailing: IconButton(tooltip: tr('더 보기'), icon: const Icon(Icons.more_vert), onPressed: () => _menu(a)),
|
||||||
|
onTap: () => _menu(a),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -58,6 +58,7 @@ import '../l10n/tr.dart';
|
||||||
import 'explorer_bookmarks.dart';
|
import 'explorer_bookmarks.dart';
|
||||||
import 'explorer_search.dart';
|
import 'explorer_search.dart';
|
||||||
import 'remote_thumb_view.dart';
|
import 'remote_thumb_view.dart';
|
||||||
|
import 'app_manager_page.dart' show openAppManager;
|
||||||
import 'dlna_page.dart' show openDlna;
|
import 'dlna_page.dart' show openDlna;
|
||||||
import 'explorer_keys.dart' show defaultExplorerKeys, effectiveExplorerKeys;
|
import 'explorer_keys.dart' show defaultExplorerKeys, effectiveExplorerKeys;
|
||||||
import 'sqlite_viewer.dart' show openSqliteViewer, sqliteExtensions;
|
import 'sqlite_viewer.dart' show openSqliteViewer, sqliteExtensions;
|
||||||
|
|
@ -201,7 +202,9 @@ enum ExplorerButton {
|
||||||
// 89: 지금 폴더를 같은 Wi-Fi 의 다른 기기 브라우저에서 보기 · 받기 (읽기 전용)
|
// 89: 지금 폴더를 같은 Wi-Fi 의 다른 기기 브라우저에서 보기 · 받기 (읽기 전용)
|
||||||
wifiShare(Icons.wifi_tethering, 'Wi-Fi 공유'),
|
wifiShare(Icons.wifi_tethering, 'Wi-Fi 공유'),
|
||||||
// X-plore 비교 e: 휴지통 (Android 앱 휴지통 · Windows 는 시스템 휴지통 열기)
|
// X-plore 비교 e: 휴지통 (Android 앱 휴지통 · Windows 는 시스템 휴지통 열기)
|
||||||
trash(Icons.restore_from_trash_outlined, '휴지통');
|
trash(Icons.restore_from_trash_outlined, '휴지통'),
|
||||||
|
// X-plore 비교 5-2 (사용자 결정 10/21): 앱 관리자 (Android 만)
|
||||||
|
apps(Icons.apps, '앱 관리자');
|
||||||
|
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String label;
|
final String label;
|
||||||
|
|
@ -211,7 +214,10 @@ enum ExplorerButton {
|
||||||
bool get rsyncOnly => this == toRight || this == toLeft || this == both || this == monitor;
|
bool get rsyncOnly => this == toRight || this == toLeft || this == both || this == monitor;
|
||||||
|
|
||||||
/// 파일 탐색기에서 고를 수 있는 버튼
|
/// 파일 탐색기에서 고를 수 있는 버튼
|
||||||
static List<ExplorerButton> get explorerValues => [for (final b in values) if (!b.rsyncOnly) b];
|
static List<ExplorerButton> get explorerValues => [for (final b in values) if (!b.rsyncOnly && (b != apps || appsButtonSupported())) b];
|
||||||
|
|
||||||
|
/// 앱 관리자 버튼을 쓰는 기기 (Android - 시험에서 바꿈)
|
||||||
|
static bool Function() appsButtonSupported = () => Platform.isAndroid;
|
||||||
|
|
||||||
/// Rsync 화면의 버튼 (늘 이대로, 두 창 사이)
|
/// Rsync 화면의 버튼 (늘 이대로, 두 창 사이)
|
||||||
// 208: 방향 실행 (Rsync 화면의 핵심) 을 앞에 - 좁아서 [더 보기] 로 가는 것은 뒤쪽
|
// 208: 방향 실행 (Rsync 화면의 핵심) 을 앞에 - 좁아서 [더 보기] 로 가는 것은 뒤쪽
|
||||||
|
|
@ -2664,6 +2670,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
await _storage(pane, pane.current);
|
await _storage(pane, pane.current);
|
||||||
case ExplorerButton.wifiShare:
|
case ExplorerButton.wifiShare:
|
||||||
await _wifiShare(pane.current);
|
await _wifiShare(pane.current);
|
||||||
|
case ExplorerButton.apps:
|
||||||
|
await openAppManager(context);
|
||||||
case ExplorerButton.trash:
|
case ExplorerButton.trash:
|
||||||
await _openTrash();
|
await _openTrash();
|
||||||
case ExplorerButton.sort:
|
case ExplorerButton.sort:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jj_mkvmaker/core/app_manager.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/app_manager_page.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/explorer_page.dart' show ExplorerButton;
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
/// X-plore 비교 5-2 앱 관리자 (사용자 결정 10/21, Android): 보이는 앱만 · APK 백업 · 앱 정보 · 지우기 (시스템 창)
|
||||||
|
class _Fake implements AppManagerBackend {
|
||||||
|
final List<InstalledApp> apps;
|
||||||
|
final String dir;
|
||||||
|
final calls = <String>[];
|
||||||
|
_Fake(this.apps, this.dir);
|
||||||
|
@override
|
||||||
|
Future<List<InstalledApp>> list() async => apps;
|
||||||
|
@override
|
||||||
|
Future<Uint8List?> icon(String package) async => null;
|
||||||
|
@override
|
||||||
|
Future<bool> details(String package) async {
|
||||||
|
calls.add('details:$package');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> uninstall(String package) async {
|
||||||
|
calls.add('uninstall:$package');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> backupDir() async => dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late Directory tmp;
|
||||||
|
setUp(() => tmp = Directory.systemTemp.createTempSync('jj_apps_'));
|
||||||
|
tearDown(() => tmp.deleteSync(recursive: true));
|
||||||
|
|
||||||
|
InstalledApp app(String label, {int size = 1000, int day = 1, bool system = false, int splits = 0, String? apk}) => InstalledApp(
|
||||||
|
package: 'com.x.${label.toLowerCase()}',
|
||||||
|
label: label,
|
||||||
|
version: '1.2',
|
||||||
|
apk: apk ?? '',
|
||||||
|
installed: DateTime(2026, 1, day),
|
||||||
|
updated: DateTime(2026, 1, day),
|
||||||
|
size: size,
|
||||||
|
system: system,
|
||||||
|
splits: splits,
|
||||||
|
);
|
||||||
|
|
||||||
|
test('기기 답 (지도) 읽기 · APK 백업 이름 (못 쓰는 글자 _ · 이미 있으면 (2))', () async {
|
||||||
|
final a = InstalledApp.fromMap({'package': 'p', 'label': 'A/B', 'version': '3', 'apk': '/x', 'installed': 0, 'updated': 0, 'size': 5, 'splits': 2, 'system': true});
|
||||||
|
expect((a.label, a.size, a.splits, a.system), ('A/B', 5, 2, true));
|
||||||
|
final src = File(p.join(tmp.path, 'base.apk'))..writeAsStringSync('apk');
|
||||||
|
final b = app('A/B', apk: src.path);
|
||||||
|
final out = p.join(tmp.path, 'out');
|
||||||
|
final first = await backupApk(b, out);
|
||||||
|
final second = await backupApk(b, out);
|
||||||
|
expect(p.basename(first), 'A_B_1.2.apk');
|
||||||
|
expect(p.basename(second), 'A_B_1.2 (2).apk');
|
||||||
|
expect(File(second).readAsStringSync(), 'apk');
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('화면: 안내 · 이름 / 크기 / 설치일 정렬 · 찾기 · [APK 백업] · [앱 정보] · 시스템 앱은 [지우기] 없음', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(900, 1400);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
final src = File(p.join(tmp.path, 'b.apk'))..writeAsStringSync('apk');
|
||||||
|
final fake = _Fake([
|
||||||
|
app('Banana', size: 300, day: 5, apk: src.path, splits: 3),
|
||||||
|
app('Apple', size: 900, day: 1),
|
||||||
|
app('Settings', size: 100, day: 2, system: true),
|
||||||
|
], p.join(tmp.path, 'Download', 'JJ_MKVMaker', 'apk'));
|
||||||
|
await tester.pumpWidget(MaterialApp(home: AppManagerPage(backend: fake)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('보이는 앱만 나옵니다 (모든 앱 목록 권한은 쓰지 않음)'), findsOneWidget);
|
||||||
|
double y(String t) => tester.getTopLeft(find.text(t)).dy;
|
||||||
|
expect(y('Apple') < y('Banana') && y('Banana') < y('Settings'), isTrue, reason: '처음은 이름 순');
|
||||||
|
await tester.tap(find.text('이름'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('크기').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(y('Apple') < y('Banana'), isTrue, reason: '큰 것부터');
|
||||||
|
await tester.enterText(find.byType(TextField), 'ban');
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Apple'), findsNothing);
|
||||||
|
// APK 백업 (나눠진 앱이면 알림)
|
||||||
|
await tester.tap(find.text('Banana'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('지우기 (시스템 창에서 확인)'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('APK 백업'));
|
||||||
|
for (var i = 0; i < 20; i++) {
|
||||||
|
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
expect(File(p.join(fake.dir, 'Banana_1.2.apk')).existsSync(), isTrue);
|
||||||
|
expect(find.textContaining('나눠진 앱 (조각 3개)'), findsOneWidget);
|
||||||
|
// 시스템 앱: 지우기 없음 · 앱 정보는 시스템 화면
|
||||||
|
await tester.enterText(find.byType(TextField), 'set');
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.text('Settings'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('지우기 (시스템 창에서 확인)'), findsNothing);
|
||||||
|
await tester.tap(find.text('앱 정보 (시스템 화면)'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(fake.calls, ['details:com.x.settings']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('가운데 버튼 [앱 관리자] 는 Android 에서만 고를 수 있음', () {
|
||||||
|
final was = ExplorerButton.appsButtonSupported;
|
||||||
|
addTearDown(() => ExplorerButton.appsButtonSupported = was);
|
||||||
|
ExplorerButton.appsButtonSupported = () => false;
|
||||||
|
expect(ExplorerButton.explorerValues, isNot(contains(ExplorerButton.apps)));
|
||||||
|
ExplorerButton.appsButtonSupported = () => true;
|
||||||
|
expect(ExplorerButton.explorerValues, contains(ExplorerButton.apps));
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue