컴포넌트 · 화면 순서 · 좌우로 밀어 화면 이동 · 알림 아이콘
- app/components.dart: 기능 묶음 (MKV · 웹 브라우저 · 파일 탐색기 · Rsync · 다운로드 · 이미지/PDF/ZIP 보기 · 문서 미리보기). 설정 components (설치한 것) · navOrder (화면 순서) · swipeNav. - 위쪽 이동 버튼: 설치한 화면만 순서대로 (하나뿐이면 그것만). 홈 화면을 제거했으면 설치된 첫 화면. - SwipeNav: 화면 가운데를 왼쪽으로 밀면 다음, 오른쪽으로 밀면 이전 화면 (끝 다음은 처음). - 환경 설정 > 컴포넌트: 켜기 · 끄기, 화면 순서 (끌어서), 좌우로 밀기 켜기 · 끄기. - Android 작업 알림: 받는 중이면 내려받기 아이콘, 동기화 · 작업 중이면 좌우로 오가는 화살표 (animation-list). - 시험: components_test (설치 · 순서 · 저장, 버튼 줄, 좌우로 밀기). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음3
parent
2553107aa5
commit
6eb0dfc70a
|
|
@ -23,7 +23,8 @@ class KeepAliveService : Service() {
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
val text = intent?.getStringExtra(EXTRA_TEXT) ?: ""
|
val text = intent?.getStringExtra(EXTRA_TEXT) ?: ""
|
||||||
val progress = intent?.getIntExtra(EXTRA_PROGRESS, -1) ?: -1
|
val progress = intent?.getIntExtra(EXTRA_PROGRESS, -1) ?: -1
|
||||||
val n = notification(this, text, progress)
|
val icon = intent?.getStringExtra(EXTRA_ICON) ?: ""
|
||||||
|
val n = notification(this, text, progress, icon)
|
||||||
running = true
|
running = true
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
// 실시간 동기화는 앱이 켜져 있는 내내 지켜봐야 하므로 하루 6시간 제한이 있는 dataSync 대신 specialUse (Android 14+)
|
// 실시간 동기화는 앱이 켜져 있는 내내 지켜봐야 하므로 하루 6시간 제한이 있는 dataSync 대신 specialUse (Android 14+)
|
||||||
|
|
@ -58,17 +59,18 @@ class KeepAliveService : Service() {
|
||||||
const val NOTIFICATION_ID = 1
|
const val NOTIFICATION_ID = 1
|
||||||
const val EXTRA_TEXT = "text"
|
const val EXTRA_TEXT = "text"
|
||||||
const val EXTRA_PROGRESS = "progress"
|
const val EXTRA_PROGRESS = "progress"
|
||||||
|
const val EXTRA_ICON = "icon"
|
||||||
|
|
||||||
/// 서비스가 떠 있는지 (떠 있으면 알림만 바꾼다 - 백그라운드에서는 서비스를 새로 시작할 수 없음)
|
/// 서비스가 떠 있는지 (떠 있으면 알림만 바꾼다 - 백그라운드에서는 서비스를 새로 시작할 수 없음)
|
||||||
@Volatile
|
@Volatile
|
||||||
var running = false
|
var running = false
|
||||||
|
|
||||||
fun update(context: Context, text: String, progress: Int) {
|
fun update(context: Context, text: String, progress: Int, icon: String) {
|
||||||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
nm.notify(NOTIFICATION_ID, notification(context, text, progress))
|
nm.notify(NOTIFICATION_ID, notification(context, text, progress, icon))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notification(context: Context, text: String, progress: Int): Notification {
|
private fun notification(context: Context, text: String, progress: Int, icon: String): Notification {
|
||||||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && nm.getNotificationChannel(CHANNEL_ID) == null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && nm.getNotificationChannel(CHANNEL_ID) == null) {
|
||||||
nm.createNotificationChannel(
|
nm.createNotificationChannel(
|
||||||
|
|
@ -91,7 +93,8 @@ class KeepAliveService : Service() {
|
||||||
} else {
|
} else {
|
||||||
@Suppress("DEPRECATION") Notification.Builder(context)
|
@Suppress("DEPRECATION") Notification.Builder(context)
|
||||||
}
|
}
|
||||||
b.setSmallIcon(android.R.drawable.stat_sys_download)
|
// 다운로드 중이면 내려받기 아이콘, 그 밖 (실시간 동기화 · MKV · AI 자막) 은 좌우로 오가는 화살표
|
||||||
|
b.setSmallIcon(if (icon == "download") android.R.drawable.stat_sys_download else R.drawable.ic_stat_sync_anim)
|
||||||
.setContentTitle("JJ_MKVMaker 작업 중")
|
.setContentTitle("JJ_MKVMaker 작업 중")
|
||||||
.setContentText(text)
|
.setContentText(text)
|
||||||
.setContentIntent(open)
|
.setContentIntent(open)
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,8 @@ class MainActivity : FlutterActivity() {
|
||||||
)
|
)
|
||||||
"setAppIcon" -> result.success(setAppIcon(call.argument<String>("id") ?: ""))
|
"setAppIcon" -> result.success(setAppIcon(call.argument<String>("id") ?: ""))
|
||||||
"keepAlive" -> {
|
"keepAlive" -> {
|
||||||
keepAlive(call.argument<String>("text") ?: "", call.argument<Int>("progress") ?: -1)
|
keepAlive(call.argument<String>("text") ?: "", call.argument<Int>("progress") ?: -1,
|
||||||
|
call.argument<String>("icon") ?: "")
|
||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
"stopKeepAlive" -> {
|
"stopKeepAlive" -> {
|
||||||
|
|
@ -409,7 +410,7 @@ class MainActivity : FlutterActivity() {
|
||||||
/// 작업 진행 알림 (포그라운드 서비스) 시작 · 갱신. 처음 한 번 알림 권한을 묻는다 (Android 13+, 거절해도 작업은 계속)
|
/// 작업 진행 알림 (포그라운드 서비스) 시작 · 갱신. 처음 한 번 알림 권한을 묻는다 (Android 13+, 거절해도 작업은 계속)
|
||||||
private var askedNotifications = false
|
private var askedNotifications = false
|
||||||
|
|
||||||
private fun keepAlive(text: String, progress: Int) {
|
private fun keepAlive(text: String, progress: Int, icon: String) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !askedNotifications && !isDestroyed &&
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !askedNotifications && !isDestroyed &&
|
||||||
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
|
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
|
|
@ -417,12 +418,13 @@ class MainActivity : FlutterActivity() {
|
||||||
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 2)
|
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 2)
|
||||||
}
|
}
|
||||||
if (KeepAliveService.running) {
|
if (KeepAliveService.running) {
|
||||||
KeepAliveService.update(applicationContext, text, progress)
|
KeepAliveService.update(applicationContext, text, progress, icon)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val i = Intent(applicationContext, KeepAliveService::class.java)
|
val i = Intent(applicationContext, KeepAliveService::class.java)
|
||||||
.putExtra(KeepAliveService.EXTRA_TEXT, text)
|
.putExtra(KeepAliveService.EXTRA_TEXT, text)
|
||||||
.putExtra(KeepAliveService.EXTRA_PROGRESS, progress)
|
.putExtra(KeepAliveService.EXTRA_PROGRESS, progress)
|
||||||
|
.putExtra(KeepAliveService.EXTRA_ICON, icon)
|
||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) applicationContext.startForegroundService(i)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) applicationContext.startForegroundService(i)
|
||||||
else applicationContext.startService(i)
|
else applicationContext.startService(i)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#FFFFFFFF" android:pathData="M2,7 L14,7 L14,4 L19,8 L14,12 L14,9 L2,9 Z"/>
|
||||||
|
<path android:fillColor="#FFFFFFFF" android:pathData="M22,15 L10,15 L10,12 L5,16 L10,20 L10,17 L22,17 Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#FFFFFFFF" android:pathData="M3.5,7 L15.5,7 L15.5,4 L20.5,8 L15.5,12 L15.5,9 L3.5,9 Z"/>
|
||||||
|
<path android:fillColor="#FFFFFFFF" android:pathData="M20.5,15 L8.5,15 L8.5,12 L3.5,16 L8.5,20 L8.5,17 L20.5,17 Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#FFFFFFFF" android:pathData="M5,7 L17,7 L17,4 L22,8 L17,12 L17,9 L5,9 Z"/>
|
||||||
|
<path android:fillColor="#FFFFFFFF" android:pathData="M19,15 L7,15 L7,12 L2,16 L7,20 L7,17 L19,17 Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#FFFFFFFF" android:pathData="M6.5,7 L18.5,7 L18.5,4 L23.5,8 L18.5,12 L18.5,9 L6.5,9 Z"/>
|
||||||
|
<path android:fillColor="#FFFFFFFF" android:pathData="M17.5,15 L5.5,15 L5.5,12 L0.5,16 L5.5,20 L5.5,17 L17.5,17 Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#FFFFFFFF" android:pathData="M5,7 L17,7 L17,4 L22,8 L17,12 L17,9 L5,9 Z"/>
|
||||||
|
<path android:fillColor="#FFFFFFFF" android:pathData="M19,15 L7,15 L7,12 L2,16 L7,20 L7,17 L19,17 Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#FFFFFFFF" android:pathData="M3.5,7 L15.5,7 L15.5,4 L20.5,8 L15.5,12 L15.5,9 L3.5,9 Z"/>
|
||||||
|
<path android:fillColor="#FFFFFFFF" android:pathData="M20.5,15 L8.5,15 L8.5,12 L3.5,16 L8.5,20 L8.5,17 L20.5,17 Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 실시간 동기화 · 작업 중 알림 아이콘: 위 화살표는 오른쪽, 아래 화살표는 왼쪽으로 오간다 -->
|
||||||
|
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false">
|
||||||
|
<item android:drawable="@drawable/ic_stat_sync_0" android:duration="150"/>
|
||||||
|
<item android:drawable="@drawable/ic_stat_sync_1" android:duration="150"/>
|
||||||
|
<item android:drawable="@drawable/ic_stat_sync_2" android:duration="150"/>
|
||||||
|
<item android:drawable="@drawable/ic_stat_sync_3" android:duration="150"/>
|
||||||
|
<item android:drawable="@drawable/ic_stat_sync_4" android:duration="150"/>
|
||||||
|
<item android:drawable="@drawable/ic_stat_sync_5" android:duration="150"/>
|
||||||
|
</animation-list>
|
||||||
|
|
@ -74,6 +74,7 @@
|
||||||
"Chrome 엔진은 프로그램을 다시 시작하면 쓸 수 있습니다. 지금 다시 시작할까요?": "The Chrome engine can be used after restarting the program. Restart now?",
|
"Chrome 엔진은 프로그램을 다시 시작하면 쓸 수 있습니다. 지금 다시 시작할까요?": "The Chrome engine can be used after restarting the program. Restart now?",
|
||||||
"Chrome 엔진을 받을 수 없습니다 (HTTP {0})": "Cannot download the Chrome engine (HTTP {0})",
|
"Chrome 엔진을 받을 수 없습니다 (HTTP {0})": "Cannot download the Chrome engine (HTTP {0})",
|
||||||
"Chrome 엔진을 시작할 수 없습니다. 환경 설정에서 Edge 로 바꿔 쓰세요.\n{0}": "Cannot start the Chrome engine. Switch to Edge in Settings.\n{0}",
|
"Chrome 엔진을 시작할 수 없습니다. 환경 설정에서 Edge 로 바꿔 쓰세요.\n{0}": "Cannot start the Chrome engine. Switch to Edge in Settings.\n{0}",
|
||||||
|
"DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "View DOC · DOCX · HWP · XLS · XLSX converted to PDF (downloads a converter)",
|
||||||
"Edge (내장)": "Edge (built-in)",
|
"Edge (내장)": "Edge (built-in)",
|
||||||
"Edge 에서 가져오기": "Import from Edge",
|
"Edge 에서 가져오기": "Import from Edge",
|
||||||
"Edge 에서 가져옴": "Imported from Edge",
|
"Edge 에서 가져옴": "Imported from Edge",
|
||||||
|
|
@ -139,6 +140,7 @@
|
||||||
"Windows 기본 브라우저": "Windows default browser",
|
"Windows 기본 브라우저": "Windows default browser",
|
||||||
"Windows 기본 프로그램": "Windows default program",
|
"Windows 기본 프로그램": "Windows default program",
|
||||||
"Windows 탐색기": "Windows Explorer",
|
"Windows 탐색기": "Windows Explorer",
|
||||||
|
"YouTube · 토렌트 다운로드 목록": "YouTube · torrent download list",
|
||||||
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp, magnet · .torrent → aria2 (no seeding after completion)",
|
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp, magnet · .torrent → aria2 (no seeding after completion)",
|
||||||
"YouTube 가 로봇 확인을 요구합니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 브라우저(Firefox 권장) 또는 cookies.txt 를 지정한 뒤 다시 받으세요.": "YouTube is asking for a robot check. In Settings > Downloads > YouTube cookies, choose a browser (Firefox recommended) or a cookies.txt, then download again.",
|
"YouTube 가 로봇 확인을 요구합니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 브라우저(Firefox 권장) 또는 cookies.txt 를 지정한 뒤 다시 받으세요.": "YouTube is asking for a robot check. In Settings > Downloads > YouTube cookies, choose a browser (Firefox recommended) or a cookies.txt, then download again.",
|
||||||
"YouTube 광고 배너 숨기기": "Hide YouTube ad banners",
|
"YouTube 광고 배너 숨기기": "Hide YouTube ad banners",
|
||||||
|
|
@ -278,6 +280,7 @@
|
||||||
"권한 허용 화면 열기": "Open the permission screen",
|
"권한 허용 화면 열기": "Open the permission screen",
|
||||||
"그 파일 하나만": "Only that file",
|
"그 파일 하나만": "Only that file",
|
||||||
"그리스어": "Greek",
|
"그리스어": "Greek",
|
||||||
|
"그림 · 만화 보기, PDF 보기 · 페이지 편집, ZIP 목록 · 만화 보기": "Images · comic reading, PDF view · page editing, ZIP list · comic reading",
|
||||||
"글을 격자에 반영": "Apply text to grid",
|
"글을 격자에 반영": "Apply text to grid",
|
||||||
"글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "Text · button size. Change it any time with − · + on the top bar; press the number in the middle to return to this size (now {0}%)",
|
"글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "Text · button size. Change it any time with − · + on the top bar; press the number in the middle to return to this size (now {0}%)",
|
||||||
"금": "Fri",
|
"금": "Fri",
|
||||||
|
|
@ -422,6 +425,7 @@
|
||||||
"동시 다운로드 수": "Simultaneous downloads",
|
"동시 다운로드 수": "Simultaneous downloads",
|
||||||
"동시에 처리할 개수 (0 = 무한)": "Number processed at once (0 = unlimited)",
|
"동시에 처리할 개수 (0 = 무한)": "Number processed at once (0 = unlimited)",
|
||||||
"동영상": "Video",
|
"동영상": "Video",
|
||||||
|
"동영상 + 자막을 MKV 로 합치기 · AI 자막 · 화면 보정": "Merge video + subtitles into MKV · AI subtitles · picture adjustments",
|
||||||
"동영상 {0}개 · 전체 선택": "{0} videos · select all",
|
"동영상 {0}개 · 전체 선택": "{0} videos · select all",
|
||||||
"동영상 목록 · 창 위치 저장": "Save video list · window position",
|
"동영상 목록 · 창 위치 저장": "Save video list · window position",
|
||||||
"동영상 목록에 추가: {0}": "Added to the video list: {0}",
|
"동영상 목록에 추가: {0}": "Added to the video list: {0}",
|
||||||
|
|
@ -442,6 +446,7 @@
|
||||||
"두 창": "Two panes",
|
"두 창": "Two panes",
|
||||||
"두 창 배치": "Two-pane arrangement",
|
"두 창 배치": "Two-pane arrangement",
|
||||||
"두 창 사이": "Between panes",
|
"두 창 사이": "Between panes",
|
||||||
|
"두 창 파일 탐색기 · 복사 · 이동 · WebDAV": "Dual-pane file explorer · copy · move · WebDAV",
|
||||||
"뒤로": "Back",
|
"뒤로": "Back",
|
||||||
"등록": "Register",
|
"등록": "Register",
|
||||||
"등록 중 일부가 실패했습니다.": "Some registrations failed.",
|
"등록 중 일부가 실패했습니다.": "Some registrations failed.",
|
||||||
|
|
@ -491,6 +496,7 @@
|
||||||
"목록의 동영상 {0}개 모두": "All {0} videos in the list",
|
"목록의 동영상 {0}개 모두": "All {0} videos in the list",
|
||||||
"몽골어": "Mongolian",
|
"몽골어": "Mongolian",
|
||||||
"무한": "Unlimited",
|
"무한": "Unlimited",
|
||||||
|
"문서 미리보기": "Document preview",
|
||||||
"문자셋 경고": "Charset warning",
|
"문자셋 경고": "Charset warning",
|
||||||
"미리보기: 목록에서 동영상을 고르세요": "Preview: choose a video in the list",
|
"미리보기: 목록에서 동영상을 고르세요": "Preview: choose a video in the list",
|
||||||
"미얀마어": "Burmese",
|
"미얀마어": "Burmese",
|
||||||
|
|
@ -627,6 +633,7 @@
|
||||||
"설치가 끝나면 프로그램이 다시 시작됩니다.": "The program restarts when the installation finishes.",
|
"설치가 끝나면 프로그램이 다시 시작됩니다.": "The program restarts when the installation finishes.",
|
||||||
"설치됨": "Installed",
|
"설치됨": "Installed",
|
||||||
"설치하는 중": "Installing",
|
"설치하는 중": "Installing",
|
||||||
|
"설치하면 필요한 파일을 내려받습니다 (준비 중)": "Installing downloads the files it needs (coming soon)",
|
||||||
"설치하지 못했습니다.\n{0}": "Could not install.\n{0}",
|
"설치하지 못했습니다.\n{0}": "Could not install.\n{0}",
|
||||||
"성공: {0}": "Succeeded: {0}",
|
"성공: {0}": "Succeeded: {0}",
|
||||||
"세로 4:5": "Portrait 4:5",
|
"세로 4:5": "Portrait 4:5",
|
||||||
|
|
@ -703,6 +710,7 @@
|
||||||
"앱 아이콘": "App icon",
|
"앱 아이콘": "App icon",
|
||||||
"앱 안 브라우저 (권장)": "In-app browser (recommended)",
|
"앱 안 브라우저 (권장)": "In-app browser (recommended)",
|
||||||
"앱 안 브라우저의 로그인 사용 (브라우저 화면에서 YouTube 에 로그인하세요)": "Use the in-app browser's sign-in (sign in to YouTube on the browser screen)",
|
"앱 안 브라우저의 로그인 사용 (브라우저 화면에서 YouTube 에 로그인하세요)": "Use the in-app browser's sign-in (sign in to YouTube on the browser screen)",
|
||||||
|
"앱 안 웹 브라우저 · 즐겨찾기 · 동영상 다운로드": "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)",
|
||||||
"앱 지우기": "Uninstall app",
|
"앱 지우기": "Uninstall app",
|
||||||
"앱에 들어 있는 것 (기본)": "Built into the app (default)",
|
"앱에 들어 있는 것 (기본)": "Built into the app (default)",
|
||||||
|
|
@ -806,6 +814,7 @@
|
||||||
"위 폴더": "Parent folder",
|
"위 폴더": "Parent folder",
|
||||||
"위로": "Up",
|
"위로": "Up",
|
||||||
"위아래": "Top and bottom",
|
"위아래": "Top and bottom",
|
||||||
|
"위쪽 이동 버튼과 좌우로 밀기의 순서입니다. 줄을 끌어 바꿉니다.": "Order of the top navigation buttons and of swiping. Drag a row to change it.",
|
||||||
"유형": "Type",
|
"유형": "Type",
|
||||||
"음성": "Audio",
|
"음성": "Audio",
|
||||||
"음성 {0}개": "{0} audio",
|
"음성 {0}개": "{0} audio",
|
||||||
|
|
@ -872,6 +881,7 @@
|
||||||
"이미 받는 중이거나 받을 수 없는 주소입니다.": "Already downloading or a URL that cannot be downloaded.",
|
"이미 받는 중이거나 받을 수 없는 주소입니다.": "Already downloading or a URL that cannot be downloaded.",
|
||||||
"이미 이 폴더에 있습니다: {0}": "Already in this folder: {0}",
|
"이미 이 폴더에 있습니다: {0}": "Already in this folder: {0}",
|
||||||
"이미 있는 폴더입니다: {0}": "The folder already exists: {0}",
|
"이미 있는 폴더입니다: {0}": "The folder already exists: {0}",
|
||||||
|
"이미지 · PDF · ZIP 보기": "Image · PDF · ZIP viewer",
|
||||||
"이미지 자막은 번역할 수 없습니다": "Image subtitles cannot be translated",
|
"이미지 자막은 번역할 수 없습니다": "Image subtitles cannot be translated",
|
||||||
"이미지 자막은 편집할 수 없습니다": "Image subtitles cannot be edited",
|
"이미지 자막은 편집할 수 없습니다": "Image subtitles cannot be edited",
|
||||||
"이전": "Previous",
|
"이전": "Previous",
|
||||||
|
|
@ -1002,6 +1012,8 @@
|
||||||
"좌 → 우": "Left → right",
|
"좌 → 우": "Left → right",
|
||||||
"좌 ⇄ 우": "Left ⇄ right",
|
"좌 ⇄ 우": "Left ⇄ right",
|
||||||
"좌우": "Side by side",
|
"좌우": "Side by side",
|
||||||
|
"좌우 두 폴더 맞추기 (rsync) · 모니터링 · 실시간 동기화 (lsync)": "Sync two folders (rsync) · monitoring · live sync (lsync)",
|
||||||
|
"좌우로 밀어 화면 이동": "Swipe left/right to switch screens",
|
||||||
"주말 종일": "Weekends, all day",
|
"주말 종일": "Weekends, all day",
|
||||||
"주소": "Address",
|
"주소": "Address",
|
||||||
"주소 입력 또는 검색 (Ctrl+L)": "Enter an address or search (Ctrl+L)",
|
"주소 입력 또는 검색 (Ctrl+L)": "Enter an address or search (Ctrl+L)",
|
||||||
|
|
@ -1094,6 +1106,7 @@
|
||||||
"취소했습니다.": "Cancelled.",
|
"취소했습니다.": "Cancelled.",
|
||||||
"칠한 시간에만 동기화하고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다. 칸을 누르거나 끌어서 칠합니다 (요일 이름 · 시 숫자를 누르면 그 줄 전체).": "Syncs only in the painted hours; changes outside are collected and synced at the next start. Tap or drag cells to paint (tap a weekday name · hour number for the whole row · column).",
|
"칠한 시간에만 동기화하고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다. 칸을 누르거나 끌어서 칠합니다 (요일 이름 · 시 숫자를 누르면 그 줄 전체).": "Syncs only in the painted hours; changes outside are collected and synced at the next start. Tap or drag cells to paint (tap a weekday name · hour number for the whole row · column).",
|
||||||
"칠한 시간이 없어 동기화하지 않습니다.": "No hours painted, so nothing will sync.",
|
"칠한 시간이 없어 동기화하지 않습니다.": "No hours painted, so nothing will sync.",
|
||||||
|
"컴포넌트": "Components",
|
||||||
"켜져 있는 창에서 (기본)": "In the open window (default)",
|
"켜져 있는 창에서 (기본)": "In the open window (default)",
|
||||||
"코덱": "Codec",
|
"코덱": "Codec",
|
||||||
"크기": "Size",
|
"크기": "Size",
|
||||||
|
|
@ -1206,15 +1219,18 @@
|
||||||
"화": "Tue",
|
"화": "Tue",
|
||||||
"화면": "Display",
|
"화면": "Display",
|
||||||
"화면 · 색 보정": "Frame · color",
|
"화면 · 색 보정": "Frame · color",
|
||||||
|
"화면 가운데를 왼쪽으로 밀면 다음 화면, 오른쪽으로 밀면 이전 화면 (마지막 다음은 처음으로).": "Swipe left in the middle of the screen for the next screen, right for the previous one (after the last comes the first).",
|
||||||
"화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "Translates all of the program's text on this device. It may take a few minutes.",
|
"화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "Translates all of the program's text on this device. It may take a few minutes.",
|
||||||
"화면 모양 따라": "Follow screen shape",
|
"화면 모양 따라": "Follow screen shape",
|
||||||
"화면 방향": "Screen orientation",
|
"화면 방향": "Screen orientation",
|
||||||
"화면 비율": "Aspect ratio",
|
"화면 비율": "Aspect ratio",
|
||||||
"화면 비율 (가로 · 세로) · 회전 · 밝기 · 대비 · 채도 · 색온도": "Aspect ratio (landscape · portrait) · rotation · brightness · contrast · saturation · temperature",
|
"화면 비율 (가로 · 세로) · 회전 · 밝기 · 대비 · 채도 · 색온도": "Aspect ratio (landscape · portrait) · rotation · brightness · contrast · saturation · temperature",
|
||||||
|
"화면 순서": "Screen order",
|
||||||
"화면 언어": "Display language",
|
"화면 언어": "Display language",
|
||||||
"화면 작게": "Smaller",
|
"화면 작게": "Smaller",
|
||||||
"화면 크게": "Larger",
|
"화면 크게": "Larger",
|
||||||
"화면 크기": "Size",
|
"화면 크기": "Size",
|
||||||
|
"화면이 있는 컴포넌트는 하나 이상 남아 있어야 합니다.": "At least one component with a screen must stay installed.",
|
||||||
"화질": "Quality",
|
"화질": "Quality",
|
||||||
"확인": "OK",
|
"확인": "OK",
|
||||||
"확인 · 압축 푸는 중…": "Verifying · extracting…",
|
"확인 · 압축 푸는 중…": "Verifying · extracting…",
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@
|
||||||
"Chrome 엔진은 프로그램을 다시 시작하면 쓸 수 있습니다. 지금 다시 시작할까요?": "Chrome エンジンはプログラムを再起動すると使えます。今すぐ再起動しますか?",
|
"Chrome 엔진은 프로그램을 다시 시작하면 쓸 수 있습니다. 지금 다시 시작할까요?": "Chrome エンジンはプログラムを再起動すると使えます。今すぐ再起動しますか?",
|
||||||
"Chrome 엔진을 받을 수 없습니다 (HTTP {0})": "Chrome エンジンをダウンロードできません(HTTP {0})",
|
"Chrome 엔진을 받을 수 없습니다 (HTTP {0})": "Chrome エンジンをダウンロードできません(HTTP {0})",
|
||||||
"Chrome 엔진을 시작할 수 없습니다. 환경 설정에서 Edge 로 바꿔 쓰세요.\n{0}": "Chrome エンジンを起動できません。環境設定で Edge に切り替えてください。\n{0}",
|
"Chrome 엔진을 시작할 수 없습니다. 환경 설정에서 Edge 로 바꿔 쓰세요.\n{0}": "Chrome エンジンを起動できません。環境設定で Edge に切り替えてください。\n{0}",
|
||||||
|
"DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "DOC・DOCX・HWP・XLS・XLSX を PDF に変換して表示 (変換ツールをダウンロード)",
|
||||||
"Edge (내장)": "Edge(内蔵)",
|
"Edge (내장)": "Edge(内蔵)",
|
||||||
"Edge 에서 가져오기": "Edge から取り込む",
|
"Edge 에서 가져오기": "Edge から取り込む",
|
||||||
"Edge 에서 가져옴": "Edge から取り込み",
|
"Edge 에서 가져옴": "Edge から取り込み",
|
||||||
|
|
@ -139,6 +140,7 @@
|
||||||
"Windows 기본 브라우저": "Windows の既定のブラウザー",
|
"Windows 기본 브라우저": "Windows の既定のブラウザー",
|
||||||
"Windows 기본 프로그램": "Windows の既定のプログラム",
|
"Windows 기본 프로그램": "Windows の既定のプログラム",
|
||||||
"Windows 탐색기": "Windows エクスプローラー",
|
"Windows 탐색기": "Windows エクスプローラー",
|
||||||
|
"YouTube · 토렌트 다운로드 목록": "YouTube・トレント ダウンロード一覧",
|
||||||
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp、マグネット・.torrent → aria2(完了後のアップロードなし)",
|
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp、マグネット・.torrent → aria2(完了後のアップロードなし)",
|
||||||
"YouTube 가 로봇 확인을 요구합니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 브라우저(Firefox 권장) 또는 cookies.txt 를 지정한 뒤 다시 받으세요.": "YouTube がロボット確認を求めています。環境設定 > ダウンロード > YouTube クッキー でブラウザー(Firefox 推奨)または cookies.txt を指定してから、もう一度ダウンロードしてください。",
|
"YouTube 가 로봇 확인을 요구합니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 브라우저(Firefox 권장) 또는 cookies.txt 를 지정한 뒤 다시 받으세요.": "YouTube がロボット確認を求めています。環境設定 > ダウンロード > YouTube クッキー でブラウザー(Firefox 推奨)または cookies.txt を指定してから、もう一度ダウンロードしてください。",
|
||||||
"YouTube 광고 배너 숨기기": "YouTube の広告バナーを隠す",
|
"YouTube 광고 배너 숨기기": "YouTube の広告バナーを隠す",
|
||||||
|
|
@ -278,6 +280,7 @@
|
||||||
"권한 허용 화면 열기": "権限の許可画面を開く",
|
"권한 허용 화면 열기": "権限の許可画面を開く",
|
||||||
"그 파일 하나만": "そのファイルだけ",
|
"그 파일 하나만": "そのファイルだけ",
|
||||||
"그리스어": "ギリシャ語",
|
"그리스어": "ギリシャ語",
|
||||||
|
"그림 · 만화 보기, PDF 보기 · 페이지 편집, ZIP 목록 · 만화 보기": "画像・マンガ表示、PDF 表示・ページ編集、ZIP 一覧・マンガ表示",
|
||||||
"글을 격자에 반영": "テキストをグリッドに反映",
|
"글을 격자에 반영": "テキストをグリッドに反映",
|
||||||
"글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "文字・ボタンの大きさです。上部バーの − ・ + でその都度変更でき、中央の数字を押すとこの大きさに戻ります(現在 {0}%)",
|
"글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "文字・ボタンの大きさです。上部バーの − ・ + でその都度変更でき、中央の数字を押すとこの大きさに戻ります(現在 {0}%)",
|
||||||
"금": "金",
|
"금": "金",
|
||||||
|
|
@ -422,6 +425,7 @@
|
||||||
"동시 다운로드 수": "同時ダウンロード数",
|
"동시 다운로드 수": "同時ダウンロード数",
|
||||||
"동시에 처리할 개수 (0 = 무한)": "同時に処理する数(0 = 無制限)",
|
"동시에 처리할 개수 (0 = 무한)": "同時に処理する数(0 = 無制限)",
|
||||||
"동영상": "動画",
|
"동영상": "動画",
|
||||||
|
"동영상 + 자막을 MKV 로 합치기 · AI 자막 · 화면 보정": "動画 + 字幕を MKV に結合・AI 字幕・画面補正",
|
||||||
"동영상 {0}개 · 전체 선택": "動画 {0}個・すべて選択",
|
"동영상 {0}개 · 전체 선택": "動画 {0}個・すべて選択",
|
||||||
"동영상 목록 · 창 위치 저장": "動画リスト・ウィンドウ位置を保存",
|
"동영상 목록 · 창 위치 저장": "動画リスト・ウィンドウ位置を保存",
|
||||||
"동영상 목록에 추가: {0}": "動画リストに追加: {0}",
|
"동영상 목록에 추가: {0}": "動画リストに追加: {0}",
|
||||||
|
|
@ -442,6 +446,7 @@
|
||||||
"두 창": "2 ペイン",
|
"두 창": "2 ペイン",
|
||||||
"두 창 배치": "2 ペインの配置",
|
"두 창 배치": "2 ペインの配置",
|
||||||
"두 창 사이": "ペインの間",
|
"두 창 사이": "ペインの間",
|
||||||
|
"두 창 파일 탐색기 · 복사 · 이동 · WebDAV": "2 画面ファイルエクスプローラー・コピー・移動・WebDAV",
|
||||||
"뒤로": "戻る",
|
"뒤로": "戻る",
|
||||||
"등록": "登録",
|
"등록": "登録",
|
||||||
"등록 중 일부가 실패했습니다.": "登録の一部に失敗しました。",
|
"등록 중 일부가 실패했습니다.": "登録の一部に失敗しました。",
|
||||||
|
|
@ -491,6 +496,7 @@
|
||||||
"목록의 동영상 {0}개 모두": "リストの動画 {0}個すべて",
|
"목록의 동영상 {0}개 모두": "リストの動画 {0}個すべて",
|
||||||
"몽골어": "モンゴル語",
|
"몽골어": "モンゴル語",
|
||||||
"무한": "無制限",
|
"무한": "無制限",
|
||||||
|
"문서 미리보기": "文書プレビュー",
|
||||||
"문자셋 경고": "文字コードの警告",
|
"문자셋 경고": "文字コードの警告",
|
||||||
"미리보기: 목록에서 동영상을 고르세요": "プレビュー: リストで動画を選んでください",
|
"미리보기: 목록에서 동영상을 고르세요": "プレビュー: リストで動画を選んでください",
|
||||||
"미얀마어": "ミャンマー語",
|
"미얀마어": "ミャンマー語",
|
||||||
|
|
@ -627,6 +633,7 @@
|
||||||
"설치가 끝나면 프로그램이 다시 시작됩니다.": "インストールが終わるとプログラムが再起動します。",
|
"설치가 끝나면 프로그램이 다시 시작됩니다.": "インストールが終わるとプログラムが再起動します。",
|
||||||
"설치됨": "インストール済み",
|
"설치됨": "インストール済み",
|
||||||
"설치하는 중": "インストール中",
|
"설치하는 중": "インストール中",
|
||||||
|
"설치하면 필요한 파일을 내려받습니다 (준비 중)": "インストールすると必要なファイルをダウンロードします (準備中)",
|
||||||
"설치하지 못했습니다.\n{0}": "インストールできませんでした。\n{0}",
|
"설치하지 못했습니다.\n{0}": "インストールできませんでした。\n{0}",
|
||||||
"성공: {0}": "成功: {0}",
|
"성공: {0}": "成功: {0}",
|
||||||
"세로 4:5": "縦 4:5",
|
"세로 4:5": "縦 4:5",
|
||||||
|
|
@ -703,6 +710,7 @@
|
||||||
"앱 아이콘": "アプリアイコン",
|
"앱 아이콘": "アプリアイコン",
|
||||||
"앱 안 브라우저 (권장)": "アプリ内ブラウザー(推奨)",
|
"앱 안 브라우저 (권장)": "アプリ内ブラウザー(推奨)",
|
||||||
"앱 안 브라우저의 로그인 사용 (브라우저 화면에서 YouTube 에 로그인하세요)": "アプリ内ブラウザーのログインを使う(ブラウザー画面で YouTube にログインしてください)",
|
"앱 안 브라우저의 로그인 사용 (브라우저 화면에서 YouTube 에 로그인하세요)": "アプリ内ブラウザーのログインを使う(ブラウザー画面で YouTube にログインしてください)",
|
||||||
|
"앱 안 웹 브라우저 · 즐겨찾기 · 동영상 다운로드": "アプリ内 Web ブラウザー・ブックマーク・動画ダウンロード",
|
||||||
"앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 (넘기는 동안 화면 위쪽에 \"광고 건너뛰는 중…\")": "アプリ内ウェブブラウザー:[スキップ]を代わりに押し、スキップできない広告は音を消して早送りします(スキップ中は画面上部に「広告をスキップ中…」)",
|
"앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 (넘기는 동안 화면 위쪽에 \"광고 건너뛰는 중…\")": "アプリ内ウェブブラウザー:[スキップ]を代わりに押し、スキップできない広告は音を消して早送りします(スキップ中は画面上部に「広告をスキップ中…」)",
|
||||||
"앱 지우기": "アプリを削除",
|
"앱 지우기": "アプリを削除",
|
||||||
"앱에 들어 있는 것 (기본)": "アプリに内蔵 (既定)",
|
"앱에 들어 있는 것 (기본)": "アプリに内蔵 (既定)",
|
||||||
|
|
@ -806,6 +814,7 @@
|
||||||
"위 폴더": "上のフォルダー",
|
"위 폴더": "上のフォルダー",
|
||||||
"위로": "上へ",
|
"위로": "上へ",
|
||||||
"위아래": "上下",
|
"위아래": "上下",
|
||||||
|
"위쪽 이동 버튼과 좌우로 밀기의 순서입니다. 줄을 끌어 바꿉니다.": "上部の移動ボタンと左右スワイプの順番です。行をドラッグして変更します。",
|
||||||
"유형": "種類",
|
"유형": "種類",
|
||||||
"음성": "音声",
|
"음성": "音声",
|
||||||
"음성 {0}개": "音声 {0}個",
|
"음성 {0}개": "音声 {0}個",
|
||||||
|
|
@ -872,6 +881,7 @@
|
||||||
"이미 받는 중이거나 받을 수 없는 주소입니다.": "既にダウンロード中か、ダウンロードできない URL です。",
|
"이미 받는 중이거나 받을 수 없는 주소입니다.": "既にダウンロード中か、ダウンロードできない URL です。",
|
||||||
"이미 이 폴더에 있습니다: {0}": "すでにこのフォルダーにあります: {0}",
|
"이미 이 폴더에 있습니다: {0}": "すでにこのフォルダーにあります: {0}",
|
||||||
"이미 있는 폴더입니다: {0}": "既にあるフォルダーです: {0}",
|
"이미 있는 폴더입니다: {0}": "既にあるフォルダーです: {0}",
|
||||||
|
"이미지 · PDF · ZIP 보기": "画像・PDF・ZIP ビューアー",
|
||||||
"이미지 자막은 번역할 수 없습니다": "画像字幕は翻訳できません",
|
"이미지 자막은 번역할 수 없습니다": "画像字幕は翻訳できません",
|
||||||
"이미지 자막은 편집할 수 없습니다": "画像字幕は編集できません",
|
"이미지 자막은 편집할 수 없습니다": "画像字幕は編集できません",
|
||||||
"이전": "前へ",
|
"이전": "前へ",
|
||||||
|
|
@ -1002,6 +1012,8 @@
|
||||||
"좌 → 우": "左 → 右",
|
"좌 → 우": "左 → 右",
|
||||||
"좌 ⇄ 우": "左 ⇄ 右",
|
"좌 ⇄ 우": "左 ⇄ 右",
|
||||||
"좌우": "左右",
|
"좌우": "左右",
|
||||||
|
"좌우 두 폴더 맞추기 (rsync) · 모니터링 · 실시간 동기화 (lsync)": "左右 2 つのフォルダーを同期 (rsync)・モニタリング・リアルタイム同期 (lsync)",
|
||||||
|
"좌우로 밀어 화면 이동": "左右スワイプで画面を移動",
|
||||||
"주말 종일": "週末 終日",
|
"주말 종일": "週末 終日",
|
||||||
"주소": "アドレス",
|
"주소": "アドレス",
|
||||||
"주소 입력 또는 검색 (Ctrl+L)": "アドレスを入力または検索(Ctrl+L)",
|
"주소 입력 또는 검색 (Ctrl+L)": "アドレスを入力または検索(Ctrl+L)",
|
||||||
|
|
@ -1094,6 +1106,7 @@
|
||||||
"취소했습니다.": "キャンセルしました。",
|
"취소했습니다.": "キャンセルしました。",
|
||||||
"칠한 시간에만 동기화하고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다. 칸을 누르거나 끌어서 칠합니다 (요일 이름 · 시 숫자를 누르면 그 줄 전체).": "塗った時間だけ同期し、それ以外は変更を貯めて次の開始時刻に同期します。マスをタップまたはドラッグして塗ります (曜日名 · 時の数字をタップするとその行 · 列全体)。",
|
"칠한 시간에만 동기화하고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다. 칸을 누르거나 끌어서 칠합니다 (요일 이름 · 시 숫자를 누르면 그 줄 전체).": "塗った時間だけ同期し、それ以外は変更を貯めて次の開始時刻に同期します。マスをタップまたはドラッグして塗ります (曜日名 · 時の数字をタップするとその行 · 列全体)。",
|
||||||
"칠한 시간이 없어 동기화하지 않습니다.": "塗った時間がないため同期しません。",
|
"칠한 시간이 없어 동기화하지 않습니다.": "塗った時間がないため同期しません。",
|
||||||
|
"컴포넌트": "コンポーネント",
|
||||||
"켜져 있는 창에서 (기본)": "開いているウィンドウで(既定)",
|
"켜져 있는 창에서 (기본)": "開いているウィンドウで(既定)",
|
||||||
"코덱": "コーデック",
|
"코덱": "コーデック",
|
||||||
"크기": "サイズ",
|
"크기": "サイズ",
|
||||||
|
|
@ -1206,15 +1219,18 @@
|
||||||
"화": "火",
|
"화": "火",
|
||||||
"화면": "画面",
|
"화면": "画面",
|
||||||
"화면 · 색 보정": "画面・カラー補正",
|
"화면 · 색 보정": "画面・カラー補正",
|
||||||
|
"화면 가운데를 왼쪽으로 밀면 다음 화면, 오른쪽으로 밀면 이전 화면 (마지막 다음은 처음으로).": "画面中央を左へスワイプで次の画面、右へで前の画面 (最後の次は最初へ)。",
|
||||||
"화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "画面の文字をすべてこの端末で翻訳します。数分かかることがあります。",
|
"화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "画面の文字をすべてこの端末で翻訳します。数分かかることがあります。",
|
||||||
"화면 모양 따라": "画面の形に合わせる",
|
"화면 모양 따라": "画面の形に合わせる",
|
||||||
"화면 방향": "画面の向き",
|
"화면 방향": "画面の向き",
|
||||||
"화면 비율": "画面比率",
|
"화면 비율": "画面比率",
|
||||||
"화면 비율 (가로 · 세로) · 회전 · 밝기 · 대비 · 채도 · 색온도": "画面比率(横・縦)・回転・明るさ・コントラスト・彩度・色温度",
|
"화면 비율 (가로 · 세로) · 회전 · 밝기 · 대비 · 채도 · 색온도": "画面比率(横・縦)・回転・明るさ・コントラスト・彩度・色温度",
|
||||||
|
"화면 순서": "画面の順番",
|
||||||
"화면 언어": "表示言語",
|
"화면 언어": "表示言語",
|
||||||
"화면 작게": "画面を小さく",
|
"화면 작게": "画面を小さく",
|
||||||
"화면 크게": "画面を大きく",
|
"화면 크게": "画面を大きく",
|
||||||
"화면 크기": "画面サイズ",
|
"화면 크기": "画面サイズ",
|
||||||
|
"화면이 있는 컴포넌트는 하나 이상 남아 있어야 합니다.": "画面のあるコンポーネントを少なくとも1つ残す必要があります。",
|
||||||
"화질": "画質",
|
"화질": "画質",
|
||||||
"확인": "OK",
|
"확인": "OK",
|
||||||
"확인 · 압축 푸는 중…": "確認・展開中…",
|
"확인 · 압축 푸는 중…": "確認・展開中…",
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@
|
||||||
"Chrome 엔진은 프로그램을 다시 시작하면 쓸 수 있습니다. 지금 다시 시작할까요?": "重新启动程序后即可使用 Chrome 引擎。现在重新启动吗?",
|
"Chrome 엔진은 프로그램을 다시 시작하면 쓸 수 있습니다. 지금 다시 시작할까요?": "重新启动程序后即可使用 Chrome 引擎。现在重新启动吗?",
|
||||||
"Chrome 엔진을 받을 수 없습니다 (HTTP {0})": "无法下载 Chrome 引擎(HTTP {0})",
|
"Chrome 엔진을 받을 수 없습니다 (HTTP {0})": "无法下载 Chrome 引擎(HTTP {0})",
|
||||||
"Chrome 엔진을 시작할 수 없습니다. 환경 설정에서 Edge 로 바꿔 쓰세요.\n{0}": "无法启动 Chrome 引擎。请在设置中切换为 Edge。\n{0}",
|
"Chrome 엔진을 시작할 수 없습니다. 환경 설정에서 Edge 로 바꿔 쓰세요.\n{0}": "无法启动 Chrome 引擎。请在设置中切换为 Edge。\n{0}",
|
||||||
|
"DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "将 DOC · DOCX · HWP · XLS · XLSX 转为 PDF 查看 (需下载转换器)",
|
||||||
"Edge (내장)": "Edge(内置)",
|
"Edge (내장)": "Edge(内置)",
|
||||||
"Edge 에서 가져오기": "从 Edge 导入",
|
"Edge 에서 가져오기": "从 Edge 导入",
|
||||||
"Edge 에서 가져옴": "从 Edge 导入",
|
"Edge 에서 가져옴": "从 Edge 导入",
|
||||||
|
|
@ -139,6 +140,7 @@
|
||||||
"Windows 기본 브라우저": "Windows 默认浏览器",
|
"Windows 기본 브라우저": "Windows 默认浏览器",
|
||||||
"Windows 기본 프로그램": "Windows 默认程序",
|
"Windows 기본 프로그램": "Windows 默认程序",
|
||||||
"Windows 탐색기": "Windows 资源管理器",
|
"Windows 탐색기": "Windows 资源管理器",
|
||||||
|
"YouTube · 토렌트 다운로드 목록": "YouTube · 种子下载列表",
|
||||||
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp,磁力链接 · .torrent → aria2(完成后不做种)",
|
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp,磁力链接 · .torrent → aria2(完成后不做种)",
|
||||||
"YouTube 가 로봇 확인을 요구합니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 브라우저(Firefox 권장) 또는 cookies.txt 를 지정한 뒤 다시 받으세요.": "YouTube 要求进行机器人验证。请在 设置 > 下载 > YouTube Cookie 中指定浏览器(推荐 Firefox)或 cookies.txt 后重新下载。",
|
"YouTube 가 로봇 확인을 요구합니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 브라우저(Firefox 권장) 또는 cookies.txt 를 지정한 뒤 다시 받으세요.": "YouTube 要求进行机器人验证。请在 设置 > 下载 > YouTube Cookie 中指定浏览器(推荐 Firefox)或 cookies.txt 后重新下载。",
|
||||||
"YouTube 광고 배너 숨기기": "隐藏 YouTube 广告横幅",
|
"YouTube 광고 배너 숨기기": "隐藏 YouTube 广告横幅",
|
||||||
|
|
@ -278,6 +280,7 @@
|
||||||
"권한 허용 화면 열기": "打开权限授予界面",
|
"권한 허용 화면 열기": "打开权限授予界面",
|
||||||
"그 파일 하나만": "仅该文件",
|
"그 파일 하나만": "仅该文件",
|
||||||
"그리스어": "希腊语",
|
"그리스어": "希腊语",
|
||||||
|
"그림 · 만화 보기, PDF 보기 · 페이지 편집, ZIP 목록 · 만화 보기": "图片 · 漫画阅读,PDF 查看 · 页面编辑,ZIP 列表 · 漫画阅读",
|
||||||
"글을 격자에 반영": "将文本应用到网格",
|
"글을 격자에 반영": "将文本应用到网格",
|
||||||
"글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "文字 · 按钮大小。可随时用顶部栏的 − · + 调整,点击中间的数字恢复为此大小(当前 {0}%)",
|
"글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "文字 · 按钮大小。可随时用顶部栏的 − · + 调整,点击中间的数字恢复为此大小(当前 {0}%)",
|
||||||
"금": "五",
|
"금": "五",
|
||||||
|
|
@ -422,6 +425,7 @@
|
||||||
"동시 다운로드 수": "同时下载数",
|
"동시 다운로드 수": "同时下载数",
|
||||||
"동시에 처리할 개수 (0 = 무한)": "同时处理的数量(0 = 无限制)",
|
"동시에 처리할 개수 (0 = 무한)": "同时处理的数量(0 = 无限制)",
|
||||||
"동영상": "视频",
|
"동영상": "视频",
|
||||||
|
"동영상 + 자막을 MKV 로 합치기 · AI 자막 · 화면 보정": "将视频 + 字幕合并为 MKV · AI 字幕 · 画面调整",
|
||||||
"동영상 {0}개 · 전체 선택": "视频 {0} 个 · 全选",
|
"동영상 {0}개 · 전체 선택": "视频 {0} 个 · 全选",
|
||||||
"동영상 목록 · 창 위치 저장": "保存视频列表 · 窗口位置",
|
"동영상 목록 · 창 위치 저장": "保存视频列表 · 窗口位置",
|
||||||
"동영상 목록에 추가: {0}": "已加入视频列表: {0}",
|
"동영상 목록에 추가: {0}": "已加入视频列表: {0}",
|
||||||
|
|
@ -442,6 +446,7 @@
|
||||||
"두 창": "双窗格",
|
"두 창": "双窗格",
|
||||||
"두 창 배치": "双窗格排列",
|
"두 창 배치": "双窗格排列",
|
||||||
"두 창 사이": "窗格之间",
|
"두 창 사이": "窗格之间",
|
||||||
|
"두 창 파일 탐색기 · 복사 · 이동 · WebDAV": "双窗格文件浏览器 · 复制 · 移动 · WebDAV",
|
||||||
"뒤로": "后退",
|
"뒤로": "后退",
|
||||||
"등록": "注册",
|
"등록": "注册",
|
||||||
"등록 중 일부가 실패했습니다.": "部分注册失败。",
|
"등록 중 일부가 실패했습니다.": "部分注册失败。",
|
||||||
|
|
@ -491,6 +496,7 @@
|
||||||
"목록의 동영상 {0}개 모두": "列表中的全部 {0} 个视频",
|
"목록의 동영상 {0}개 모두": "列表中的全部 {0} 个视频",
|
||||||
"몽골어": "蒙古语",
|
"몽골어": "蒙古语",
|
||||||
"무한": "无限制",
|
"무한": "无限制",
|
||||||
|
"문서 미리보기": "文档预览",
|
||||||
"문자셋 경고": "字符集警告",
|
"문자셋 경고": "字符集警告",
|
||||||
"미리보기: 목록에서 동영상을 고르세요": "预览: 请在列表中选择视频",
|
"미리보기: 목록에서 동영상을 고르세요": "预览: 请在列表中选择视频",
|
||||||
"미얀마어": "缅甸语",
|
"미얀마어": "缅甸语",
|
||||||
|
|
@ -627,6 +633,7 @@
|
||||||
"설치가 끝나면 프로그램이 다시 시작됩니다.": "安装完成后程序将重新启动。",
|
"설치가 끝나면 프로그램이 다시 시작됩니다.": "安装完成后程序将重新启动。",
|
||||||
"설치됨": "已安装",
|
"설치됨": "已安装",
|
||||||
"설치하는 중": "正在安装",
|
"설치하는 중": "正在安装",
|
||||||
|
"설치하면 필요한 파일을 내려받습니다 (준비 중)": "安装时会下载所需文件 (准备中)",
|
||||||
"설치하지 못했습니다.\n{0}": "未能安装。\n{0}",
|
"설치하지 못했습니다.\n{0}": "未能安装。\n{0}",
|
||||||
"성공: {0}": "成功: {0}",
|
"성공: {0}": "成功: {0}",
|
||||||
"세로 4:5": "竖屏 4:5",
|
"세로 4:5": "竖屏 4:5",
|
||||||
|
|
@ -703,6 +710,7 @@
|
||||||
"앱 아이콘": "应用图标",
|
"앱 아이콘": "应用图标",
|
||||||
"앱 안 브라우저 (권장)": "应用内浏览器(推荐)",
|
"앱 안 브라우저 (권장)": "应用内浏览器(推荐)",
|
||||||
"앱 안 브라우저의 로그인 사용 (브라우저 화면에서 YouTube 에 로그인하세요)": "使用应用内浏览器的登录(请在浏览器界面登录 YouTube)",
|
"앱 안 브라우저의 로그인 사용 (브라우저 화면에서 YouTube 에 로그인하세요)": "使用应用内浏览器的登录(请在浏览器界面登录 YouTube)",
|
||||||
|
"앱 안 웹 브라우저 · 즐겨찾기 · 동영상 다운로드": "应用内浏览器 · 书签 · 视频下载",
|
||||||
"앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 (넘기는 동안 화면 위쪽에 \"광고 건너뛰는 중…\")": "应用内网页浏览器: 代为点击[跳过],无法跳过的广告会静音并快进(跳过时屏幕顶部显示“正在跳过广告…”)",
|
"앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 (넘기는 동안 화면 위쪽에 \"광고 건너뛰는 중…\")": "应用内网页浏览器: 代为点击[跳过],无法跳过的广告会静音并快进(跳过时屏幕顶部显示“正在跳过广告…”)",
|
||||||
"앱 지우기": "卸载应用",
|
"앱 지우기": "卸载应用",
|
||||||
"앱에 들어 있는 것 (기본)": "应用内置 (默认)",
|
"앱에 들어 있는 것 (기본)": "应用内置 (默认)",
|
||||||
|
|
@ -806,6 +814,7 @@
|
||||||
"위 폴더": "上级文件夹",
|
"위 폴더": "上级文件夹",
|
||||||
"위로": "向上",
|
"위로": "向上",
|
||||||
"위아래": "上下",
|
"위아래": "上下",
|
||||||
|
"위쪽 이동 버튼과 좌우로 밀기의 순서입니다. 줄을 끌어 바꿉니다.": "顶部导航按钮和左右滑动的顺序。拖动行即可更改。",
|
||||||
"유형": "类型",
|
"유형": "类型",
|
||||||
"음성": "音频",
|
"음성": "音频",
|
||||||
"음성 {0}개": "音频 {0} 个",
|
"음성 {0}개": "音频 {0} 个",
|
||||||
|
|
@ -872,6 +881,7 @@
|
||||||
"이미 받는 중이거나 받을 수 없는 주소입니다.": "已在下载中,或该地址无法下载。",
|
"이미 받는 중이거나 받을 수 없는 주소입니다.": "已在下载中,或该地址无法下载。",
|
||||||
"이미 이 폴더에 있습니다: {0}": "已在此文件夹中: {0}",
|
"이미 이 폴더에 있습니다: {0}": "已在此文件夹中: {0}",
|
||||||
"이미 있는 폴더입니다: {0}": "文件夹已存在: {0}",
|
"이미 있는 폴더입니다: {0}": "文件夹已存在: {0}",
|
||||||
|
"이미지 · PDF · ZIP 보기": "图片 · PDF · ZIP 查看",
|
||||||
"이미지 자막은 번역할 수 없습니다": "图像字幕无法翻译",
|
"이미지 자막은 번역할 수 없습니다": "图像字幕无法翻译",
|
||||||
"이미지 자막은 편집할 수 없습니다": "图像字幕无法编辑",
|
"이미지 자막은 편집할 수 없습니다": "图像字幕无法编辑",
|
||||||
"이전": "上一个",
|
"이전": "上一个",
|
||||||
|
|
@ -1002,6 +1012,8 @@
|
||||||
"좌 → 우": "左 → 右",
|
"좌 → 우": "左 → 右",
|
||||||
"좌 ⇄ 우": "左 ⇄ 右",
|
"좌 ⇄ 우": "左 ⇄ 右",
|
||||||
"좌우": "左右",
|
"좌우": "左右",
|
||||||
|
"좌우 두 폴더 맞추기 (rsync) · 모니터링 · 실시간 동기화 (lsync)": "同步左右两个文件夹 (rsync) · 监控 · 实时同步 (lsync)",
|
||||||
|
"좌우로 밀어 화면 이동": "左右滑动切换界面",
|
||||||
"주말 종일": "周末 全天",
|
"주말 종일": "周末 全天",
|
||||||
"주소": "地址",
|
"주소": "地址",
|
||||||
"주소 입력 또는 검색 (Ctrl+L)": "输入地址或搜索(Ctrl+L)",
|
"주소 입력 또는 검색 (Ctrl+L)": "输入地址或搜索(Ctrl+L)",
|
||||||
|
|
@ -1094,6 +1106,7 @@
|
||||||
"취소했습니다.": "已取消。",
|
"취소했습니다.": "已取消。",
|
||||||
"칠한 시간에만 동기화하고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다. 칸을 누르거나 끌어서 칠합니다 (요일 이름 · 시 숫자를 누르면 그 줄 전체).": "只在涂色的时间同步,其他时间的变化累积到下次开始再同步。点按或拖动格子涂色 (点按星期名 · 小时数字可选整行 · 整列)。",
|
"칠한 시간에만 동기화하고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다. 칸을 누르거나 끌어서 칠합니다 (요일 이름 · 시 숫자를 누르면 그 줄 전체).": "只在涂色的时间同步,其他时间的变化累积到下次开始再同步。点按或拖动格子涂色 (点按星期名 · 小时数字可选整行 · 整列)。",
|
||||||
"칠한 시간이 없어 동기화하지 않습니다.": "没有涂色的时间,因此不会同步。",
|
"칠한 시간이 없어 동기화하지 않습니다.": "没有涂色的时间,因此不会同步。",
|
||||||
|
"컴포넌트": "组件",
|
||||||
"켜져 있는 창에서 (기본)": "在已打开的窗口中(默认)",
|
"켜져 있는 창에서 (기본)": "在已打开的窗口中(默认)",
|
||||||
"코덱": "编解码器",
|
"코덱": "编解码器",
|
||||||
"크기": "大小",
|
"크기": "大小",
|
||||||
|
|
@ -1206,15 +1219,18 @@
|
||||||
"화": "二",
|
"화": "二",
|
||||||
"화면": "显示",
|
"화면": "显示",
|
||||||
"화면 · 색 보정": "画面 · 色彩",
|
"화면 · 색 보정": "画面 · 色彩",
|
||||||
|
"화면 가운데를 왼쪽으로 밀면 다음 화면, 오른쪽으로 밀면 이전 화면 (마지막 다음은 처음으로).": "在屏幕中间向左滑动进入下一个界面,向右滑动返回上一个界面 (最后一个之后回到第一个)。",
|
||||||
"화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "将在此设备上翻译所有界面文字。可能需要几分钟。",
|
"화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "将在此设备上翻译所有界面文字。可能需要几分钟。",
|
||||||
"화면 모양 따라": "跟随屏幕形状",
|
"화면 모양 따라": "跟随屏幕形状",
|
||||||
"화면 방향": "屏幕方向",
|
"화면 방향": "屏幕方向",
|
||||||
"화면 비율": "画面比例",
|
"화면 비율": "画面比例",
|
||||||
"화면 비율 (가로 · 세로) · 회전 · 밝기 · 대비 · 채도 · 색온도": "画面比例(横屏 · 竖屏)· 旋转 · 亮度 · 对比度 · 饱和度 · 色温",
|
"화면 비율 (가로 · 세로) · 회전 · 밝기 · 대비 · 채도 · 색온도": "画面比例(横屏 · 竖屏)· 旋转 · 亮度 · 对比度 · 饱和度 · 色温",
|
||||||
|
"화면 순서": "界面顺序",
|
||||||
"화면 언어": "界面语言",
|
"화면 언어": "界面语言",
|
||||||
"화면 작게": "缩小界面",
|
"화면 작게": "缩小界面",
|
||||||
"화면 크게": "放大界面",
|
"화면 크게": "放大界面",
|
||||||
"화면 크기": "画面大小",
|
"화면 크기": "画面大小",
|
||||||
|
"화면이 있는 컴포넌트는 하나 이상 남아 있어야 합니다.": "至少要保留一个带界面的组件。",
|
||||||
"화질": "画质",
|
"화질": "画质",
|
||||||
"확인": "确定",
|
"확인": "确定",
|
||||||
"확인 · 압축 푸는 중…": "正在校验 · 解压…",
|
"확인 · 압축 푸는 중…": "正在校验 · 解压…",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 앱의 기능 묶음 (컴포넌트). 코드는 앱에 들어 있고, 환경 설정 > 컴포넌트에서 설치 (필요한 파일을 받아 켬) · 제거 (끔) 한다.
|
||||||
|
/// 화면이 있는 것 ([page]) 은 위쪽 이동 버튼 · 좌우로 밀기로 오간다 (순서는 [AppSettings.navOrder]).
|
||||||
|
class AppComponent {
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// 화면 이름 (tr() 로 번역)
|
||||||
|
final String name;
|
||||||
|
final String description;
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
/// 위쪽 이동 버튼 · 좌우로 밀기로 오가는 화면
|
||||||
|
final bool page;
|
||||||
|
|
||||||
|
/// 설치할 때 받아야 하는 파일이 있음 (GitHub 릴리스의 components.json)
|
||||||
|
final bool needsDownload;
|
||||||
|
|
||||||
|
/// 처음 설치할 때부터 켜져 있음
|
||||||
|
final bool builtIn;
|
||||||
|
|
||||||
|
const AppComponent(this.id, this.name, this.description, this.icon,
|
||||||
|
{this.page = true, this.needsDownload = false, this.builtIn = true});
|
||||||
|
|
||||||
|
static const mkv = AppComponent('mkv', 'MKV 만들기', '동영상 + 자막을 MKV 로 합치기 · AI 자막 · 화면 보정', Icons.video_library_outlined);
|
||||||
|
static const browser = AppComponent('browser', '웹 브라우저', '앱 안 웹 브라우저 · 즐겨찾기 · 동영상 다운로드', Icons.public);
|
||||||
|
static const explorer = AppComponent('explorer', '파일 탐색기', '두 창 파일 탐색기 · 복사 · 이동 · WebDAV', Icons.folder_copy_outlined);
|
||||||
|
static const rsync = AppComponent('rsync', 'Rsync', '좌우 두 폴더 맞추기 (rsync) · 모니터링 · 실시간 동기화 (lsync)', Icons.sync_alt);
|
||||||
|
static const downloads = AppComponent('downloads', '다운로드', 'YouTube · 토렌트 다운로드 목록', Icons.download_for_offline_outlined);
|
||||||
|
static const viewer = AppComponent('viewer', '이미지 · PDF · ZIP 보기', '그림 · 만화 보기, PDF 보기 · 페이지 편집, ZIP 목록 · 만화 보기',
|
||||||
|
Icons.photo_library_outlined, page: false);
|
||||||
|
static const docs = AppComponent('docs', '문서 미리보기', 'DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)',
|
||||||
|
Icons.description_outlined, page: false, needsDownload: true, builtIn: false);
|
||||||
|
|
||||||
|
static const all = [mkv, browser, explorer, rsync, downloads, viewer, docs];
|
||||||
|
|
||||||
|
/// 화면 있는 것의 기본 순서 (좌우로 밀기 · 위쪽 버튼)
|
||||||
|
static const defaultOrder = ['mkv', 'browser', 'explorer', 'rsync', 'downloads'];
|
||||||
|
|
||||||
|
static AppComponent? byId(String id) => all.where((c) => c.id == id).firstOrNull;
|
||||||
|
|
||||||
|
/// 처음 설치 때 켜진 것
|
||||||
|
static List<String> get defaultInstalled => [for (final c in all) if (c.builtIn) c.id];
|
||||||
|
|
||||||
|
/// 설치된 화면들을 [order] 순서로 (순서에 없는 것은 기본 순서대로 뒤에)
|
||||||
|
static List<AppComponent> pages(List<String> installed, List<String> order) {
|
||||||
|
final ids = [
|
||||||
|
...order.where(defaultOrder.contains),
|
||||||
|
...defaultOrder.where((x) => !order.contains(x)),
|
||||||
|
];
|
||||||
|
return [for (final id in ids) if (installed.contains(id)) byId(id)!];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
import 'components.dart';
|
||||||
import '../core/download_detect.dart';
|
import '../core/download_detect.dart';
|
||||||
import '../core/encode_options.dart';
|
import '../core/encode_options.dart';
|
||||||
import '../core/playlist.dart';
|
import '../core/playlist.dart';
|
||||||
|
|
@ -332,6 +333,15 @@ class AppSettings {
|
||||||
/// Rsync 화면에서 마지막으로 연 폴더 (왼쪽 · 오른쪽)
|
/// Rsync 화면에서 마지막으로 연 폴더 (왼쪽 · 오른쪽)
|
||||||
List<String> rsyncPaths = [];
|
List<String> rsyncPaths = [];
|
||||||
|
|
||||||
|
/// 설치한 (켠) 컴포넌트 id (app/components.dart). 처음엔 기본으로 들어 있는 것 모두
|
||||||
|
List<String> components = AppComponent.defaultInstalled;
|
||||||
|
|
||||||
|
/// 화면 순서 (위쪽 이동 버튼 · 화면 가운데를 좌우로 밀어 이동): 마지막 다음은 처음으로
|
||||||
|
List<String> navOrder = AppComponent.defaultOrder;
|
||||||
|
|
||||||
|
/// 화면 가운데를 좌우로 밀어 다음 · 이전 화면으로
|
||||||
|
bool swipeNav = true;
|
||||||
|
|
||||||
/// WebDAV 서버 (파일 탐색기 · Rsync 화면 위쪽 "SD 카드" 옆 탭). 비밀번호는 이 설정 파일에만.
|
/// WebDAV 서버 (파일 탐색기 · Rsync 화면 위쪽 "SD 카드" 옆 탭). 비밀번호는 이 설정 파일에만.
|
||||||
List<DavServer> webdavServers = [];
|
List<DavServer> webdavServers = [];
|
||||||
|
|
||||||
|
|
@ -444,6 +454,9 @@ class AppSettings {
|
||||||
'copyMethodFile': copyMethodFile,
|
'copyMethodFile': copyMethodFile,
|
||||||
'copyMethodFolder': copyMethodFolder,
|
'copyMethodFolder': copyMethodFolder,
|
||||||
'rsyncPaths': rsyncPaths,
|
'rsyncPaths': rsyncPaths,
|
||||||
|
'components': components,
|
||||||
|
'navOrder': navOrder,
|
||||||
|
'swipeNav': swipeNav,
|
||||||
'webdavServers': [for (final x in webdavServers) x.toJson()],
|
'webdavServers': [for (final x in webdavServers) x.toJson()],
|
||||||
'rsyncOptions': rsyncOptions,
|
'rsyncOptions': rsyncOptions,
|
||||||
'robocopyOptions': robocopyOptions,
|
'robocopyOptions': robocopyOptions,
|
||||||
|
|
@ -550,6 +563,13 @@ class AppSettings {
|
||||||
..copyMethodFile = _method(j['copyMethodFile'])
|
..copyMethodFile = _method(j['copyMethodFile'])
|
||||||
..copyMethodFolder = _method(j['copyMethodFolder'])
|
..copyMethodFolder = _method(j['copyMethodFolder'])
|
||||||
..rsyncPaths = [for (final x in (j['rsyncPaths'] as List?) ?? const []) '$x']
|
..rsyncPaths = [for (final x in (j['rsyncPaths'] as List?) ?? const []) '$x']
|
||||||
|
..components = j['components'] is List
|
||||||
|
? [for (final x in j['components'] as List) if (AppComponent.byId('$x') != null) '$x']
|
||||||
|
: AppComponent.defaultInstalled
|
||||||
|
..navOrder = j['navOrder'] is List
|
||||||
|
? [for (final x in j['navOrder'] as List) if (AppComponent.defaultOrder.contains('$x')) '$x']
|
||||||
|
: AppComponent.defaultOrder
|
||||||
|
..swipeNav = j['swipeNav'] as bool? ?? true
|
||||||
..webdavServers = [
|
..webdavServers = [
|
||||||
for (final x in (j['webdavServers'] as List?) ?? const [])
|
for (final x in (j['webdavServers'] as List?) ?? const [])
|
||||||
if (x is Map) DavServer.fromJson(x),
|
if (x is Map) DavServer.fromJson(x),
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ import 'ui/app_actions.dart';
|
||||||
import 'ui/app_drop.dart';
|
import 'ui/app_drop.dart';
|
||||||
import 'ui/browser_page.dart';
|
import 'ui/browser_page.dart';
|
||||||
import 'ui/downloads_page.dart';
|
import 'ui/downloads_page.dart';
|
||||||
import 'ui/explorer_page.dart';
|
|
||||||
import 'ui/exit_dialog.dart';
|
import 'ui/exit_dialog.dart';
|
||||||
import 'ui/home_page.dart';
|
import 'ui/home_page.dart';
|
||||||
import 'ui/monitor_page.dart' show askLiveSyncStart;
|
import 'ui/monitor_page.dart' show askLiveSyncStart;
|
||||||
|
|
@ -295,13 +294,11 @@ Future<void> main(List<String> args) async {
|
||||||
content: Text(trf('지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 ' '{0}:{1}). ' '작업 기록에 남겨 두었습니다.', [crashed.$2.hour.toString().padLeft(2, '0'), crashed.$2.minute.toString().padLeft(2, '0')])),
|
content: Text(trf('지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 ' '{0}:{1}). ' '작업 기록에 남겨 두었습니다.', [crashed.$2.hour.toString().padLeft(2, '0'), crashed.$2.minute.toString().padLeft(2, '0')])),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// 시작 화면을 "웹 브라우저" · "파일 탐색기" 로 정했으면 그 화면을 연다 (MKV 화면은 그 아래에 있음)
|
// 홈 화면이 MKV 화면이 아니면 (또는 MKV 만들기를 제거했으면) 그 화면을 연다 (MKV 화면은 그 아래에 있음)
|
||||||
if (controller.settings.startScreen == 'browser') {
|
final home = AppNavButtons.homeId(controller.settings);
|
||||||
final nav = navigatorKey.currentState;
|
final nav0 = navigatorKey.currentState;
|
||||||
if (nav != null) unawaited(BrowserPage.open(nav, c: controller, downloads: downloads, bookmarks: bookmarks));
|
if (home != 'mkv' && nav0 != null) {
|
||||||
} else if (controller.settings.startScreen == 'files') {
|
unawaited(AppNavButtons.openPage(nav0, home, c: controller, downloads: downloads, bookmarks: bookmarks));
|
||||||
final nav = navigatorKey.currentState;
|
|
||||||
if (nav != null) unawaited(ExplorerPage.open(nav, c: controller));
|
|
||||||
}
|
}
|
||||||
if (controller.settings.liveSyncOnStart == 'ask' && ctx.mounted) await askLiveSyncStart(ctx, live);
|
if (controller.settings.liveSyncOnStart == 'ask' && ctx.mounted) await askLiveSyncStart(ctx, live);
|
||||||
// 다른 버전을 쓰다가 이 버전으로 돌아왔으면 보관해 둔 이 버전의 설정을 되살릴지
|
// 다른 버전을 쓰다가 이 버전으로 돌아왔으면 보관해 둔 이 버전의 설정을 되살릴지
|
||||||
|
|
@ -481,12 +478,10 @@ Future<void> runAndroid(String dataDir) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
if (controller.settings.startScreen == 'browser') {
|
final home = AppNavButtons.homeId(controller.settings);
|
||||||
final nav = navigatorKey.currentState;
|
final nav0 = navigatorKey.currentState;
|
||||||
if (nav != null) unawaited(BrowserPage.open(nav, c: controller, downloads: downloads, bookmarks: bookmarks));
|
if (home != 'mkv' && nav0 != null) {
|
||||||
} else if (controller.settings.startScreen == 'files') {
|
unawaited(AppNavButtons.openPage(nav0, home, c: controller, downloads: downloads, bookmarks: bookmarks));
|
||||||
final nav = navigatorKey.currentState;
|
|
||||||
if (nav != null) unawaited(ExplorerPage.open(nav, c: controller));
|
|
||||||
}
|
}
|
||||||
// 다시 켤 때 실시간 동기화를 골라서 시작
|
// 다시 켤 때 실시간 동기화를 골라서 시작
|
||||||
final ctx0 = navigatorKey.currentContext;
|
final ctx0 = navigatorKey.currentContext;
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,11 @@ class AndroidKeepAlive {
|
||||||
_last = '';
|
_last = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final key = '$text|$progress';
|
// 알림 아이콘: 받는 중이면 내려받기, 아니면 (동기화 · 작업) 좌우로 오가는 화살표
|
||||||
|
final icon = running > 0 ? 'download' : 'sync';
|
||||||
|
final key = '$text|$progress|$icon';
|
||||||
if (_on && key == _last) return;
|
if (_on && key == _last) return;
|
||||||
await _ch.invokeMethod<void>('keepAlive', {'text': text, 'progress': progress});
|
await _ch.invokeMethod<void>('keepAlive', {'text': text, 'progress': progress, 'icon': icon});
|
||||||
_on = true;
|
_on = true;
|
||||||
_last = key;
|
_last = key;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../app/app_controller.dart';
|
import '../app/app_controller.dart';
|
||||||
import '../app/bookmarks_controller.dart';
|
import '../app/bookmarks_controller.dart';
|
||||||
|
import '../app/components.dart';
|
||||||
import '../app/download_manager.dart';
|
import '../app/download_manager.dart';
|
||||||
import '../app/settings.dart';
|
import '../app/settings.dart';
|
||||||
import '../services/app_shell.dart' show appIconButtonAsset;
|
import '../services/app_shell.dart' show appIconButtonAsset;
|
||||||
|
|
@ -100,37 +101,108 @@ class AppNavButtons extends StatelessWidget {
|
||||||
this.onExplorerPage = false,
|
this.onExplorerPage = false,
|
||||||
this.onRsyncPage = false});
|
this.onRsyncPage = false});
|
||||||
|
|
||||||
|
/// 버튼 줄의 가장 넓은 폭 (JJ · 화면 5개 · 뒤로)
|
||||||
static const double width = 7 * 40;
|
static const double width = 7 * 40;
|
||||||
|
|
||||||
/// MKV 화면 (맨 처음 화면) 까지 돌아가기
|
/// MKV 화면 (맨 처음 화면) 까지 돌아가기
|
||||||
static void toMkv(BuildContext context) => Navigator.of(context).popUntil((r) => r.isFirst);
|
static void toMkv(BuildContext context) => Navigator.of(context).popUntil((r) => r.isFirst);
|
||||||
|
|
||||||
/// 홈 화면으로: MKV 화면까지 돌아간 뒤, 홈 화면이 웹 브라우저 · 파일 탐색기면 그 화면을 연다
|
/// 홈 화면 (환경 설정의 "홈 화면") 의 컴포넌트 id. 그 컴포넌트를 제거했으면 설치된 첫 화면.
|
||||||
|
static String homeId(AppSettings s) {
|
||||||
|
final want = switch (s.startScreen) { 'browser' => 'browser', 'files' => 'explorer', _ => 'mkv' };
|
||||||
|
final pages = AppComponent.pages(s.components, s.navOrder);
|
||||||
|
if (pages.any((p) => p.id == want)) return want;
|
||||||
|
return pages.isEmpty ? 'mkv' : pages.first.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 그 화면으로 (이미 열려 있으면 그 화면으로 돌아감). 'mkv' 는 맨 처음 화면.
|
||||||
|
static Future<void> openPage(NavigatorState nav, String id,
|
||||||
|
{required AppController c, DownloadManager? downloads, BookmarksController? bookmarks}) async {
|
||||||
|
switch (id) {
|
||||||
|
case 'mkv':
|
||||||
|
nav.popUntil((r) => r.isFirst);
|
||||||
|
case 'browser':
|
||||||
|
if (bookmarks != null) await BrowserPage.open(nav, c: c, downloads: downloads, bookmarks: bookmarks);
|
||||||
|
case 'explorer':
|
||||||
|
await ExplorerPage.open(nav, c: c);
|
||||||
|
case 'rsync':
|
||||||
|
await ExplorerPage.openRsync(nav, c: c);
|
||||||
|
case 'downloads':
|
||||||
|
if (downloads != null) await DownloadsPage.open(nav, downloads);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 이 창에서 열 수 있는 화면 (브라우저 · 다운로드가 없는 새 창에서는 그 둘을 뺀다)
|
||||||
|
static List<AppComponent> pagesOf(AppScope scope) => [
|
||||||
|
for (final p in AppComponent.pages(scope.controller.settings.components, scope.controller.settings.navOrder))
|
||||||
|
if ((p.id != 'browser' || scope.bookmarks != null) && (p.id != 'downloads' || scope.downloads != null)) p,
|
||||||
|
];
|
||||||
|
|
||||||
|
static void open(BuildContext context, String id) {
|
||||||
|
final scope = AppScope.maybeOf(context);
|
||||||
|
if (scope == null) return;
|
||||||
|
openPage(Navigator.of(context), id, c: scope.controller, downloads: scope.downloads, bookmarks: scope.bookmarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 홈 화면으로: MKV 화면까지 돌아간 뒤, 홈 화면이 다른 화면이면 그 화면을 연다
|
||||||
static void toHome(BuildContext context) {
|
static void toHome(BuildContext context) {
|
||||||
final scope = AppScope.maybeOf(context);
|
final scope = AppScope.maybeOf(context);
|
||||||
final nav = Navigator.of(context);
|
final nav = Navigator.of(context);
|
||||||
nav.popUntil((r) => r.isFirst);
|
nav.popUntil((r) => r.isFirst);
|
||||||
if (scope == null) return;
|
if (scope == null) return;
|
||||||
final start = scope.controller.settings.startScreen;
|
final id = homeId(scope.controller.settings);
|
||||||
if (start == 'browser' && scope.bookmarks != null) {
|
if (id != 'mkv') open(context, id);
|
||||||
BrowserPage.open(nav, c: scope.controller, downloads: scope.downloads, bookmarks: scope.bookmarks!);
|
|
||||||
} else if (start == 'files') {
|
|
||||||
ExplorerPage.open(nav, c: scope.controller);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 지금 화면에서 [step] 만큼 (−1 이전 · +1 다음) 옮긴 화면으로. 끝 다음은 처음으로.
|
||||||
|
static void step(BuildContext context, String current, int step) {
|
||||||
|
final scope = AppScope.maybeOf(context);
|
||||||
|
if (scope == null) return;
|
||||||
|
final pages = pagesOf(scope);
|
||||||
|
if (pages.length < 2) return;
|
||||||
|
var i = pages.indexWhere((p) => p.id == current);
|
||||||
|
if (i < 0) i = 0;
|
||||||
|
open(context, pages[(i + step) % pages.length].id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final scope = AppScope.maybeOf(context);
|
final scope = AppScope.maybeOf(context);
|
||||||
|
// 컴포넌트 설치 · 순서를 바꾸면 바로 다시 그린다 (이 위젯은 const 로 쓰여 부모가 다시 그려도 그대로이므로)
|
||||||
|
if (scope == null) return _build(context, null);
|
||||||
|
return ListenableBuilder(listenable: scope.controller, builder: (context, _) => _build(context, scope));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _build(BuildContext context, AppScope? scope) {
|
||||||
final nav = Navigator.maybeOf(context);
|
final nav = Navigator.maybeOf(context);
|
||||||
final atRoot = !(nav?.canPop() ?? false);
|
final atRoot = !(nav?.canPop() ?? false);
|
||||||
final start = scope?.controller.settings.startScreen;
|
final current = onDownloadsPage
|
||||||
final homeName = start == 'browser' && scope?.bookmarks != null
|
? 'downloads'
|
||||||
? tr('웹 브라우저')
|
: onBrowserPage
|
||||||
: start == 'files'
|
? 'browser'
|
||||||
? tr('파일 탐색기')
|
: onExplorerPage
|
||||||
: tr('MKV 화면');
|
? 'explorer'
|
||||||
final downloads = scope?.downloads;
|
: onRsyncPage
|
||||||
|
? 'rsync'
|
||||||
|
: atRoot
|
||||||
|
? 'mkv'
|
||||||
|
: '';
|
||||||
|
final pages = scope == null ? const <AppComponent>[] : pagesOf(scope);
|
||||||
|
final homeName = switch (scope == null ? 'mkv' : homeId(scope.controller.settings)) {
|
||||||
|
'browser' => tr('웹 브라우저'),
|
||||||
|
'explorer' => tr('파일 탐색기'),
|
||||||
|
'rsync' => 'Rsync',
|
||||||
|
'downloads' => tr('다운로드 목록'),
|
||||||
|
_ => tr('MKV 화면'),
|
||||||
|
};
|
||||||
|
String tip(String id, bool here) => switch (id) {
|
||||||
|
'mkv' => here ? tr('MKV 화면 (지금 여기)') : tr('MKV 화면으로'),
|
||||||
|
'browser' => here ? tr('웹 브라우저 (지금 여기)') : tr('웹 브라우저'),
|
||||||
|
'explorer' => here ? tr('파일 탐색기 (지금 여기)') : tr('파일 탐색기'),
|
||||||
|
'rsync' => here ? tr('Rsync (지금 여기)') : 'Rsync',
|
||||||
|
'downloads' => here ? tr('다운로드 목록 (지금 여기)') : tr('다운로드 목록'),
|
||||||
|
_ => id,
|
||||||
|
};
|
||||||
Widget btn(Widget icon, String tip, VoidCallback? f, {bool here = false}) => SizedBox(
|
Widget btn(Widget icon, String tip, VoidCallback? f, {bool here = false}) => SizedBox(
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
|
|
@ -142,57 +214,60 @@ class AppNavButtons extends StatelessWidget {
|
||||||
onPressed: f,
|
onPressed: f,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return SizedBox(
|
return Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
width: width,
|
|
||||||
child: Row(children: [
|
|
||||||
btn(
|
btn(
|
||||||
Image.asset(appIconButtonAsset(scope?.controller.settings.appIcon),
|
Image.asset(appIconButtonAsset(scope?.controller.settings.appIcon),
|
||||||
width: 26, height: 26, filterQuality: FilterQuality.medium),
|
width: 26, height: 26, filterQuality: FilterQuality.medium),
|
||||||
trf('홈 화면 ({0}) · 환경 설정에서 바꿈', [homeName]),
|
trf('홈 화면 ({0}) · 환경 설정에서 바꿈', [homeName]),
|
||||||
scope == null ? null : () => toHome(context),
|
scope == null ? null : () => toHome(context),
|
||||||
),
|
),
|
||||||
|
// 설치한 화면들 (환경 설정 > 컴포넌트 의 순서). 하나뿐이면 버튼 줄은 그것만.
|
||||||
|
for (final p in pages)
|
||||||
btn(
|
btn(
|
||||||
Icon(Icons.video_library_outlined, color: atRoot ? JjColors.accent : null),
|
Icon(p.icon, color: p.id == current ? JjColors.accent : null),
|
||||||
atRoot ? tr('MKV 화면 (지금 여기)') : tr('MKV 화면으로'),
|
tip(p.id, p.id == current),
|
||||||
atRoot ? null : () => toMkv(context),
|
p.id == current ? null : () => open(context, p.id),
|
||||||
here: atRoot,
|
here: p.id == current,
|
||||||
),
|
|
||||||
// 웹 브라우저: MKV 화면 버튼 다음 (모든 화면 같은 자리)
|
|
||||||
btn(
|
|
||||||
Icon(Icons.public, color: onBrowserPage ? JjColors.accent : null),
|
|
||||||
onBrowserPage ? tr('웹 브라우저 (지금 여기)') : tr('웹 브라우저'),
|
|
||||||
scope?.bookmarks == null || onBrowserPage
|
|
||||||
? null
|
|
||||||
: () => BrowserPage.open(Navigator.of(context),
|
|
||||||
c: scope!.controller, downloads: scope.downloads, bookmarks: scope.bookmarks!),
|
|
||||||
here: onBrowserPage,
|
|
||||||
),
|
|
||||||
// 파일 탐색기: 웹 브라우저 다음
|
|
||||||
btn(
|
|
||||||
Icon(Icons.folder_copy_outlined, color: onExplorerPage ? JjColors.accent : null),
|
|
||||||
onExplorerPage ? tr('파일 탐색기 (지금 여기)') : tr('파일 탐색기'),
|
|
||||||
scope == null || onExplorerPage ? null : () => ExplorerPage.open(Navigator.of(context), c: scope.controller),
|
|
||||||
here: onExplorerPage,
|
|
||||||
),
|
|
||||||
// Rsync: 파일 탐색기 다음 (좌우 두 창 · 폴더 하나씩 골라 → ← ↔)
|
|
||||||
btn(
|
|
||||||
Icon(Icons.sync_alt, color: onRsyncPage ? JjColors.accent : null),
|
|
||||||
onRsyncPage ? tr('Rsync (지금 여기)') : 'Rsync',
|
|
||||||
scope == null || onRsyncPage ? null : () => ExplorerPage.openRsync(Navigator.of(context), c: scope.controller),
|
|
||||||
here: onRsyncPage,
|
|
||||||
),
|
),
|
||||||
btn(const Icon(Icons.arrow_back), tr('뒤로'),
|
btn(const Icon(Icons.arrow_back), tr('뒤로'),
|
||||||
// 브라우저 화면: 뒤로 키는 웹 페이지 뒤로 (PopScope) 이지만 이 버튼은 화면 이동이라 바로 닫는다
|
// 브라우저 화면: 뒤로 키는 웹 페이지 뒤로 (PopScope) 이지만 이 버튼은 화면 이동이라 바로 닫는다
|
||||||
atRoot ? null : () => onBrowserPage ? Navigator.pop(context) : Navigator.maybePop(context)),
|
atRoot ? null : () => onBrowserPage ? Navigator.pop(context) : Navigator.maybePop(context)),
|
||||||
btn(
|
]);
|
||||||
Icon(Icons.download_for_offline_outlined, color: onDownloadsPage ? JjColors.accent : null),
|
}
|
||||||
onDownloadsPage ? tr('다운로드 목록 (지금 여기)') : tr('다운로드 목록'),
|
}
|
||||||
downloads == null || onDownloadsPage
|
|
||||||
? null
|
/// 화면 가운데를 좌우로 밀면 다음 · 이전 화면으로 (환경 설정 > 컴포넌트 의 순서, 끝 다음은 처음).
|
||||||
: () => DownloadsPage.open(Navigator.of(context), downloads),
|
/// 안쪽의 옆으로 밀리는 것 (가로 목록 · 막대) 이 먼저 받고, 위 · 아래 가장자리 (1/5) 에서 시작한 것은 무시한다.
|
||||||
here: onDownloadsPage,
|
class SwipeNav extends StatefulWidget {
|
||||||
),
|
final String current;
|
||||||
]),
|
final Widget child;
|
||||||
|
const SwipeNav({super.key, required this.current, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SwipeNav> createState() => _SwipeNavState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SwipeNavState extends State<SwipeNav> {
|
||||||
|
bool _middle = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final scope = AppScope.maybeOf(context);
|
||||||
|
if (scope == null) return widget.child;
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onHorizontalDragStart: (d) {
|
||||||
|
final h = context.size?.height ?? 0;
|
||||||
|
_middle = h > 0 && d.localPosition.dy > h * 0.2 && d.localPosition.dy < h * 0.8;
|
||||||
|
},
|
||||||
|
onHorizontalDragEnd: (d) {
|
||||||
|
final v = d.primaryVelocity ?? 0;
|
||||||
|
// 켜고 끄기는 밀 때 확인 (이 위젯은 설정이 바뀌어도 다시 그려지지 않을 수 있음)
|
||||||
|
if (!scope.controller.settings.swipeNav || !_middle || v.abs() < 500) return;
|
||||||
|
// 왼쪽으로 밀면 다음 화면, 오른쪽으로 밀면 이전 화면
|
||||||
|
AppNavButtons.step(context, widget.current, v < 0 ? 1 : -1);
|
||||||
|
},
|
||||||
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -432,7 +432,10 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
||||||
child: Focus(
|
child: Focus(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: ListenableBuilder(
|
// 웹 페이지 위에서의 밀기는 웹 페이지가 받으므로, 위쪽 막대 · 즐겨찾기 줄에서 좌우로 밀면 화면 이동
|
||||||
|
body: SwipeNav(
|
||||||
|
current: 'browser',
|
||||||
|
child: ListenableBuilder(
|
||||||
listenable: Listenable.merge([bm, widget.c]),
|
listenable: Listenable.merge([bm, widget.c]),
|
||||||
builder: (context, _) => Column(children: [
|
builder: (context, _) => Column(children: [
|
||||||
_toolbar(),
|
_toolbar(),
|
||||||
|
|
@ -505,6 +508,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../app/app_controller.dart';
|
||||||
|
import '../app/components.dart';
|
||||||
|
import '../l10n/tr.dart';
|
||||||
|
import 'setting_tile.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// 환경 설정 > 컴포넌트: 기능 묶음 설치 (켜기) · 제거 (끄기), 화면 순서 (끌어서 바꿈), 좌우로 밀어 화면 이동.
|
||||||
|
/// 화면이 하나만 남으면 위쪽 이동 버튼 줄에는 그것만 보인다.
|
||||||
|
class ComponentSettings extends StatelessWidget {
|
||||||
|
final AppController c;
|
||||||
|
const ComponentSettings({super.key, required this.c});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final s = c.settings;
|
||||||
|
final installed = s.components;
|
||||||
|
final pages = AppComponent.pages(installed, s.navOrder);
|
||||||
|
|
||||||
|
Future<void> setInstalled(AppComponent x, bool on) async {
|
||||||
|
// 화면 있는 것이 하나도 남지 않게는 못 한다
|
||||||
|
if (!on && x.page && pages.length <= 1 && pages.first.id == x.id) {
|
||||||
|
ScaffoldMessenger.maybeOf(context)
|
||||||
|
?.showSnackBar(SnackBar(content: Text(tr('화면이 있는 컴포넌트는 하나 이상 남아 있어야 합니다.'))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await c.updateSettings((v) => v.components = [
|
||||||
|
for (final id in v.components) if (id != x.id) id,
|
||||||
|
if (on) x.id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||||
|
SwitchListTile(
|
||||||
|
value: s.swipeNav,
|
||||||
|
onChanged: (v) => c.updateSettings((x) => x.swipeNav = v),
|
||||||
|
title: Text(tr('좌우로 밀어 화면 이동')),
|
||||||
|
subtitle: Text(tr('화면 가운데를 왼쪽으로 밀면 다음 화면, 오른쪽으로 밀면 이전 화면 (마지막 다음은 처음으로).')),
|
||||||
|
),
|
||||||
|
for (final x in AppComponent.all)
|
||||||
|
SettingTile(
|
||||||
|
leading: Icon(x.icon, color: installed.contains(x.id) ? JjColors.accent : JjColors.textDim),
|
||||||
|
title: Text(tr(x.name)),
|
||||||
|
subtitle: Text(x.needsDownload ? '${tr(x.description)}\n${tr('설치하면 필요한 파일을 내려받습니다 (준비 중)')}' : tr(x.description)),
|
||||||
|
trailing: Switch(
|
||||||
|
value: installed.contains(x.id),
|
||||||
|
onChanged: x.needsDownload && !installed.contains(x.id) ? null : (v) => setInstalled(x, v),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
SettingTile(
|
||||||
|
leading: const Icon(Icons.swap_vert),
|
||||||
|
title: Text(tr('화면 순서')),
|
||||||
|
subtitle: Text(tr('위쪽 이동 버튼과 좌우로 밀기의 순서입니다. 줄을 끌어 바꿉니다.')),
|
||||||
|
),
|
||||||
|
ReorderableListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
buildDefaultDragHandles: false,
|
||||||
|
onReorderItem: (from, to) {
|
||||||
|
final ids = [for (final p in pages) p.id];
|
||||||
|
ids.insert(to, ids.removeAt(from));
|
||||||
|
// 설치하지 않은 화면은 원래 자리 순서대로 뒤에
|
||||||
|
c.updateSettings((v) => v.navOrder = [...ids, ...AppComponent.defaultOrder.where((x) => !ids.contains(x))]);
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
for (final (i, p) in pages.indexed)
|
||||||
|
ReorderableDragStartListener(
|
||||||
|
key: ValueKey(p.id),
|
||||||
|
index: i,
|
||||||
|
child: ListTile(
|
||||||
|
dense: true,
|
||||||
|
leading: Text('${i + 1}', style: const TextStyle(color: JjColors.textDim)),
|
||||||
|
title: Row(children: [Icon(p.icon, size: 18), const SizedBox(width: 10), Text(tr(p.name))]),
|
||||||
|
trailing: const Icon(Icons.drag_handle),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -75,7 +75,9 @@ class _DownloadsPageState extends State<DownloadsPage> {
|
||||||
final hasSel = d.selected.isNotEmpty;
|
final hasSel = d.selected.isNotEmpty;
|
||||||
final allSel = d.tasks.isNotEmpty && d.selected.length == d.tasks.length;
|
final allSel = d.tasks.isNotEmpty && d.selected.length == d.tasks.length;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Column(children: [
|
body: SwipeNav(
|
||||||
|
current: 'downloads',
|
||||||
|
child: Column(children: [
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
nav: const AppNavButtons(onDownloadsPage: true),
|
nav: const AppNavButtons(onDownloadsPage: true),
|
||||||
actions: const AppActions(),
|
actions: const AppActions(),
|
||||||
|
|
@ -182,6 +184,7 @@ class _DownloadsPageState extends State<DownloadsPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1544,7 +1544,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
child: Focus(
|
child: Focus(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: ListenableBuilder(
|
body: SwipeNav(
|
||||||
|
current: widget.rsync ? 'rsync' : 'explorer',
|
||||||
|
child: ListenableBuilder(
|
||||||
listenable: c,
|
listenable: c,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
_dropRemovedServers();
|
_dropRemovedServers();
|
||||||
|
|
@ -1562,6 +1564,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,8 @@ class HomePage extends StatelessWidget {
|
||||||
listenable: c,
|
listenable: c,
|
||||||
builder: (context, _) => Scaffold(
|
builder: (context, _) => Scaffold(
|
||||||
// 끌어다 놓기는 앱 전체에서 받는다 (ui/app_drop.dart 의 AppDropArea - 어느 화면에서 놓아도 이 목록에 추가)
|
// 끌어다 놓기는 앱 전체에서 받는다 (ui/app_drop.dart 의 AppDropArea - 어느 화면에서 놓아도 이 목록에 추가)
|
||||||
body: SizedBox(
|
body: SwipeNav(
|
||||||
|
current: 'mkv',
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_TopBar(c: c, downloads: downloads, onExit: onExit, bookmarks: bookmarks),
|
_TopBar(c: c, downloads: downloads, onExit: onExit, bookmarks: bookmarks),
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import 'explorer_look.dart' show ExplorerStyle;
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
import '../l10n/tr.dart';
|
import '../l10n/tr.dart';
|
||||||
import 'setting_tile.dart';
|
import 'setting_tile.dart';
|
||||||
|
import 'component_settings.dart';
|
||||||
|
|
||||||
/// 동시 작업 수 고르기: 1 · 5 · 10 · 무한(0) · 직접 입력
|
/// 동시 작업 수 고르기: 1 · 5 · 10 · 무한(0) · 직접 입력
|
||||||
class CountSelector extends StatelessWidget {
|
class CountSelector extends StatelessWidget {
|
||||||
|
|
@ -326,6 +327,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
_group('components', Icons.extension_outlined, tr('컴포넌트'), [ComponentSettings(c: c)]),
|
||||||
_group('display', Icons.aspect_ratio, tr('화면'), [
|
_group('display', Icons.aspect_ratio, tr('화면'), [
|
||||||
if (!desk)
|
if (!desk)
|
||||||
SettingTile(
|
SettingTile(
|
||||||
|
|
@ -852,12 +854,13 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
final _groupKeys = <String, GlobalKey>{};
|
final _groupKeys = <String, GlobalKey>{};
|
||||||
|
|
||||||
static const _groupIds = [
|
static const _groupIds = [
|
||||||
'general', 'display', 'mkv', 'subtitle', 'play', 'browser', 'files', 'rsync', 'download', 'run', 'cleanup', 'about', //
|
'general', 'components', 'display', 'mkv', 'subtitle', 'play', 'browser', 'files', 'rsync', 'download', 'run', 'cleanup', 'about', //
|
||||||
];
|
];
|
||||||
|
|
||||||
String _groupTitle(String id) => switch (id) {
|
String _groupTitle(String id) => switch (id) {
|
||||||
'general' => tr('일반'),
|
'general' => tr('일반'),
|
||||||
'display' => tr('화면'),
|
'display' => tr('화면'),
|
||||||
|
'components' => tr('컴포넌트'),
|
||||||
'mkv' => tr('MKV 만들기'),
|
'mkv' => tr('MKV 만들기'),
|
||||||
'subtitle' => tr('자막 (AI · 인터넷)'),
|
'subtitle' => tr('자막 (AI · 인터넷)'),
|
||||||
'play' => tr('재생'),
|
'play' => tr('재생'),
|
||||||
|
|
@ -905,7 +908,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
Icon(icon, size: 20, color: JjColors.accent),
|
Icon(icon, size: 20, color: JjColors.accent),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
Text(title, key: ValueKey('group-title:$id'), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
...children,
|
...children,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||||
|
import 'package:jj_mkvmaker/app/components.dart';
|
||||||
|
import 'package:jj_mkvmaker/app/settings.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/app_actions.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/home_page.dart';
|
||||||
|
|
||||||
|
AppController _plain() =>
|
||||||
|
AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('컴포넌트: 설치한 화면만 순서대로 · 홈 화면은 제거되면 첫 화면 · 설정 저장', () {
|
||||||
|
final s = AppSettings();
|
||||||
|
expect(s.components, containsAll(['mkv', 'browser', 'explorer', 'rsync', 'downloads', 'viewer']));
|
||||||
|
expect(s.components, isNot(contains('docs'))); // 변환기를 받아야 하는 것은 처음엔 꺼짐
|
||||||
|
expect([for (final p in AppComponent.pages(s.components, s.navOrder)) p.id],
|
||||||
|
['mkv', 'browser', 'explorer', 'rsync', 'downloads']);
|
||||||
|
s
|
||||||
|
..navOrder = ['explorer', 'mkv']
|
||||||
|
..components = ['mkv', 'explorer', 'downloads'];
|
||||||
|
expect([for (final p in AppComponent.pages(s.components, s.navOrder)) p.id], ['explorer', 'mkv', 'downloads']);
|
||||||
|
s.startScreen = 'browser'; // 제거한 화면 → 설치된 첫 화면
|
||||||
|
expect(AppNavButtons.homeId(s), 'explorer');
|
||||||
|
final back = AppSettings.fromJson(s.toJson());
|
||||||
|
expect(back.components, ['mkv', 'explorer', 'downloads']);
|
||||||
|
expect(back.navOrder, ['explorer', 'mkv']);
|
||||||
|
// 예전 설정 (컴포넌트 없음) 은 기본
|
||||||
|
expect(AppSettings.fromJson({}).components, AppComponent.defaultInstalled);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('위쪽 이동 버튼: 설치한 화면만 · 설정을 바꾸면 바로 · 하나뿐이면 그것만', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(1500, 900);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
final c = _plain();
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
builder: (context, child) => AppScope(controller: c, child: child!),
|
||||||
|
home: HomePage(c: c),
|
||||||
|
));
|
||||||
|
expect(find.byTooltip('MKV 화면 (지금 여기)'), findsOneWidget);
|
||||||
|
expect(find.byTooltip('파일 탐색기'), findsOneWidget);
|
||||||
|
expect(find.byTooltip('Rsync'), findsOneWidget);
|
||||||
|
await c.updateSettings((s) => s.components = ['mkv', 'explorer']);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.byTooltip('Rsync'), findsNothing);
|
||||||
|
expect(find.byTooltip('파일 탐색기'), findsOneWidget);
|
||||||
|
await c.updateSettings((s) => s.components = ['mkv']);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.byTooltip('파일 탐색기'), findsNothing);
|
||||||
|
expect(find.byTooltip('MKV 화면 (지금 여기)'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('화면 가운데를 좌우로 밀면 순서대로 다음 · 이전 화면 (끝 다음은 처음)', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(1500, 900);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
final c = _plain();
|
||||||
|
c.settings
|
||||||
|
..components = ['mkv', 'explorer', 'rsync']
|
||||||
|
..navOrder = ['mkv', 'explorer', 'rsync'];
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
builder: (context, child) => AppScope(controller: c, child: child!),
|
||||||
|
home: HomePage(c: c),
|
||||||
|
));
|
||||||
|
Future<void> swipe(double dx) async {
|
||||||
|
await tester.flingFrom(const Offset(750, 450), Offset(dx, 0), 2000);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
await swipe(-600); // 왼쪽으로 → 다음 (파일 탐색기)
|
||||||
|
expect(find.byTooltip('파일 탐색기 (지금 여기)'), findsOneWidget);
|
||||||
|
await swipe(-600);
|
||||||
|
expect(find.byTooltip('Rsync (지금 여기)'), findsOneWidget);
|
||||||
|
await swipe(-600); // 마지막 다음은 처음
|
||||||
|
expect(find.byTooltip('MKV 화면 (지금 여기)'), findsOneWidget);
|
||||||
|
await swipe(600); // 오른쪽으로 → 이전 (끝으로)
|
||||||
|
expect(find.byTooltip('Rsync (지금 여기)'), findsOneWidget);
|
||||||
|
// 끄면 밀어도 그대로
|
||||||
|
await c.updateSettings((s) => s.swipeNav = false);
|
||||||
|
await tester.pump();
|
||||||
|
await swipe(600);
|
||||||
|
expect(find.byTooltip('Rsync (지금 여기)'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -215,8 +215,9 @@ void main() {
|
||||||
for (final g in groups) {
|
for (final g in groups) {
|
||||||
await tester.tap(find.widgetWithText(ActionChip, g));
|
await tester.tap(find.widgetWithText(ActionChip, g));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
// 묶음 제목 (버튼이 아닌 카드 안의 글) 이 화면 위쪽에 보인다
|
// 묶음 제목 (버튼이 아닌 카드 머리의 글) 이 화면 위쪽에 보인다 (컴포넌트 묶음의 같은 이름 줄과 헷갈리지 않게 머리 글만)
|
||||||
final title = find.descendant(of: find.byType(Card), matching: find.text(g)).first;
|
final title = find.byWidgetPredicate((w) =>
|
||||||
|
w is Text && w.data == g && w.key is ValueKey<String> && (w.key as ValueKey<String>).value.startsWith('group-title:'));
|
||||||
final y = tester.getTopLeft(title).dy;
|
final y = tester.getTopLeft(title).dy;
|
||||||
expect(y, inInclusiveRange(0, 400), reason: '$g 묶음 제목 위치 $y');
|
expect(y, inInclusiveRange(0, 400), reason: '$g 묶음 제목 위치 $y');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue