백그라운드로 실행 · 다시 켤 때 동기화 (바로 / 골라서 / 시작 안 함)
- 설정 > 파일 탐색기 > 실시간 동기화 (모니터링 > lsync 위에도): [백그라운드로 실행], [앱을 다시 켤 때 동기화] - Windows: 백그라운드로 실행 = 창 ✕ 를 누르면 트레이로 (종료 동작 설정과 같은 값), 끄면 닫을 때 종료 - Android: 켜면 ← · 최근 앱에서 밀어도 Dart 엔진을 살려 두고 (엔진 캐시) 동기화 · MKV 만들기 · 다운로드 계속, 작업 알림 서비스가 동기화를 지켜보는 동안에도 유지 (Android 14+ specialUse), [종료] 는 프로세스까지 끝냄 - lsync: 이번 실행 동안 멈춤 / 시작, 모두 멈춤 / 모두 시작, 켤 때 고르기 창 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음3
parent
b38253fd67
commit
fa0b7e56b2
|
|
@ -9,6 +9,7 @@
|
||||||
<!-- 앱이 화면에서 내려가도 다운로드 · MKV 만들기 · AI 자막을 계속하고 진행 알림을 보여 주기 위해 -->
|
<!-- 앱이 화면에서 내려가도 다운로드 · MKV 만들기 · AI 자막을 계속하고 진행 알림을 보여 주기 위해 -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<!-- 환경 설정 > 최신 버전 확인: 받은 새 APK 로 설치 화면 열기 -->
|
<!-- 환경 설정 > 최신 버전 확인: 받은 새 APK 로 설치 화면 열기 -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||||
|
|
@ -132,7 +133,11 @@
|
||||||
<service
|
<service
|
||||||
android:name=".KeepAliveService"
|
android:name=".KeepAliveService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync"/>
|
android:foregroundServiceType="dataSync|specialUse">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="Keeps folder live sync, video conversion and downloads running while the app is closed (user setting: run in background)"/>
|
||||||
|
</service>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
|
||||||
/// 다운로드 · MKV 만들기 · AI 자막이 진행 중일 때 앱이 화면에서 내려가도 Android 가 멈추지 않도록 하는
|
/// 다운로드 · MKV 만들기 · AI 자막 · 실시간 동기화 (백그라운드로 실행) 가 진행 중일 때 앱이 화면에서 내려가도 Android 가 멈추지 않도록 하는
|
||||||
/// 포그라운드 서비스. 알림에 진행 상황을 보여 주고, 화면이 꺼져도 CPU 가 잠들지 않게 한다.
|
/// 포그라운드 서비스. 알림에 진행 상황을 보여 주고, 화면이 꺼져도 CPU 가 잠들지 않게 한다.
|
||||||
/// 실제 작업은 앱 (Dart) 이 같은 프로세스에서 계속한다. 작업이 모두 끝나면 앱이 멈춘다.
|
/// 실제 작업은 앱 (Dart) 이 같은 프로세스에서 계속한다. 작업이 모두 끝나면 앱이 멈춘다.
|
||||||
class KeepAliveService : Service() {
|
class KeepAliveService : Service() {
|
||||||
|
|
@ -25,7 +25,10 @@ class KeepAliveService : Service() {
|
||||||
val progress = intent?.getIntExtra(EXTRA_PROGRESS, -1) ?: -1
|
val progress = intent?.getIntExtra(EXTRA_PROGRESS, -1) ?: -1
|
||||||
val n = notification(this, text, progress)
|
val n = notification(this, text, progress)
|
||||||
running = true
|
running = true
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
// 실시간 동기화는 앱이 켜져 있는 내내 지켜봐야 하므로 하루 6시간 제한이 있는 dataSync 대신 specialUse (Android 14+)
|
||||||
|
startForeground(NOTIFICATION_ID, n, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
startForeground(NOTIFICATION_ID, n, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
startForeground(NOTIFICATION_ID, n, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||||
} else {
|
} else {
|
||||||
startForeground(NOTIFICATION_ID, n)
|
startForeground(NOTIFICATION_ID, n)
|
||||||
|
|
@ -70,7 +73,7 @@ class KeepAliveService : Service() {
|
||||||
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(
|
||||||
NotificationChannel(CHANNEL_ID, "작업 진행", NotificationManager.IMPORTANCE_LOW).apply {
|
NotificationChannel(CHANNEL_ID, "작업 진행", NotificationManager.IMPORTANCE_LOW).apply {
|
||||||
description = "다운로드 · MKV 만들기 · AI 자막 진행 상황"
|
description = "다운로드 · MKV 만들기 · AI 자막 · 동기화 진행 상황"
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.jj.jj_mkvmaker
|
package com.jj.jj_mkvmaker
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
|
@ -12,6 +13,7 @@ import com.yausername.ffmpeg.FFmpeg
|
||||||
import com.yausername.youtubedl_android.YoutubeDL
|
import com.yausername.youtubedl_android.YoutubeDL
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.embedding.engine.FlutterEngineCache
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
@ -20,6 +22,40 @@ class MainActivity : FlutterActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
/// 작업 알림을 눌렀을 때: 작업 현황 화면을 연다
|
/// 작업 알림을 눌렀을 때: 작업 현황 화면을 연다
|
||||||
const val ACTION_SHOW_JOBS = "com.jj.jj_mkvmaker.SHOW_JOBS"
|
const val ACTION_SHOW_JOBS = "com.jj.jj_mkvmaker.SHOW_JOBS"
|
||||||
|
|
||||||
|
private const val ENGINE_ID = "main"
|
||||||
|
|
||||||
|
/// 백그라운드로 실행 (환경 설정, Dart 가 알려 준다): 켜져 있으면 화면을 닫아도 (← · 최근 앱에서 밀기)
|
||||||
|
/// Dart 엔진을 없애지 않아 동기화 · MKV 만들기 · 다운로드가 계속된다 (작업 알림 서비스가 프로세스를 살려 둠).
|
||||||
|
@Volatile
|
||||||
|
var background = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 앞 화면이 닫힌 뒤 살아 있던 엔진을 다시 붙였는지 (Dart 는 이미 돌고 있다)
|
||||||
|
private var reusedEngine = false
|
||||||
|
|
||||||
|
/// 엔진은 앱 (프로세스) 이 갖는다: 화면이 닫혀도 [background] 면 살려 두고, 다시 열면 그 엔진을 붙인다
|
||||||
|
override fun provideFlutterEngine(context: Context): FlutterEngine {
|
||||||
|
val cache = FlutterEngineCache.getInstance()
|
||||||
|
cache.get(ENGINE_ID)?.let {
|
||||||
|
reusedEngine = true
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
return FlutterEngine(context.applicationContext).also { cache.put(ENGINE_ID, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 백그라운드로 실행이 꺼져 있으면 지금처럼 화면과 함께 엔진도 끝낸다
|
||||||
|
override fun shouldDestroyEngineWithHost(): Boolean {
|
||||||
|
if (background) return false
|
||||||
|
FlutterEngineCache.getInstance().remove(ENGINE_ID)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 첫 화면에서 ←: 백그라운드로 실행이면 앱을 끝내지 않고 뒤로 보낸다 (홈 버튼과 같음)
|
||||||
|
override fun popSystemNavigator(): Boolean {
|
||||||
|
if (!background) return false
|
||||||
|
moveTaskToBack(true)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private var channel: MethodChannel? = null
|
private var channel: MethodChannel? = null
|
||||||
|
|
@ -36,8 +72,30 @@ class MainActivity : FlutterActivity() {
|
||||||
pendingShowJobs = intent?.action == ACTION_SHOW_JOBS
|
pendingShowJobs = intent?.action == ACTION_SHOW_JOBS
|
||||||
val ch = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "jj_mkvmaker/android")
|
val ch = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "jj_mkvmaker/android")
|
||||||
channel = ch
|
channel = ch
|
||||||
|
if (reusedEngine) {
|
||||||
|
// Dart 는 이미 돌고 있어 take… 로 가져가지 않는다: 바로 보낸다
|
||||||
|
if (pendingShowJobs) ch.invokeMethod("showJobs", null)
|
||||||
|
if (pendingOpen.isNotEmpty()) ch.invokeMethod("openFiles", pendingOpen)
|
||||||
|
pendingShowJobs = false
|
||||||
|
pendingOpen = emptyList()
|
||||||
|
}
|
||||||
ch.setMethodCallHandler { call, result ->
|
ch.setMethodCallHandler { call, result ->
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
|
"setBackground" -> {
|
||||||
|
background = call.argument<Boolean>("on") ?: false
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
"exitApp" -> {
|
||||||
|
// [종료]: 작업 알림 · 살려 둔 엔진까지 모두 끝낸다
|
||||||
|
result.success(null)
|
||||||
|
background = false
|
||||||
|
stopService(Intent(applicationContext, KeepAliveService::class.java))
|
||||||
|
FlutterEngineCache.getInstance().remove(ENGINE_ID)
|
||||||
|
finishAndRemoveTask()
|
||||||
|
android.os.Handler(mainLooper).postDelayed({
|
||||||
|
android.os.Process.killProcess(android.os.Process.myPid())
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
"takeOpenedFiles" -> {
|
"takeOpenedFiles" -> {
|
||||||
result.success(pendingOpen)
|
result.success(pendingOpen)
|
||||||
pendingOpen = emptyList()
|
pendingOpen = emptyList()
|
||||||
|
|
@ -81,7 +139,7 @@ class MainActivity : FlutterActivity() {
|
||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
"stopKeepAlive" -> {
|
"stopKeepAlive" -> {
|
||||||
stopService(Intent(this, KeepAliveService::class.java))
|
stopService(Intent(applicationContext, KeepAliveService::class.java))
|
||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
"downloadToolsInit" -> inBackground(result) { downloadToolsInit() }
|
"downloadToolsInit" -> inBackground(result) { downloadToolsInit() }
|
||||||
|
|
@ -345,21 +403,22 @@ class MainActivity : FlutterActivity() {
|
||||||
private var askedNotifications = false
|
private var askedNotifications = false
|
||||||
|
|
||||||
private fun keepAlive(text: String, progress: Int) {
|
private fun keepAlive(text: String, progress: Int) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !askedNotifications &&
|
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
|
||||||
) {
|
) {
|
||||||
askedNotifications = true
|
askedNotifications = true
|
||||||
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 2)
|
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 2)
|
||||||
}
|
}
|
||||||
if (KeepAliveService.running) {
|
if (KeepAliveService.running) {
|
||||||
KeepAliveService.update(this, text, progress)
|
KeepAliveService.update(applicationContext, text, progress)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val i = Intent(this, 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)
|
||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) startForegroundService(i) else startService(i)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) applicationContext.startForegroundService(i)
|
||||||
|
else applicationContext.startService(i)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// 백그라운드에서는 새로 시작할 수 없다 (Android 12+). 이미 떠 있으면 다음 갱신 때 바뀐다
|
// 백그라운드에서는 새로 시작할 수 없다 (Android 12+). 이미 떠 있으면 다음 갱신 때 바뀐다
|
||||||
android.util.Log.w("jj_mkvmaker", "keepAlive", e)
|
android.util.Log.w("jj_mkvmaker", "keepAlive", e)
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,7 @@
|
||||||
"… 외 {0}개": "… and {0} more",
|
"… 외 {0}개": "… and {0} more",
|
||||||
"※ 다른 작업 중이면 대기열에 넣고 차례대로 합니다. 번역하는 동안 브라우저를 써도 됩니다.": "※ If another job is running, this is queued and done in order. You can use the browser while translating.",
|
"※ 다른 작업 중이면 대기열에 넣고 차례대로 합니다. 번역하는 동안 브라우저를 써도 됩니다.": "※ If another job is running, this is queued and done in order. You can use the browser while translating.",
|
||||||
"※ 시간이 오래 걸립니다 (영상 1시간 ≈ 음성인식 15~30분 + 언어당 번역 10~20분)": "※ This takes a long time (1 hour of video ≈ 15–30 min speech recognition + 10–20 min translation per language)",
|
"※ 시간이 오래 걸립니다 (영상 1시간 ≈ 음성인식 15~30분 + 언어당 번역 10~20분)": "※ This takes a long time (1 hour of video ≈ 15–30 min speech recognition + 10–20 min translation per language)",
|
||||||
|
"← 로 닫거나 최근 앱 목록에서 밀어도 동기화 · MKV 만들기 · 다운로드를 계속합니다 (알림에 표시). 끄면 지금처럼 닫을 때 끝납니다. 완전히 끝내려면 [종료].": "Closing with ← or swiping away from recent apps keeps sync · MKV creation · downloads running (shown in a notification). When off, closing ends the app as before. Use [Exit] to quit completely.",
|
||||||
"− 차갑게 · + 따뜻하게": "− cooler · + warmer",
|
"− 차갑게 · + 따뜻하게": "− cooler · + warmer",
|
||||||
"⋮⋮ 를 끌어 순서 바꾸기 · 아이콘을 끌어 폴더에 넣기 · 오른쪽 클릭 메뉴": "Drag ⋮⋮ to reorder · drag the icon into a folder · right-click menu",
|
"⋮⋮ 를 끌어 순서 바꾸기 · 아이콘을 끌어 폴더에 넣기 · 오른쪽 클릭 메뉴": "Drag ⋮⋮ to reorder · drag the icon into a folder · right-click menu",
|
||||||
"── 시작 {0} (Android) ──": "── Start {0} (Android) ──",
|
"── 시작 {0} (Android) ──": "── Start {0} (Android) ──",
|
||||||
|
|
@ -250,9 +251,11 @@
|
||||||
"계속 편집": "Keep editing",
|
"계속 편집": "Keep editing",
|
||||||
"고르기": "Choose",
|
"고르기": "Choose",
|
||||||
"고르지 않음": "Not chosen",
|
"고르지 않음": "Not chosen",
|
||||||
|
"고른 것 시작": "Start selected",
|
||||||
"고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "Best quality at or below the chosen size (nearest if the video lacks that size)",
|
"고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "Best quality at or below the chosen size (nearest if the video lacks that size)",
|
||||||
"고지 문서": "Notices",
|
"고지 문서": "Notices",
|
||||||
"고화질": "High quality",
|
"고화질": "High quality",
|
||||||
|
"골라서 시작": "Choose and start",
|
||||||
"관리 메뉴": "Manage menu",
|
"관리 메뉴": "Manage menu",
|
||||||
"광고 건너뛰는 중…": "Skipping ad…",
|
"광고 건너뛰는 중…": "Skipping ad…",
|
||||||
"권한 허용 화면 열기": "Open the permission screen",
|
"권한 허용 화면 열기": "Open the permission screen",
|
||||||
|
|
@ -343,6 +346,7 @@
|
||||||
"다시 만들기 대기로": "Back to waiting for remake",
|
"다시 만들기 대기로": "Back to waiting for remake",
|
||||||
"다시 세기": "Recount",
|
"다시 세기": "Recount",
|
||||||
"다시 시작": "Restart",
|
"다시 시작": "Restart",
|
||||||
|
"다시 켤 때 동기화": "Sync on restart",
|
||||||
"다운로드": "Downloads",
|
"다운로드": "Downloads",
|
||||||
"다운로드 (jj_yt-dlp · jj_aria2)": "Downloads (jj_yt-dlp · jj_aria2)",
|
"다운로드 (jj_yt-dlp · jj_aria2)": "Downloads (jj_yt-dlp · jj_aria2)",
|
||||||
"다운로드 ({0})": "Downloads ({0})",
|
"다운로드 ({0})": "Downloads ({0})",
|
||||||
|
|
@ -386,6 +390,8 @@
|
||||||
"덴마크어": "Danish",
|
"덴마크어": "Danish",
|
||||||
"독일어": "German",
|
"독일어": "German",
|
||||||
"돌아가기": "Go back",
|
"돌아가기": "Go back",
|
||||||
|
"동기화 {0}개 · 맞추는 중": "{0} syncs · syncing",
|
||||||
|
"동기화 {0}개 지켜보는 중": "Watching {0} syncs",
|
||||||
"동기화 시간": "Sync time",
|
"동기화 시간": "Sync time",
|
||||||
"동기화 시간 (계속 / 시간 지정)": "Sync time (always / scheduled)",
|
"동기화 시간 (계속 / 시간 지정)": "Sync time (always / scheduled)",
|
||||||
"동시 MKV 변환 수": "Simultaneous MKV conversions",
|
"동시 MKV 변환 수": "Simultaneous MKV conversions",
|
||||||
|
|
@ -434,14 +440,19 @@
|
||||||
"매번 고르기": "Ask every time",
|
"매번 고르기": "Ask every time",
|
||||||
"매일 종일": "Every day, all day",
|
"매일 종일": "Every day, all day",
|
||||||
"멈춤": "Stopped",
|
"멈춤": "Stopped",
|
||||||
|
"멈춤 (이번 실행 동안)": "Pause (this session)",
|
||||||
|
"멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "Paused (this session) · press the start button to sync again",
|
||||||
"멈춤 · {0}": "Stopped · {0}",
|
"멈춤 · {0}": "Stopped · {0}",
|
||||||
"모니터링": "Monitor",
|
"모니터링": "Monitor",
|
||||||
"모델 내려받는 중 {0}%": "Downloading model {0}%",
|
"모델 내려받는 중 {0}%": "Downloading model {0}%",
|
||||||
"모델 내려받는 중: {0} ({1})": "Downloading model: {0} ({1})",
|
"모델 내려받는 중: {0} ({1})": "Downloading model: {0} ({1})",
|
||||||
"모델 파일이 잘렸습니다 (위치 {0})": "The model file is truncated (position {0})",
|
"모델 파일이 잘렸습니다 (위치 {0})": "The model file is truncated (position {0})",
|
||||||
"모델을 내려받을 수 없습니다 ({0}): {1}": "Cannot download the model ({0}): {1}",
|
"모델을 내려받을 수 없습니다 ({0}): {1}": "Cannot download the model ({0}): {1}",
|
||||||
|
"모두 나중에": "All later",
|
||||||
"모두 내장 플레이어": "Built-in player for all",
|
"모두 내장 플레이어": "Built-in player for all",
|
||||||
|
"모두 멈춤": "Pause all",
|
||||||
"모두 선택": "Select all",
|
"모두 선택": "Select all",
|
||||||
|
"모두 시작 (멈춘 것 {0}개)": "Start all ({0} paused)",
|
||||||
"모두 종료": "Quit all",
|
"모두 종료": "Quit all",
|
||||||
"모두 지우기": "Clear all",
|
"모두 지우기": "Clear all",
|
||||||
"모두 초기화": "Reset all",
|
"모두 초기화": "Reset all",
|
||||||
|
|
@ -463,6 +474,8 @@
|
||||||
"바꾼 것이 없습니다 (원본 그대로)": "Nothing changed (original as is)",
|
"바꾼 것이 없습니다 (원본 그대로)": "Nothing changed (original as is)",
|
||||||
"바뀐 날": "Modified",
|
"바뀐 날": "Modified",
|
||||||
"바뀐 내용": "Changes",
|
"바뀐 내용": "Changes",
|
||||||
|
"바로 시작 (기본)": "Start right away (default)",
|
||||||
|
"바로 시작: 켜진 동기화를 모두 시작 · 골라서 시작: 켤 때 고르기 · 시작 안 함: 모니터링 > lsync 의 시작 버튼으로": "Start right away: start all enabled syncs · Choose: pick at startup · Don't start: use the start button in Monitor > lsync",
|
||||||
"바로 재생 (기본)": "Play right away (default)",
|
"바로 재생 (기본)": "Play right away (default)",
|
||||||
"바로 전 작업: {0}": "Previous job: {0}",
|
"바로 전 작업: {0}": "Previous job: {0}",
|
||||||
"받는 중": "Downloading",
|
"받는 중": "Downloading",
|
||||||
|
|
@ -484,6 +497,7 @@
|
||||||
"백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)": "Background: hides only the window and keeps downloading · converting. Reopen with {0} or the tray icon.\nTo quit completely: right-click the tray icon > Quit (always quits whatever the setting)",
|
"백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)": "Background: hides only the window and keeps downloading · converting. Reopen with {0} or the tray icon.\nTo quit completely: right-click the tray icon > Quit (always quits whatever the setting)",
|
||||||
"백그라운드로": "To background",
|
"백그라운드로": "To background",
|
||||||
"백그라운드로 계속 (기본)": "Keep running in the background (default)",
|
"백그라운드로 계속 (기본)": "Keep running in the background (default)",
|
||||||
|
"백그라운드로 실행": "Run in background",
|
||||||
"버튼 구성": "Button layout",
|
"버튼 구성": "Button layout",
|
||||||
"번역 모델 내려받는 중 {0}%": "Downloading translation model {0}%",
|
"번역 모델 내려받는 중 {0}%": "Downloading translation model {0}%",
|
||||||
"번역 모델 내려받는 중: {0}": "Downloading translation model: {0}",
|
"번역 모델 내려받는 중: {0}": "Downloading translation model: {0}",
|
||||||
|
|
@ -599,12 +613,15 @@
|
||||||
"시작": "Start",
|
"시작": "Start",
|
||||||
"시작 = 현재": "Start = now",
|
"시작 = 현재": "Start = now",
|
||||||
"시작 · 웹 브라우저": "Startup · web browser",
|
"시작 · 웹 브라우저": "Startup · web browser",
|
||||||
|
"시작 안 함 (모니터링에서 시작)": "Don't start (start from Monitor)",
|
||||||
"시작: {0} → {1}\\{2}{3}": "Started: {0} → {1}\\{2}{3}",
|
"시작: {0} → {1}\\{2}{3}": "Started: {0} → {1}\\{2}{3}",
|
||||||
|
"시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).": "Choose which syncs to start. The others stay paused for this session (start them in Monitor > lsync).",
|
||||||
"시작할 때 새 버전 확인 (하루 한 번)": "Check for a new version at startup (once a day)",
|
"시작할 때 새 버전 확인 (하루 한 번)": "Check for a new version at startup (once a day)",
|
||||||
"시작할 때마다 설정 창 보기": "Show the setup window at every start",
|
"시작할 때마다 설정 창 보기": "Show the setup window at every start",
|
||||||
"시즌": "Season",
|
"시즌": "Season",
|
||||||
"실시간 동기화 (lsyncd 처럼)": "Live sync (like lsyncd)",
|
"실시간 동기화 (lsyncd 처럼)": "Live sync (like lsyncd)",
|
||||||
"실시간 동기화 ({0}): {1} → {2}": "Live sync ({0}): {1} → {2}",
|
"실시간 동기화 ({0}): {1} → {2}": "Live sync ({0}): {1} → {2}",
|
||||||
|
"실시간 동기화 시작": "Start live sync",
|
||||||
"실시간 동기화 실패: {0} → {1}: {2}": "Live sync failed: {0} → {1}: {2}",
|
"실시간 동기화 실패: {0} → {1}: {2}": "Live sync failed: {0} → {1}: {2}",
|
||||||
"실시간 동기화 추가": "Add live sync",
|
"실시간 동기화 추가": "Add live sync",
|
||||||
"실시간 동기화: 대상 폴더": "Live sync: target folder",
|
"실시간 동기화: 대상 폴더": "Live sync: target folder",
|
||||||
|
|
@ -640,6 +657,7 @@
|
||||||
"앱 안 브라우저 (권장)": "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: 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)",
|
||||||
|
"앱을 다시 켤 때 동기화": "Sync when the app restarts",
|
||||||
"앱이 켜져 있는 동안 늘 동기화합니다.": "Always syncs while the app is running.",
|
"앱이 켜져 있는 동안 늘 동기화합니다.": "Always syncs while the app is running.",
|
||||||
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "Press anywhere to bring up the JJ_MKVMaker window",
|
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "Press anywhere to bring up the JJ_MKVMaker window",
|
||||||
"언어": "Language",
|
"언어": "Language",
|
||||||
|
|
@ -951,6 +969,7 @@
|
||||||
"창 보이기 / 숨기기 단축키": "Hotkey to show / hide the window",
|
"창 보이기 / 숨기기 단축키": "Hotkey to show / hide the window",
|
||||||
"창 숨김 → 프로세스 종료": "Window hidden → process exit",
|
"창 숨김 → 프로세스 종료": "Window hidden → process exit",
|
||||||
"창 종료": "Close window",
|
"창 종료": "Close window",
|
||||||
|
"창을 닫아도 (✕) 트레이에서 동기화 · MKV 만들기 · 다운로드를 계속합니다. 끄면 닫을 때 모두 끝냅니다. (실행 · 종료의 \"종료를 누르면\" 과 같은 설정)": "Closing the window (✕) keeps sync · MKV creation · downloads running from the tray. When off, closing ends everything. (Same setting as \"When closing\" under Run · Exit)",
|
||||||
"찾기": "Search",
|
"찾기": "Search",
|
||||||
"찾기: {0}": "Search: {0}",
|
"찾기: {0}": "Search: {0}",
|
||||||
"찾는 즐겨찾기가 없습니다.": "No matching bookmarks.",
|
"찾는 즐겨찾기가 없습니다.": "No matching bookmarks.",
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,7 @@
|
||||||
"… 외 {0}개": "… ほか {0} 件",
|
"… 외 {0}개": "… ほか {0} 件",
|
||||||
"※ 다른 작업 중이면 대기열에 넣고 차례대로 합니다. 번역하는 동안 브라우저를 써도 됩니다.": "※ 他の作業中ならキューに入れて順番に行います。翻訳中もブラウザーを使えます。",
|
"※ 다른 작업 중이면 대기열에 넣고 차례대로 합니다. 번역하는 동안 브라우저를 써도 됩니다.": "※ 他の作業中ならキューに入れて順番に行います。翻訳中もブラウザーを使えます。",
|
||||||
"※ 시간이 오래 걸립니다 (영상 1시간 ≈ 음성인식 15~30분 + 언어당 번역 10~20분)": "※ 時間がかかります(動画 1時間 ≈ 音声認識 15〜30分 + 言語ごとの翻訳 10〜20分)",
|
"※ 시간이 오래 걸립니다 (영상 1시간 ≈ 음성인식 15~30분 + 언어당 번역 10~20분)": "※ 時間がかかります(動画 1時間 ≈ 音声認識 15〜30分 + 言語ごとの翻訳 10〜20分)",
|
||||||
|
"← 로 닫거나 최근 앱 목록에서 밀어도 동기화 · MKV 만들기 · 다운로드를 계속합니다 (알림에 표시). 끄면 지금처럼 닫을 때 끝납니다. 완전히 끝내려면 [종료].": "← で閉じたり最近のアプリから払っても同期 · MKV 作成 · ダウンロードを続けます (通知に表示)。オフにすると今まで通り閉じたときに終了します。完全に終了するには [終了]。",
|
||||||
"− 차갑게 · + 따뜻하게": "− 寒色 · + 暖色",
|
"− 차갑게 · + 따뜻하게": "− 寒色 · + 暖色",
|
||||||
"⋮⋮ 를 끌어 순서 바꾸기 · 아이콘을 끌어 폴더에 넣기 · 오른쪽 클릭 메뉴": "⋮⋮ をドラッグして並べ替え・アイコンをドラッグしてフォルダーへ・右クリックメニュー",
|
"⋮⋮ 를 끌어 순서 바꾸기 · 아이콘을 끌어 폴더에 넣기 · 오른쪽 클릭 메뉴": "⋮⋮ をドラッグして並べ替え・アイコンをドラッグしてフォルダーへ・右クリックメニュー",
|
||||||
"── 시작 {0} (Android) ──": "── 開始 {0}(Android)──",
|
"── 시작 {0} (Android) ──": "── 開始 {0}(Android)──",
|
||||||
|
|
@ -250,9 +251,11 @@
|
||||||
"계속 편집": "編集を続ける",
|
"계속 편집": "編集を続ける",
|
||||||
"고르기": "選ぶ",
|
"고르기": "選ぶ",
|
||||||
"고르지 않음": "未選択",
|
"고르지 않음": "未選択",
|
||||||
|
"고른 것 시작": "選んだものを開始",
|
||||||
"고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "選んだサイズ以下で最も良い画質(動画にないサイズなら近いもの)",
|
"고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "選んだサイズ以下で最も良い画質(動画にないサイズなら近いもの)",
|
||||||
"고지 문서": "告知文書",
|
"고지 문서": "告知文書",
|
||||||
"고화질": "高画質",
|
"고화질": "高画質",
|
||||||
|
"골라서 시작": "選んで開始",
|
||||||
"관리 메뉴": "管理メニュー",
|
"관리 메뉴": "管理メニュー",
|
||||||
"광고 건너뛰는 중…": "広告をスキップ中…",
|
"광고 건너뛰는 중…": "広告をスキップ中…",
|
||||||
"권한 허용 화면 열기": "権限の許可画面を開く",
|
"권한 허용 화면 열기": "権限の許可画面を開く",
|
||||||
|
|
@ -343,6 +346,7 @@
|
||||||
"다시 만들기 대기로": "作り直し待ちに戻す",
|
"다시 만들기 대기로": "作り直し待ちに戻す",
|
||||||
"다시 세기": "再カウント",
|
"다시 세기": "再カウント",
|
||||||
"다시 시작": "再起動",
|
"다시 시작": "再起動",
|
||||||
|
"다시 켤 때 동기화": "再起動時の同期",
|
||||||
"다운로드": "ダウンロード",
|
"다운로드": "ダウンロード",
|
||||||
"다운로드 (jj_yt-dlp · jj_aria2)": "ダウンロード(jj_yt-dlp・jj_aria2)",
|
"다운로드 (jj_yt-dlp · jj_aria2)": "ダウンロード(jj_yt-dlp・jj_aria2)",
|
||||||
"다운로드 ({0})": "ダウンロード({0})",
|
"다운로드 ({0})": "ダウンロード({0})",
|
||||||
|
|
@ -386,6 +390,8 @@
|
||||||
"덴마크어": "デンマーク語",
|
"덴마크어": "デンマーク語",
|
||||||
"독일어": "ドイツ語",
|
"독일어": "ドイツ語",
|
||||||
"돌아가기": "戻る",
|
"돌아가기": "戻る",
|
||||||
|
"동기화 {0}개 · 맞추는 중": "同期 {0} 件 · 同期中",
|
||||||
|
"동기화 {0}개 지켜보는 중": "同期 {0} 件を監視中",
|
||||||
"동기화 시간": "同期時間",
|
"동기화 시간": "同期時間",
|
||||||
"동기화 시간 (계속 / 시간 지정)": "同期時間 (常時 / 時間指定)",
|
"동기화 시간 (계속 / 시간 지정)": "同期時間 (常時 / 時間指定)",
|
||||||
"동시 MKV 변환 수": "同時 MKV 変換数",
|
"동시 MKV 변환 수": "同時 MKV 変換数",
|
||||||
|
|
@ -434,14 +440,19 @@
|
||||||
"매번 고르기": "毎回選ぶ",
|
"매번 고르기": "毎回選ぶ",
|
||||||
"매일 종일": "毎日 終日",
|
"매일 종일": "毎日 終日",
|
||||||
"멈춤": "停止",
|
"멈춤": "停止",
|
||||||
|
"멈춤 (이번 실행 동안)": "停止 (今回の実行中)",
|
||||||
|
"멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "停止中 (今回の実行中) · 開始ボタンで再び同期します",
|
||||||
"멈춤 · {0}": "停止 · {0}",
|
"멈춤 · {0}": "停止 · {0}",
|
||||||
"모니터링": "モニタリング",
|
"모니터링": "モニタリング",
|
||||||
"모델 내려받는 중 {0}%": "モデルをダウンロード中 {0}%",
|
"모델 내려받는 중 {0}%": "モデルをダウンロード中 {0}%",
|
||||||
"모델 내려받는 중: {0} ({1})": "モデルをダウンロード中: {0}({1})",
|
"모델 내려받는 중: {0} ({1})": "モデルをダウンロード中: {0}({1})",
|
||||||
"모델 파일이 잘렸습니다 (위치 {0})": "モデルファイルが途中で切れています(位置 {0})",
|
"모델 파일이 잘렸습니다 (위치 {0})": "モデルファイルが途中で切れています(位置 {0})",
|
||||||
"모델을 내려받을 수 없습니다 ({0}): {1}": "モデルをダウンロードできません({0}): {1}",
|
"모델을 내려받을 수 없습니다 ({0}): {1}": "モデルをダウンロードできません({0}): {1}",
|
||||||
|
"모두 나중에": "すべて後で",
|
||||||
"모두 내장 플레이어": "すべて内蔵プレーヤー",
|
"모두 내장 플레이어": "すべて内蔵プレーヤー",
|
||||||
|
"모두 멈춤": "すべて停止",
|
||||||
"모두 선택": "すべて選択",
|
"모두 선택": "すべて選択",
|
||||||
|
"모두 시작 (멈춘 것 {0}개)": "すべて開始 (停止中 {0} 件)",
|
||||||
"모두 종료": "すべて終了",
|
"모두 종료": "すべて終了",
|
||||||
"모두 지우기": "すべて削除",
|
"모두 지우기": "すべて削除",
|
||||||
"모두 초기화": "すべてリセット",
|
"모두 초기화": "すべてリセット",
|
||||||
|
|
@ -463,6 +474,8 @@
|
||||||
"바꾼 것이 없습니다 (원본 그대로)": "変更なし(元のまま)",
|
"바꾼 것이 없습니다 (원본 그대로)": "変更なし(元のまま)",
|
||||||
"바뀐 날": "更新日時",
|
"바뀐 날": "更新日時",
|
||||||
"바뀐 내용": "変更内容",
|
"바뀐 내용": "変更内容",
|
||||||
|
"바로 시작 (기본)": "すぐ開始 (既定)",
|
||||||
|
"바로 시작: 켜진 동기화를 모두 시작 · 골라서 시작: 켤 때 고르기 · 시작 안 함: 모니터링 > lsync 의 시작 버튼으로": "すぐ開始: オンの同期をすべて開始 · 選んで開始: 起動時に選ぶ · 開始しない: モニタリング > lsync の開始ボタンで",
|
||||||
"바로 재생 (기본)": "すぐ再生(既定)",
|
"바로 재생 (기본)": "すぐ再生(既定)",
|
||||||
"바로 전 작업: {0}": "直前の作業: {0}",
|
"바로 전 작업: {0}": "直前の作業: {0}",
|
||||||
"받는 중": "ダウンロード中",
|
"받는 중": "ダウンロード中",
|
||||||
|
|
@ -484,6 +497,7 @@
|
||||||
"백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)": "バックグラウンド: ウィンドウだけ隠してダウンロード・変換は続けます。{0} またはトレイアイコンで再表示します。\n完全に終了するには トレイアイコンを右クリック > 終了(どの設定でも常に終了)",
|
"백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)": "バックグラウンド: ウィンドウだけ隠してダウンロード・変換は続けます。{0} またはトレイアイコンで再表示します。\n完全に終了するには トレイアイコンを右クリック > 終了(どの設定でも常に終了)",
|
||||||
"백그라운드로": "バックグラウンドへ",
|
"백그라운드로": "バックグラウンドへ",
|
||||||
"백그라운드로 계속 (기본)": "バックグラウンドで続ける(既定)",
|
"백그라운드로 계속 (기본)": "バックグラウンドで続ける(既定)",
|
||||||
|
"백그라운드로 실행": "バックグラウンドで実行",
|
||||||
"버튼 구성": "ボタン構成",
|
"버튼 구성": "ボタン構成",
|
||||||
"번역 모델 내려받는 중 {0}%": "翻訳モデルをダウンロード中 {0}%",
|
"번역 모델 내려받는 중 {0}%": "翻訳モデルをダウンロード中 {0}%",
|
||||||
"번역 모델 내려받는 중: {0}": "翻訳モデルをダウンロード中: {0}",
|
"번역 모델 내려받는 중: {0}": "翻訳モデルをダウンロード中: {0}",
|
||||||
|
|
@ -599,12 +613,15 @@
|
||||||
"시작": "開始",
|
"시작": "開始",
|
||||||
"시작 = 현재": "開始 = 現在",
|
"시작 = 현재": "開始 = 現在",
|
||||||
"시작 · 웹 브라우저": "起動・ウェブブラウザー",
|
"시작 · 웹 브라우저": "起動・ウェブブラウザー",
|
||||||
|
"시작 안 함 (모니터링에서 시작)": "開始しない (モニタリングで開始)",
|
||||||
"시작: {0} → {1}\\{2}{3}": "開始: {0} → {1}\\{2}{3}",
|
"시작: {0} → {1}\\{2}{3}": "開始: {0} → {1}\\{2}{3}",
|
||||||
|
"시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).": "開始する同期を選んでください。選ばなかったものは今回の実行中は停止します (モニタリング > lsync で開始)。",
|
||||||
"시작할 때 새 버전 확인 (하루 한 번)": "起動時に新しいバージョンを確認(1日1回)",
|
"시작할 때 새 버전 확인 (하루 한 번)": "起動時に新しいバージョンを確認(1日1回)",
|
||||||
"시작할 때마다 설정 창 보기": "起動するたびに設定ウィンドウを表示",
|
"시작할 때마다 설정 창 보기": "起動するたびに設定ウィンドウを表示",
|
||||||
"시즌": "シーズン",
|
"시즌": "シーズン",
|
||||||
"실시간 동기화 (lsyncd 처럼)": "リアルタイム同期 (lsyncd のように)",
|
"실시간 동기화 (lsyncd 처럼)": "リアルタイム同期 (lsyncd のように)",
|
||||||
"실시간 동기화 ({0}): {1} → {2}": "リアルタイム同期 ({0}): {1} → {2}",
|
"실시간 동기화 ({0}): {1} → {2}": "リアルタイム同期 ({0}): {1} → {2}",
|
||||||
|
"실시간 동기화 시작": "リアルタイム同期の開始",
|
||||||
"실시간 동기화 실패: {0} → {1}: {2}": "リアルタイム同期に失敗: {0} → {1}: {2}",
|
"실시간 동기화 실패: {0} → {1}: {2}": "リアルタイム同期に失敗: {0} → {1}: {2}",
|
||||||
"실시간 동기화 추가": "リアルタイム同期を追加",
|
"실시간 동기화 추가": "リアルタイム同期を追加",
|
||||||
"실시간 동기화: 대상 폴더": "リアルタイム同期: 対象フォルダー",
|
"실시간 동기화: 대상 폴더": "リアルタイム同期: 対象フォルダー",
|
||||||
|
|
@ -640,6 +657,7 @@
|
||||||
"앱 안 브라우저 (권장)": "アプリ内ブラウザー(推奨)",
|
"앱 안 브라우저 (권장)": "アプリ内ブラウザー(推奨)",
|
||||||
"앱 안 브라우저의 로그인 사용 (브라우저 화면에서 YouTube 에 로그인하세요)": "アプリ内ブラウザーのログインを使う(ブラウザー画面で YouTube にログインしてください)",
|
"앱 안 브라우저의 로그인 사용 (브라우저 화면에서 YouTube 에 로그인하세요)": "アプリ内ブラウザーのログインを使う(ブラウザー画面で YouTube にログインしてください)",
|
||||||
"앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 (넘기는 동안 화면 위쪽에 \"광고 건너뛰는 중…\")": "アプリ内ウェブブラウザー:[スキップ]を代わりに押し、スキップできない広告は音を消して早送りします(スキップ中は画面上部に「広告をスキップ中…」)",
|
"앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 (넘기는 동안 화면 위쪽에 \"광고 건너뛰는 중…\")": "アプリ内ウェブブラウザー:[スキップ]を代わりに押し、スキップできない広告は音を消して早送りします(スキップ中は画面上部に「広告をスキップ中…」)",
|
||||||
|
"앱을 다시 켤 때 동기화": "アプリ再起動時の同期",
|
||||||
"앱이 켜져 있는 동안 늘 동기화합니다.": "アプリの起動中は常に同期します。",
|
"앱이 켜져 있는 동안 늘 동기화합니다.": "アプリの起動中は常に同期します。",
|
||||||
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "どこからでも押すと JJ_MKVMaker ウィンドウが表示されます",
|
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "どこからでも押すと JJ_MKVMaker ウィンドウが表示されます",
|
||||||
"언어": "言語",
|
"언어": "言語",
|
||||||
|
|
@ -951,6 +969,7 @@
|
||||||
"창 보이기 / 숨기기 단축키": "ウィンドウ表示 / 非表示のショートカット",
|
"창 보이기 / 숨기기 단축키": "ウィンドウ表示 / 非表示のショートカット",
|
||||||
"창 숨김 → 프로세스 종료": "ウィンドウ非表示 → プロセス終了",
|
"창 숨김 → 프로세스 종료": "ウィンドウ非表示 → プロセス終了",
|
||||||
"창 종료": "ウィンドウを閉じる",
|
"창 종료": "ウィンドウを閉じる",
|
||||||
|
"창을 닫아도 (✕) 트레이에서 동기화 · MKV 만들기 · 다운로드를 계속합니다. 끄면 닫을 때 모두 끝냅니다. (실행 · 종료의 \"종료를 누르면\" 과 같은 설정)": "ウィンドウを閉じても (✕) トレイで同期 · MKV 作成 · ダウンロードを続けます。オフにすると閉じたときにすべて終了します。(実行 · 終了の「終了を押したら」と同じ設定)",
|
||||||
"찾기": "検索",
|
"찾기": "検索",
|
||||||
"찾기: {0}": "検索: {0}",
|
"찾기: {0}": "検索: {0}",
|
||||||
"찾는 즐겨찾기가 없습니다.": "該当するブックマークがありません。",
|
"찾는 즐겨찾기가 없습니다.": "該当するブックマークがありません。",
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,7 @@
|
||||||
"… 외 {0}개": "… 另外 {0} 个",
|
"… 외 {0}개": "… 另外 {0} 个",
|
||||||
"※ 다른 작업 중이면 대기열에 넣고 차례대로 합니다. 번역하는 동안 브라우저를 써도 됩니다.": "※ 如有其他任务正在进行,将加入队列依次处理。翻译期间可以使用浏览器。",
|
"※ 다른 작업 중이면 대기열에 넣고 차례대로 합니다. 번역하는 동안 브라우저를 써도 됩니다.": "※ 如有其他任务正在进行,将加入队列依次处理。翻译期间可以使用浏览器。",
|
||||||
"※ 시간이 오래 걸립니다 (영상 1시간 ≈ 음성인식 15~30분 + 언어당 번역 10~20분)": "※ 耗时较长(1 小时视频 ≈ 语音识别 15~30 分钟 + 每种语言翻译 10~20 分钟)",
|
"※ 시간이 오래 걸립니다 (영상 1시간 ≈ 음성인식 15~30분 + 언어당 번역 10~20분)": "※ 耗时较长(1 小时视频 ≈ 语音识别 15~30 分钟 + 每种语言翻译 10~20 分钟)",
|
||||||
|
"← 로 닫거나 최근 앱 목록에서 밀어도 동기화 · MKV 만들기 · 다운로드를 계속합니다 (알림에 표시). 끄면 지금처럼 닫을 때 끝납니다. 완전히 끝내려면 [종료].": "用 ← 关闭或从最近应用中划掉后仍继续同步 · 制作 MKV · 下载 (在通知中显示)。关闭此项则像以前一样关闭时结束。要完全退出请按 [退出]。",
|
||||||
"− 차갑게 · + 따뜻하게": "− 偏冷 · + 偏暖",
|
"− 차갑게 · + 따뜻하게": "− 偏冷 · + 偏暖",
|
||||||
"⋮⋮ 를 끌어 순서 바꾸기 · 아이콘을 끌어 폴더에 넣기 · 오른쪽 클릭 메뉴": "拖动 ⋮⋮ 调整顺序 · 拖动图标放入文件夹 · 右键菜单",
|
"⋮⋮ 를 끌어 순서 바꾸기 · 아이콘을 끌어 폴더에 넣기 · 오른쪽 클릭 메뉴": "拖动 ⋮⋮ 调整顺序 · 拖动图标放入文件夹 · 右键菜单",
|
||||||
"── 시작 {0} (Android) ──": "── 启动 {0}(Android)──",
|
"── 시작 {0} (Android) ──": "── 启动 {0}(Android)──",
|
||||||
|
|
@ -250,9 +251,11 @@
|
||||||
"계속 편집": "继续编辑",
|
"계속 편집": "继续编辑",
|
||||||
"고르기": "选择",
|
"고르기": "选择",
|
||||||
"고르지 않음": "未选择",
|
"고르지 않음": "未选择",
|
||||||
|
"고른 것 시작": "开始所选",
|
||||||
"고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "不超过所选尺寸的最佳画质(视频没有该尺寸时取最接近的)",
|
"고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "不超过所选尺寸的最佳画质(视频没有该尺寸时取最接近的)",
|
||||||
"고지 문서": "声明文件",
|
"고지 문서": "声明文件",
|
||||||
"고화질": "高画质",
|
"고화질": "高画质",
|
||||||
|
"골라서 시작": "选择后开始",
|
||||||
"관리 메뉴": "管理菜单",
|
"관리 메뉴": "管理菜单",
|
||||||
"광고 건너뛰는 중…": "正在跳过广告…",
|
"광고 건너뛰는 중…": "正在跳过广告…",
|
||||||
"권한 허용 화면 열기": "打开权限授予界面",
|
"권한 허용 화면 열기": "打开权限授予界面",
|
||||||
|
|
@ -343,6 +346,7 @@
|
||||||
"다시 만들기 대기로": "改回等待重新制作",
|
"다시 만들기 대기로": "改回等待重新制作",
|
||||||
"다시 세기": "重新统计",
|
"다시 세기": "重新统计",
|
||||||
"다시 시작": "重新启动",
|
"다시 시작": "重新启动",
|
||||||
|
"다시 켤 때 동기화": "重新启动时同步",
|
||||||
"다운로드": "下载",
|
"다운로드": "下载",
|
||||||
"다운로드 (jj_yt-dlp · jj_aria2)": "下载(jj_yt-dlp · jj_aria2)",
|
"다운로드 (jj_yt-dlp · jj_aria2)": "下载(jj_yt-dlp · jj_aria2)",
|
||||||
"다운로드 ({0})": "下载({0})",
|
"다운로드 ({0})": "下载({0})",
|
||||||
|
|
@ -386,6 +390,8 @@
|
||||||
"덴마크어": "丹麦语",
|
"덴마크어": "丹麦语",
|
||||||
"독일어": "德语",
|
"독일어": "德语",
|
||||||
"돌아가기": "返回",
|
"돌아가기": "返回",
|
||||||
|
"동기화 {0}개 · 맞추는 중": "同步 {0} 个 · 同步中",
|
||||||
|
"동기화 {0}개 지켜보는 중": "正在监视 {0} 个同步",
|
||||||
"동기화 시간": "同步时间",
|
"동기화 시간": "同步时间",
|
||||||
"동기화 시간 (계속 / 시간 지정)": "同步时间 (持续 / 指定时间)",
|
"동기화 시간 (계속 / 시간 지정)": "同步时间 (持续 / 指定时间)",
|
||||||
"동시 MKV 변환 수": "同时转换 MKV 数",
|
"동시 MKV 변환 수": "同时转换 MKV 数",
|
||||||
|
|
@ -434,14 +440,19 @@
|
||||||
"매번 고르기": "每次选择",
|
"매번 고르기": "每次选择",
|
||||||
"매일 종일": "每天 全天",
|
"매일 종일": "每天 全天",
|
||||||
"멈춤": "已停止",
|
"멈춤": "已停止",
|
||||||
|
"멈춤 (이번 실행 동안)": "暂停 (本次运行期间)",
|
||||||
|
"멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "已暂停 (本次运行期间) · 按开始按钮重新同步",
|
||||||
"멈춤 · {0}": "已停止 · {0}",
|
"멈춤 · {0}": "已停止 · {0}",
|
||||||
"모니터링": "监控",
|
"모니터링": "监控",
|
||||||
"모델 내려받는 중 {0}%": "正在下载模型 {0}%",
|
"모델 내려받는 중 {0}%": "正在下载模型 {0}%",
|
||||||
"모델 내려받는 중: {0} ({1})": "正在下载模型: {0}({1})",
|
"모델 내려받는 중: {0} ({1})": "正在下载模型: {0}({1})",
|
||||||
"모델 파일이 잘렸습니다 (위치 {0})": "模型文件被截断(位置 {0})",
|
"모델 파일이 잘렸습니다 (위치 {0})": "模型文件被截断(位置 {0})",
|
||||||
"모델을 내려받을 수 없습니다 ({0}): {1}": "无法下载模型({0}): {1}",
|
"모델을 내려받을 수 없습니다 ({0}): {1}": "无法下载模型({0}): {1}",
|
||||||
|
"모두 나중에": "全部稍后",
|
||||||
"모두 내장 플레이어": "全部使用内置播放器",
|
"모두 내장 플레이어": "全部使用内置播放器",
|
||||||
|
"모두 멈춤": "全部暂停",
|
||||||
"모두 선택": "全选",
|
"모두 선택": "全选",
|
||||||
|
"모두 시작 (멈춘 것 {0}개)": "全部开始 (已暂停 {0} 个)",
|
||||||
"모두 종료": "全部退出",
|
"모두 종료": "全部退出",
|
||||||
"모두 지우기": "全部清除",
|
"모두 지우기": "全部清除",
|
||||||
"모두 초기화": "全部重置",
|
"모두 초기화": "全部重置",
|
||||||
|
|
@ -463,6 +474,8 @@
|
||||||
"바꾼 것이 없습니다 (원본 그대로)": "未做更改(保持原样)",
|
"바꾼 것이 없습니다 (원본 그대로)": "未做更改(保持原样)",
|
||||||
"바뀐 날": "修改时间",
|
"바뀐 날": "修改时间",
|
||||||
"바뀐 내용": "更改内容",
|
"바뀐 내용": "更改内容",
|
||||||
|
"바로 시작 (기본)": "立即开始 (默认)",
|
||||||
|
"바로 시작: 켜진 동기화를 모두 시작 · 골라서 시작: 켤 때 고르기 · 시작 안 함: 모니터링 > lsync 의 시작 버튼으로": "立即开始: 开始所有已启用的同步 · 选择后开始: 启动时选择 · 不开始: 用 监控 > lsync 的开始按钮",
|
||||||
"바로 재생 (기본)": "立即播放(默认)",
|
"바로 재생 (기본)": "立即播放(默认)",
|
||||||
"바로 전 작업: {0}": "上一个任务: {0}",
|
"바로 전 작업: {0}": "上一个任务: {0}",
|
||||||
"받는 중": "正在下载",
|
"받는 중": "正在下载",
|
||||||
|
|
@ -484,6 +497,7 @@
|
||||||
"백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)": "后台: 仅隐藏窗口,下载 · 转换继续进行。用 {0} 或托盘图标重新打开。\n要完全退出: 右键托盘图标 > 退出(无论何种设置都会退出)",
|
"백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)": "后台: 仅隐藏窗口,下载 · 转换继续进行。用 {0} 或托盘图标重新打开。\n要完全退出: 右键托盘图标 > 退出(无论何种设置都会退出)",
|
||||||
"백그라운드로": "转到后台",
|
"백그라운드로": "转到后台",
|
||||||
"백그라운드로 계속 (기본)": "在后台继续(默认)",
|
"백그라운드로 계속 (기본)": "在后台继续(默认)",
|
||||||
|
"백그라운드로 실행": "在后台运行",
|
||||||
"버튼 구성": "按钮配置",
|
"버튼 구성": "按钮配置",
|
||||||
"번역 모델 내려받는 중 {0}%": "正在下载翻译模型 {0}%",
|
"번역 모델 내려받는 중 {0}%": "正在下载翻译模型 {0}%",
|
||||||
"번역 모델 내려받는 중: {0}": "正在下载翻译模型: {0}",
|
"번역 모델 내려받는 중: {0}": "正在下载翻译模型: {0}",
|
||||||
|
|
@ -599,12 +613,15 @@
|
||||||
"시작": "开始",
|
"시작": "开始",
|
||||||
"시작 = 현재": "开始 = 当前",
|
"시작 = 현재": "开始 = 当前",
|
||||||
"시작 · 웹 브라우저": "启动 · 网页浏览器",
|
"시작 · 웹 브라우저": "启动 · 网页浏览器",
|
||||||
|
"시작 안 함 (모니터링에서 시작)": "不开始 (在监控中开始)",
|
||||||
"시작: {0} → {1}\\{2}{3}": "开始: {0} → {1}\\{2}{3}",
|
"시작: {0} → {1}\\{2}{3}": "开始: {0} → {1}\\{2}{3}",
|
||||||
|
"시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).": "请选择要开始的同步。未选的在本次运行期间暂停 (可在 监控 > lsync 中开始)。",
|
||||||
"시작할 때 새 버전 확인 (하루 한 번)": "启动时检查新版本(每天一次)",
|
"시작할 때 새 버전 확인 (하루 한 번)": "启动时检查新版本(每天一次)",
|
||||||
"시작할 때마다 설정 창 보기": "每次启动时显示设置窗口",
|
"시작할 때마다 설정 창 보기": "每次启动时显示设置窗口",
|
||||||
"시즌": "季",
|
"시즌": "季",
|
||||||
"실시간 동기화 (lsyncd 처럼)": "实时同步 (类似 lsyncd)",
|
"실시간 동기화 (lsyncd 처럼)": "实时同步 (类似 lsyncd)",
|
||||||
"실시간 동기화 ({0}): {1} → {2}": "实时同步 ({0}): {1} → {2}",
|
"실시간 동기화 ({0}): {1} → {2}": "实时同步 ({0}): {1} → {2}",
|
||||||
|
"실시간 동기화 시작": "开始实时同步",
|
||||||
"실시간 동기화 실패: {0} → {1}: {2}": "实时同步失败: {0} → {1}: {2}",
|
"실시간 동기화 실패: {0} → {1}: {2}": "实时同步失败: {0} → {1}: {2}",
|
||||||
"실시간 동기화 추가": "添加实时同步",
|
"실시간 동기화 추가": "添加实时同步",
|
||||||
"실시간 동기화: 대상 폴더": "实时同步: 目标文件夹",
|
"실시간 동기화: 대상 폴더": "实时同步: 目标文件夹",
|
||||||
|
|
@ -640,6 +657,7 @@
|
||||||
"앱 안 브라우저 (권장)": "应用内浏览器(推荐)",
|
"앱 안 브라우저 (권장)": "应用内浏览器(推荐)",
|
||||||
"앱 안 브라우저의 로그인 사용 (브라우저 화면에서 YouTube 에 로그인하세요)": "使用应用内浏览器的登录(请在浏览器界面登录 YouTube)",
|
"앱 안 브라우저의 로그인 사용 (브라우저 화면에서 YouTube 에 로그인하세요)": "使用应用内浏览器的登录(请在浏览器界面登录 YouTube)",
|
||||||
"앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 (넘기는 동안 화면 위쪽에 \"광고 건너뛰는 중…\")": "应用内网页浏览器: 代为点击[跳过],无法跳过的广告会静音并快进(跳过时屏幕顶部显示“正在跳过广告…”)",
|
"앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 (넘기는 동안 화면 위쪽에 \"광고 건너뛰는 중…\")": "应用内网页浏览器: 代为点击[跳过],无法跳过的广告会静音并快进(跳过时屏幕顶部显示“正在跳过广告…”)",
|
||||||
|
"앱을 다시 켤 때 동기화": "应用重新启动时同步",
|
||||||
"앱이 켜져 있는 동안 늘 동기화합니다.": "应用运行期间始终同步。",
|
"앱이 켜져 있는 동안 늘 동기화합니다.": "应用运行期间始终同步。",
|
||||||
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "在任何地方按下即可显示 JJ_MKVMaker 窗口",
|
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "在任何地方按下即可显示 JJ_MKVMaker 窗口",
|
||||||
"언어": "语言",
|
"언어": "语言",
|
||||||
|
|
@ -951,6 +969,7 @@
|
||||||
"창 보이기 / 숨기기 단축키": "显示 / 隐藏窗口的快捷键",
|
"창 보이기 / 숨기기 단축키": "显示 / 隐藏窗口的快捷键",
|
||||||
"창 숨김 → 프로세스 종료": "窗口隐藏 → 进程退出",
|
"창 숨김 → 프로세스 종료": "窗口隐藏 → 进程退出",
|
||||||
"창 종료": "关闭窗口",
|
"창 종료": "关闭窗口",
|
||||||
|
"창을 닫아도 (✕) 트레이에서 동기화 · MKV 만들기 · 다운로드를 계속합니다. 끄면 닫을 때 모두 끝냅니다. (실행 · 종료의 \"종료를 누르면\" 과 같은 설정)": "关闭窗口 (✕) 后在托盘中继续同步 · 制作 MKV · 下载。关闭此项则关闭时全部结束。(与 运行 · 退出 中的“按退出时”为同一设置)",
|
||||||
"찾기": "搜索",
|
"찾기": "搜索",
|
||||||
"찾기: {0}": "搜索: {0}",
|
"찾기: {0}": "搜索: {0}",
|
||||||
"찾는 즐겨찾기가 없습니다.": "没有匹配的书签。",
|
"찾는 즐겨찾기가 없습니다.": "没有匹配的书签。",
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,13 @@ class LiveSync extends ChangeNotifier {
|
||||||
/// 1분마다: 동작 시간 (cron) 이 시작되면 밀린 것을 맞춘다
|
/// 1분마다: 동작 시간 (cron) 이 시작되면 밀린 것을 맞춘다
|
||||||
Timer? _clock;
|
Timer? _clock;
|
||||||
|
|
||||||
void start() {
|
/// 이번 실행 동안 멈춘 쌍 (설정의 켜기 · 끄기와 따로, 앱을 다시 켜면 [AppSettings.liveSyncOnStart] 를 따른다)
|
||||||
|
final paused = <String>{};
|
||||||
|
|
||||||
|
/// [hold] 면 켜진 쌍을 모두 멈춘 채로 시작 (모니터링 · [resume] 으로 시작)
|
||||||
|
void start({bool hold = false}) {
|
||||||
instance = this;
|
instance = this;
|
||||||
|
if (hold) paused.addAll(c.settings.liveSyncPairs.where((x) => x.enabled).map(keyOf));
|
||||||
c.addListener(_apply);
|
c.addListener(_apply);
|
||||||
_apply();
|
_apply();
|
||||||
_clock ??= Timer.periodic(const Duration(minutes: 1), (_) => _onClock());
|
_clock ??= Timer.periodic(const Duration(minutes: 1), (_) => _onClock());
|
||||||
|
|
@ -76,8 +81,35 @@ class LiveSync extends ChangeNotifier {
|
||||||
/// 지금 동작 시간인지 (일정이 없으면 늘)
|
/// 지금 동작 시간인지 (일정이 없으면 늘)
|
||||||
static bool activeNow(LiveSyncPair x, [DateTime? now]) => scheduleActive(x.schedule, now ?? DateTime.now());
|
static bool activeNow(LiveSyncPair x, [DateTime? now]) => scheduleActive(x.schedule, now ?? DateTime.now());
|
||||||
|
|
||||||
|
bool isPaused(LiveSyncPair x) => paused.contains(keyOf(x));
|
||||||
|
|
||||||
|
/// 지금 지켜보는 쌍 (켜져 있고 멈추지 않은 것)
|
||||||
|
List<LiveSyncPair> get watching => [
|
||||||
|
for (final x in c.settings.liveSyncPairs)
|
||||||
|
if (x.enabled && !paused.contains(keyOf(x))) x,
|
||||||
|
];
|
||||||
|
|
||||||
|
void pause(LiveSyncPair x) => _setPaused({...paused, keyOf(x)});
|
||||||
|
void resume(LiveSyncPair x) => _setPaused({...paused}..remove(keyOf(x)));
|
||||||
|
void pauseAll() => _setPaused({...paused, ...c.settings.liveSyncPairs.map(keyOf)});
|
||||||
|
void resumeAll() => _setPaused({});
|
||||||
|
|
||||||
|
/// 시작할 때 고른 쌍만 (나머지는 멈춤)
|
||||||
|
void runOnly(Iterable<LiveSyncPair> xs) {
|
||||||
|
final keep = xs.map(keyOf).toSet();
|
||||||
|
_setPaused({for (final x in c.settings.liveSyncPairs) if (!keep.contains(keyOf(x))) keyOf(x)});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setPaused(Set<String> v) {
|
||||||
|
paused
|
||||||
|
..clear()
|
||||||
|
..addAll(v);
|
||||||
|
_apply();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void _onClock() {
|
void _onClock() {
|
||||||
for (final x in c.settings.liveSyncPairs.where((x) => x.enabled)) {
|
for (final x in watching) {
|
||||||
if (activeNow(x) && (pending[keyOf(x)]?.isNotEmpty ?? false)) _schedule(x);
|
if (activeNow(x) && (pending[keyOf(x)]?.isNotEmpty ?? false)) _schedule(x);
|
||||||
}
|
}
|
||||||
notifyListeners(); // 남은 시간 표시
|
notifyListeners(); // 남은 시간 표시
|
||||||
|
|
@ -144,7 +176,7 @@ class LiveSync extends ChangeNotifier {
|
||||||
/// 설정이 바뀌면 감시를 다시 꾸린다
|
/// 설정이 바뀌면 감시를 다시 꾸린다
|
||||||
void _apply() {
|
void _apply() {
|
||||||
final s = c.settings;
|
final s = c.settings;
|
||||||
final pairs = s.liveSyncPairs.where((x) => x.enabled).toList();
|
final pairs = watching;
|
||||||
final sig = jsonEncode([for (final x in pairs) x.toJson(), s.liveSyncIntervalSec]);
|
final sig = jsonEncode([for (final x in pairs) x.toJson(), s.liveSyncIntervalSec]);
|
||||||
if (sig == _sig) return;
|
if (sig == _sig) return;
|
||||||
_sig = sig;
|
_sig = sig;
|
||||||
|
|
@ -187,6 +219,9 @@ class LiveSync extends ChangeNotifier {
|
||||||
|
|
||||||
bool isRunning(LiveSyncPair x) => _running.contains(keyOf(x));
|
bool isRunning(LiveSyncPair x) => _running.contains(keyOf(x));
|
||||||
|
|
||||||
|
/// 지금 맞추는 쌍이 있는지
|
||||||
|
bool get anyRunning => _running.isNotEmpty;
|
||||||
|
|
||||||
/// 지금 맞추기 (원본 내용 → 대상)
|
/// 지금 맞추기 (원본 내용 → 대상)
|
||||||
Future<void> syncNow(LiveSyncPair x) async {
|
Future<void> syncNow(LiveSyncPair x) async {
|
||||||
final k = keyOf(x);
|
final k = keyOf(x);
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,23 @@ class AppSettings {
|
||||||
/// 실시간 동기화 확인 간격 (초). Windows 는 바뀌면 바로, 그 밖은 이 간격으로 살핀다.
|
/// 실시간 동기화 확인 간격 (초). Windows 는 바뀌면 바로, 그 밖은 이 간격으로 살핀다.
|
||||||
int liveSyncIntervalSec = 30;
|
int liveSyncIntervalSec = 30;
|
||||||
|
|
||||||
|
/// 백그라운드로 실행 (Android): 켜면 ← 로 닫거나 최근 앱에서 밀어도 동기화 · MKV 만들기 · 다운로드를 계속한다 (알림에 표시).
|
||||||
|
/// 끄면 지금처럼 닫을 때 끝난다. Windows 는 [closeAction] 'background' 가 같은 뜻 ([backgroundRun]).
|
||||||
|
bool runInBackground = false;
|
||||||
|
|
||||||
|
/// 앱을 다시 켤 때 실시간 동기화: 'auto' 바로 시작 / 'ask' 골라서 시작 / 'off' 시작 안 함 (모니터링에서 시작)
|
||||||
|
String liveSyncOnStart = 'auto';
|
||||||
|
|
||||||
|
/// 백그라운드로 실행 (설정 화면의 체크 하나). Windows: 창 ✕ 를 누르면 트레이로 (끄면 종료), Android: [runInBackground]
|
||||||
|
bool get backgroundRun => Platform.isWindows ? closeAction == 'background' : runInBackground;
|
||||||
|
set backgroundRun(bool v) {
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
closeAction = v ? 'background' : 'quit';
|
||||||
|
} else {
|
||||||
|
runInBackground = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 마지막으로 연 폴더 (왼쪽 · 오른쪽 창)
|
/// 마지막으로 연 폴더 (왼쪽 · 오른쪽 창)
|
||||||
List<String> explorerPaths = [];
|
List<String> explorerPaths = [];
|
||||||
|
|
||||||
|
|
@ -414,6 +431,8 @@ class AppSettings {
|
||||||
'rsyncPath': rsyncPath,
|
'rsyncPath': rsyncPath,
|
||||||
'liveSyncPairs': [for (final x in liveSyncPairs) x.toJson()],
|
'liveSyncPairs': [for (final x in liveSyncPairs) x.toJson()],
|
||||||
'liveSyncIntervalSec': liveSyncIntervalSec,
|
'liveSyncIntervalSec': liveSyncIntervalSec,
|
||||||
|
'runInBackground': runInBackground,
|
||||||
|
'liveSyncOnStart': liveSyncOnStart,
|
||||||
'copyMonitor': copyMonitor,
|
'copyMonitor': copyMonitor,
|
||||||
'copyTasks': [for (final x in copyTasks) x.toJson()],
|
'copyTasks': [for (final x in copyTasks) x.toJson()],
|
||||||
'explorerHistory': explorerHistory,
|
'explorerHistory': explorerHistory,
|
||||||
|
|
@ -519,6 +538,8 @@ class AppSettings {
|
||||||
]
|
]
|
||||||
..liveSyncIntervalSec = ((j['liveSyncIntervalSec'] as num?)?.toInt() ?? 30).clamp(5, 3600)
|
..liveSyncIntervalSec = ((j['liveSyncIntervalSec'] as num?)?.toInt() ?? 30).clamp(5, 3600)
|
||||||
..copyMonitor = j['copyMonitor'] == true
|
..copyMonitor = j['copyMonitor'] == true
|
||||||
|
..runInBackground = j['runInBackground'] == true
|
||||||
|
..liveSyncOnStart = const ['auto', 'ask', 'off'].contains(j['liveSyncOnStart']) ? j['liveSyncOnStart'] as String : 'auto'
|
||||||
..copyTasks = [
|
..copyTasks = [
|
||||||
for (final x in (j['copyTasks'] as List?) ?? const [])
|
for (final x in (j['copyTasks'] as List?) ?? const [])
|
||||||
if (x is Map && x['dest'] is String) CopyTask.fromJson(x),
|
if (x is Map && x['dest'] is String) CopyTask.fromJson(x),
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import 'ui/downloads_page.dart';
|
||||||
import 'ui/explorer_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/player_page.dart';
|
import 'ui/player_page.dart';
|
||||||
import 'ui/setup_dialog.dart';
|
import 'ui/setup_dialog.dart';
|
||||||
import 'ui/update_dialog.dart';
|
import 'ui/update_dialog.dart';
|
||||||
|
|
@ -91,8 +92,8 @@ Future<void> main(List<String> args) async {
|
||||||
controller.note(trf('⚠ 지난 실행이 정상적으로 끝나지 않았습니다 (시작 {0}, 마지막 확인 {1}). ' '그 전의 기록은 Logs 폴더의 app.log 에 있습니다.', [hm(crashed.$1), hm(crashed.$2)]));
|
controller.note(trf('⚠ 지난 실행이 정상적으로 끝나지 않았습니다 (시작 {0}, 마지막 확인 {1}). ' '그 전의 기록은 Logs 폴더의 app.log 에 있습니다.', [hm(crashed.$1), hm(crashed.$2)]));
|
||||||
}
|
}
|
||||||
await controller.init();
|
await controller.init();
|
||||||
// 실시간 동기화 (환경 설정 > 파일 탐색기): 앱이 켜져 있는 동안
|
// 실시간 동기화 (환경 설정 > 파일 탐색기): 앱이 켜져 있는 동안. 다시 켤 때 바로 / 골라서 / 시작 안 함
|
||||||
LiveSync(controller).start();
|
final live = LiveSync(controller)..start(hold: controller.settings.liveSyncOnStart != 'auto');
|
||||||
// 화면 언어 (환경 설정 > 화면 언어)
|
// 화면 언어 (환경 설정 > 화면 언어)
|
||||||
i18n.init(controller, dataDir);
|
i18n.init(controller, dataDir);
|
||||||
await i18n.apply(controller.settings.uiLanguage, save: false);
|
await i18n.apply(controller.settings.uiLanguage, save: false);
|
||||||
|
|
@ -295,6 +296,8 @@ Future<void> main(List<String> args) async {
|
||||||
final nav = navigatorKey.currentState;
|
final nav = navigatorKey.currentState;
|
||||||
if (nav != null) unawaited(ExplorerPage.open(nav, c: controller));
|
if (nav != null) unawaited(ExplorerPage.open(nav, c: controller));
|
||||||
}
|
}
|
||||||
|
if (controller.settings.liveSyncOnStart == 'ask' && ctx.mounted) await askLiveSyncStart(ctx, live);
|
||||||
|
if (!ctx.mounted) return;
|
||||||
await checkRequiredTools(ctx, services.shell);
|
await checkRequiredTools(ctx, services.shell);
|
||||||
final ctx2 = navigatorKey.currentContext;
|
final ctx2 = navigatorKey.currentContext;
|
||||||
if (ctx2 != null && ctx2.mounted) await checkForUpdate(ctx2, controller);
|
if (ctx2 != null && ctx2.mounted) await checkForUpdate(ctx2, controller);
|
||||||
|
|
@ -374,8 +377,8 @@ Future<void> runAndroid(String dataDir) async {
|
||||||
final controller = AppController(services, settingsStore: SettingsStore())..logFile = p.join(logs.path, 'app.log');
|
final controller = AppController(services, settingsStore: SettingsStore())..logFile = p.join(logs.path, 'app.log');
|
||||||
controller.note(trf('── 시작 {0} (Android) ──', [appTitle]));
|
controller.note(trf('── 시작 {0} (Android) ──', [appTitle]));
|
||||||
await controller.init();
|
await controller.init();
|
||||||
// 실시간 동기화 (환경 설정 > 파일 탐색기): 앱이 켜져 있는 동안
|
// 실시간 동기화 (환경 설정 > 파일 탐색기): 앱이 켜져 있는 동안. 다시 켤 때 바로 / 골라서 / 시작 안 함
|
||||||
LiveSync(controller).start();
|
final live = LiveSync(controller)..start(hold: controller.settings.liveSyncOnStart != 'auto');
|
||||||
// 화면 언어 (환경 설정 > 화면 언어)
|
// 화면 언어 (환경 설정 > 화면 언어)
|
||||||
i18n.init(controller, dataDir);
|
i18n.init(controller, dataDir);
|
||||||
await i18n.apply(controller.settings.uiLanguage, save: false);
|
await i18n.apply(controller.settings.uiLanguage, save: false);
|
||||||
|
|
@ -414,6 +417,8 @@ Future<void> runAndroid(String dataDir) async {
|
||||||
downloads.log = controller.note;
|
downloads.log = controller.note;
|
||||||
// 진행 중인 일이 있으면 화면에서 내려가도 계속 (알림에 진행 상황)
|
// 진행 중인 일이 있으면 화면에서 내려가도 계속 (알림에 진행 상황)
|
||||||
AndroidKeepAlive(controller, downloads);
|
AndroidKeepAlive(controller, downloads);
|
||||||
|
// 백그라운드로 실행: 화면을 닫았다가 다시 열면 새 화면 (Activity) 이 살아 있던 엔진에 붙는다 → 화면 방향을 다시 적용
|
||||||
|
AppLifecycleListener(onResume: () => applyScreenOrientation(controller.settings.screenOrientation));
|
||||||
// 동영상 목록 기억 (앱을 껐다 켜도 그대로, 없어진 파일은 뺀다) - Windows 와 같은 파일
|
// 동영상 목록 기억 (앱을 껐다 켜도 그대로, 없어진 파일은 뺀다) - Windows 와 같은 파일
|
||||||
unawaited(controller.shareVideoList(p.join(dataDir, 'videos.json')));
|
unawaited(controller.shareVideoList(p.join(dataDir, 'videos.json')));
|
||||||
|
|
||||||
|
|
@ -452,6 +457,9 @@ Future<void> runAndroid(String dataDir) async {
|
||||||
final nav = navigatorKey.currentState;
|
final nav = navigatorKey.currentState;
|
||||||
if (nav != null) unawaited(ExplorerPage.open(nav, c: controller));
|
if (nav != null) unawaited(ExplorerPage.open(nav, c: controller));
|
||||||
}
|
}
|
||||||
|
// 다시 켤 때 실시간 동기화를 골라서 시작
|
||||||
|
final ctx0 = navigatorKey.currentContext;
|
||||||
|
if (controller.settings.liveSyncOnStart == 'ask' && ctx0 != null && ctx0.mounted) await askLiveSyncStart(ctx0, live);
|
||||||
// 새 버전 확인 (하루 한 번, 환경 설정에서 끌 수 있음)
|
// 새 버전 확인 (하루 한 번, 환경 설정에서 끌 수 있음)
|
||||||
final ctx = navigatorKey.currentContext;
|
final ctx = navigatorKey.currentContext;
|
||||||
if (ctx != null && ctx.mounted) unawaited(checkForUpdate(ctx, controller));
|
if (ctx != null && ctx.mounted) unawaited(checkForUpdate(ctx, controller));
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,14 @@ import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import '../../app/app_controller.dart';
|
import '../../app/app_controller.dart';
|
||||||
import '../../app/download_manager.dart';
|
import '../../app/download_manager.dart';
|
||||||
|
import '../../app/live_sync.dart';
|
||||||
import '../../services/downloader.dart';
|
import '../../services/downloader.dart';
|
||||||
import '../../l10n/tr.dart';
|
import '../../l10n/tr.dart';
|
||||||
|
|
||||||
/// Android: 다운로드 · MKV 만들기 · AI 자막이 진행 중이면 포그라운드 서비스 (MainActivity.kt · KeepAliveService.kt)
|
/// Android: 다운로드 · MKV 만들기 · AI 자막이 진행 중이면 포그라운드 서비스 (MainActivity.kt · KeepAliveService.kt)
|
||||||
/// 를 띄워 앱이 화면에서 내려가도 계속하게 하고, 알림에 진행 상황을 보여 준다. 모두 끝나면 내린다.
|
/// 를 띄워 앱이 화면에서 내려가도 계속하게 하고, 알림에 진행 상황을 보여 준다. 모두 끝나면 내린다.
|
||||||
|
/// [AppSettings.runInBackground] 를 켜면 실시간 동기화를 지켜보는 동안에도 띄워 두고, 앱을 닫아도 (← · 최근 앱에서 밀기)
|
||||||
|
/// Dart 엔진을 살려 둔다 (MainActivity.setBackground).
|
||||||
class AndroidKeepAlive {
|
class AndroidKeepAlive {
|
||||||
static const _ch = MethodChannel('jj_mkvmaker/android');
|
static const _ch = MethodChannel('jj_mkvmaker/android');
|
||||||
|
|
||||||
|
|
@ -21,18 +24,37 @@ class AndroidKeepAlive {
|
||||||
AndroidKeepAlive(this.c, this.downloads) {
|
AndroidKeepAlive(this.c, this.downloads) {
|
||||||
c.addListener(_changed);
|
c.addListener(_changed);
|
||||||
downloads.addListener(_changed);
|
downloads.addListener(_changed);
|
||||||
|
_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LiveSync? _live;
|
||||||
|
bool? _background;
|
||||||
|
|
||||||
/// 진행률 알림은 자주 바뀌므로 1초에 한 번만 보낸다
|
/// 진행률 알림은 자주 바뀌므로 1초에 한 번만 보낸다
|
||||||
void _changed() => _timer ??= Timer(const Duration(seconds: 1), _apply);
|
void _changed() => _timer ??= Timer(const Duration(seconds: 1), _apply);
|
||||||
|
|
||||||
Future<void> _apply() async {
|
Future<void> _apply() async {
|
||||||
_timer = null;
|
_timer = null;
|
||||||
|
// 실시간 동기화 (나중에 만들어질 수도 있다)
|
||||||
|
final live = LiveSync.instance;
|
||||||
|
if (live != _live) {
|
||||||
|
_live?.removeListener(_changed);
|
||||||
|
_live = live?..addListener(_changed);
|
||||||
|
}
|
||||||
|
final bg = c.settings.runInBackground;
|
||||||
|
if (bg != _background) {
|
||||||
|
_background = bg;
|
||||||
|
try {
|
||||||
|
await _ch.invokeMethod<void>('setBackground', {'on': bg});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
// 일시정지한 다운로드는 세지 않는다 (받는 중 · 대기 중만)
|
// 일시정지한 다운로드는 세지 않는다 (받는 중 · 대기 중만)
|
||||||
final running = downloads.tasks
|
final running = downloads.tasks
|
||||||
.where((t) => t.state == DownloadState.downloading || t.state == DownloadState.queued)
|
.where((t) => t.state == DownloadState.downloading || t.state == DownloadState.queued)
|
||||||
.length;
|
.length;
|
||||||
final (text, progress) = status(c.currentJob, c.busy, running, downloads.overallProgress);
|
final syncs = bg && live != null ? live.watching.length : 0;
|
||||||
|
final (text, progress) =
|
||||||
|
status(c.currentJob, c.busy, running, downloads.overallProgress, syncs: syncs, syncing: live?.anyRunning ?? false);
|
||||||
try {
|
try {
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
if (_on) await _ch.invokeMethod<void>('stopKeepAlive');
|
if (_on) await _ch.invokeMethod<void>('stopKeepAlive');
|
||||||
|
|
@ -49,10 +71,13 @@ class AndroidKeepAlive {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 알림 글과 진행률 (0~100, 모르면 -1). 진행 중인 일이 없으면 글이 null.
|
/// 알림 글과 진행률 (0~100, 모르면 -1). 진행 중인 일이 없으면 글이 null.
|
||||||
static (String?, int) status(String? job, bool busy, int downloads, double? downloadProgress) {
|
/// [syncs] 백그라운드로 지켜보는 실시간 동기화 수 ([syncing] 지금 맞추는 중)
|
||||||
|
static (String?, int) status(String? job, bool busy, int downloads, double? downloadProgress,
|
||||||
|
{int syncs = 0, bool syncing = false}) {
|
||||||
final parts = [
|
final parts = [
|
||||||
if (busy) job ?? tr('작업 중'),
|
if (busy) job ?? tr('작업 중'),
|
||||||
if (downloads > 0) trf('다운로드 {0}개{1}', [downloads, downloadProgress == null ? '' : ' · ${(downloadProgress * 100).round()}%']),
|
if (downloads > 0) trf('다운로드 {0}개{1}', [downloads, downloadProgress == null ? '' : ' · ${(downloadProgress * 100).round()}%']),
|
||||||
|
if (syncs > 0) syncing ? trf('동기화 {0}개 · 맞추는 중', [syncs]) : trf('동기화 {0}개 지켜보는 중', [syncs]),
|
||||||
];
|
];
|
||||||
if (parts.isEmpty) return (null, -1);
|
if (parts.isEmpty) return (null, -1);
|
||||||
// 진행률 막대는 다운로드만 있을 때 (작업 진행률은 동영상마다 따로라 하나로 합치기 어렵다)
|
// 진행률 막대는 다운로드만 있을 때 (작업 진행률은 동영상마다 따로라 하나로 합치기 어렵다)
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,15 @@ class AndroidShell extends NoopShell {
|
||||||
static const _ch = MethodChannel('jj_mkvmaker/android');
|
static const _ch = MethodChannel('jj_mkvmaker/android');
|
||||||
bool _full = false;
|
bool _full = false;
|
||||||
|
|
||||||
|
/// [종료]: 백그라운드로 살려 둔 엔진 · 작업 알림까지 끝낸다 (화면만 닫는 ← 와 다름)
|
||||||
@override
|
@override
|
||||||
Future<void> quit() => SystemNavigator.pop();
|
Future<void> quit() async {
|
||||||
|
try {
|
||||||
|
await _ch.invokeMethod<void>('exitApp');
|
||||||
|
} catch (_) {
|
||||||
|
await SystemNavigator.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setFullScreen(bool on) async {
|
Future<void> setFullScreen(bool on) async {
|
||||||
|
|
|
||||||
|
|
@ -286,6 +286,7 @@ class _LiveSyncTile extends StatelessWidget {
|
||||||
label: Text(tr('추가')),
|
label: Text(tr('추가')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Padding(padding: const EdgeInsets.only(left: 16), child: LiveSyncRunOptions(c: c)),
|
||||||
for (var i = 0; i < pairs.length; i++)
|
for (var i = 0; i < pairs.length; i++)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 16),
|
padding: const EdgeInsets.only(left: 16),
|
||||||
|
|
@ -336,3 +337,66 @@ class _LiveSyncTile extends StatelessWidget {
|
||||||
return live == null ? body() : ListenableBuilder(listenable: live, builder: (_, _) => body());
|
return live == null ? body() : ListenableBuilder(listenable: live, builder: (_, _) => body());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 백그라운드로 실행 · 앱을 다시 켤 때 동기화 (환경 설정 > 파일 탐색기 > 실시간 동기화, 모니터링 > lsync 위)
|
||||||
|
class LiveSyncRunOptions extends StatelessWidget {
|
||||||
|
final AppController c;
|
||||||
|
|
||||||
|
/// 모니터링: 한 줄로 짧게
|
||||||
|
final bool dense;
|
||||||
|
const LiveSyncRunOptions({super.key, required this.c, this.dense = false});
|
||||||
|
|
||||||
|
static String backgroundHelp() => Platform.isWindows
|
||||||
|
? tr('창을 닫아도 (✕) 트레이에서 동기화 · MKV 만들기 · 다운로드를 계속합니다. 끄면 닫을 때 모두 끝냅니다. (실행 · 종료의 "종료를 누르면" 과 같은 설정)')
|
||||||
|
: tr('← 로 닫거나 최근 앱 목록에서 밀어도 동기화 · MKV 만들기 · 다운로드를 계속합니다 (알림에 표시). 끄면 지금처럼 닫을 때 끝납니다. 완전히 끝내려면 [종료].');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final s = c.settings;
|
||||||
|
final onStart = DropdownButton<String>(
|
||||||
|
value: s.liveSyncOnStart,
|
||||||
|
isDense: dense,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(value: 'auto', child: Text(tr('바로 시작 (기본)'))),
|
||||||
|
DropdownMenuItem(value: 'ask', child: Text(tr('골라서 시작'))),
|
||||||
|
DropdownMenuItem(value: 'off', child: Text(tr('시작 안 함 (모니터링에서 시작)'))),
|
||||||
|
],
|
||||||
|
onChanged: (v) => c.updateSettings((x) => x.liveSyncOnStart = v!),
|
||||||
|
);
|
||||||
|
void setBackground(bool? v) => c.updateSettings((x) => x.backgroundRun = v ?? false);
|
||||||
|
if (dense) {
|
||||||
|
return Wrap(spacing: 12, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||||
|
Tooltip(
|
||||||
|
message: backgroundHelp(),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => setBackground(!s.backgroundRun),
|
||||||
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
Checkbox(value: s.backgroundRun, onChanged: setBackground),
|
||||||
|
Text(tr('백그라운드로 실행'), style: const TextStyle(fontSize: 13)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
Text(tr('다시 켤 때 동기화'), style: const TextStyle(fontSize: 13, color: JjColors.textDim)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
onStart,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return Column(children: [
|
||||||
|
CheckboxListTile(
|
||||||
|
dense: true,
|
||||||
|
value: s.backgroundRun,
|
||||||
|
onChanged: setBackground,
|
||||||
|
title: Text(tr('백그라운드로 실행')),
|
||||||
|
subtitle: Text(backgroundHelp()),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(tr('앱을 다시 켤 때 동기화')),
|
||||||
|
subtitle: Text(tr('바로 시작: 켜진 동기화를 모두 시작 · 골라서 시작: 켤 때 고르기 · 시작 안 함: 모니터링 > lsync 의 시작 버튼으로')),
|
||||||
|
trailing: onStart,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import '../core/file_ops.dart';
|
||||||
import '../core/sync_tools.dart';
|
import '../core/sync_tools.dart';
|
||||||
import '../l10n/tr.dart';
|
import '../l10n/tr.dart';
|
||||||
import 'app_actions.dart';
|
import 'app_actions.dart';
|
||||||
import 'copy_sync_settings.dart' show addLiveSyncPairDialog;
|
import 'copy_sync_settings.dart' show addLiveSyncPairDialog, LiveSyncRunOptions;
|
||||||
import 'rsync_setup.dart';
|
import 'rsync_setup.dart';
|
||||||
import 'schedule_editor.dart';
|
import 'schedule_editor.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
@ -442,6 +442,18 @@ class _LiveTabState extends State<_LiveTab> {
|
||||||
),
|
),
|
||||||
TextButton.icon(onPressed: () => addLiveSyncPairDialog(context, c), icon: const Icon(Icons.add), label: Text(tr('추가'))),
|
TextButton.icon(onPressed: () => addLiveSyncPairDialog(context, c), icon: const Icon(Icons.add), label: Text(tr('추가'))),
|
||||||
]),
|
]),
|
||||||
|
// 백그라운드로 실행 · 다시 켤 때 동기화 (환경 설정과 같은 값) · 모두 시작 / 멈춤
|
||||||
|
Wrap(spacing: 12, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||||
|
LiveSyncRunOptions(c: c, dense: true),
|
||||||
|
if (live != null && pairs.any((x) => x.enabled))
|
||||||
|
live.paused.isEmpty
|
||||||
|
? TextButton.icon(
|
||||||
|
onPressed: live.pauseAll, icon: const Icon(Icons.pause, size: 18), label: Text(tr('모두 멈춤')))
|
||||||
|
: FilledButton.tonalIcon(
|
||||||
|
onPressed: live.resumeAll,
|
||||||
|
icon: const Icon(Icons.play_arrow, size: 18),
|
||||||
|
label: Text(trf('모두 시작 (멈춘 것 {0}개)', [pairs.where(live.isPaused).length]))),
|
||||||
|
]),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (pairs.isEmpty)
|
if (pairs.isEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
|
|
@ -461,6 +473,7 @@ class _LiveTabState extends State<_LiveTab> {
|
||||||
final st = live?.status[k];
|
final st = live?.status[k];
|
||||||
final active = LiveSync.activeNow(x);
|
final active = LiveSync.activeNow(x);
|
||||||
final running = live?.isRunning(x) ?? false;
|
final running = live?.isRunning(x) ?? false;
|
||||||
|
final held = live?.isPaused(x) ?? false;
|
||||||
return Card(
|
return Card(
|
||||||
color: JjColors.panel,
|
color: JjColors.panel,
|
||||||
margin: const EdgeInsets.only(bottom: 10),
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
|
@ -468,8 +481,17 @@ class _LiveTabState extends State<_LiveTab> {
|
||||||
padding: const EdgeInsets.fromLTRB(14, 10, 10, 10),
|
padding: const EdgeInsets.fromLTRB(14, 10, 10, 10),
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
Icon(running ? Icons.sync : x.enabled ? (active ? Icons.sync : Icons.bedtime_outlined) : Icons.sync_disabled,
|
Icon(
|
||||||
color: x.enabled ? (active ? JjColors.accent : Colors.orangeAccent) : JjColors.textDim),
|
running
|
||||||
|
? Icons.sync
|
||||||
|
: !x.enabled
|
||||||
|
? Icons.sync_disabled
|
||||||
|
: held
|
||||||
|
? Icons.pause_circle_outline
|
||||||
|
: active
|
||||||
|
? Icons.sync
|
||||||
|
: Icons.bedtime_outlined,
|
||||||
|
color: x.enabled && !held ? (active ? JjColors.accent : Colors.orangeAccent) : JjColors.textDim),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
|
@ -481,8 +503,21 @@ class _LiveTabState extends State<_LiveTab> {
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
label: Text([tr(CopyMethod.of(x.method).label), if (x.delete) tr('지우기 포함')].join(' · ')),
|
label: Text([tr(CopyMethod.of(x.method).label), if (x.delete) tr('지우기 포함')].join(' · ')),
|
||||||
),
|
),
|
||||||
|
// 이번 실행 동안 멈춤 / 시작 (설정의 켜기 · 끄기는 오른쪽 스위치)
|
||||||
|
if (live != null && x.enabled)
|
||||||
|
IconButton(
|
||||||
|
tooltip: held ? tr('시작') : tr('멈춤 (이번 실행 동안)'),
|
||||||
|
icon: Icon(held ? Icons.play_arrow : Icons.pause, color: held ? JjColors.accent : null),
|
||||||
|
onPressed: () => held ? live.resume(x) : live.pause(x),
|
||||||
|
),
|
||||||
Switch(value: x.enabled, onChanged: (v) => _replace(x, x.copyWith(enabled: v))),
|
Switch(value: x.enabled, onChanged: (v) => _replace(x, x.copyWith(enabled: v))),
|
||||||
]),
|
]),
|
||||||
|
if (held)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 32, top: 2),
|
||||||
|
child: Text(tr('멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다'),
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 32, top: 4),
|
padding: const EdgeInsets.only(left: 32, top: 4),
|
||||||
child: Wrap(spacing: 16, runSpacing: 6, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
child: Wrap(spacing: 16, runSpacing: 6, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||||
|
|
@ -578,3 +613,45 @@ class _LiveTabState extends State<_LiveTab> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 앱을 다시 켤 때 ([AppSettings.liveSyncOnStart] 'ask'): 어느 실시간 동기화를 시작할지 고르기.
|
||||||
|
/// 고르지 않은 것은 이번 실행 동안 멈춤 (모니터링 > lsync 에서 ▶ 로 시작).
|
||||||
|
Future<void> askLiveSyncStart(BuildContext context, LiveSync live) async {
|
||||||
|
final pairs = live.c.settings.liveSyncPairs.where((x) => x.enabled).toList();
|
||||||
|
if (pairs.isEmpty) return;
|
||||||
|
final pick = {for (final x in pairs) LiveSync.keyOf(x)};
|
||||||
|
final ok = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => StatefulBuilder(
|
||||||
|
builder: (ctx, set) => AlertDialog(
|
||||||
|
title: Text(tr('실시간 동기화 시작')),
|
||||||
|
content: SizedBox(
|
||||||
|
width: 520,
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(tr('시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).'),
|
||||||
|
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Flexible(
|
||||||
|
child: ListView(shrinkWrap: true, children: [
|
||||||
|
for (final x in pairs)
|
||||||
|
CheckboxListTile(
|
||||||
|
dense: true,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
value: pick.contains(LiveSync.keyOf(x)),
|
||||||
|
onChanged: (v) => set(() => v == true ? pick.add(LiveSync.keyOf(x)) : pick.remove(LiveSync.keyOf(x))),
|
||||||
|
title: Text('${x.source} → ${x.target}', maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||||
|
subtitle: Text(scheduleSummary(x.schedule)),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('모두 나중에'))),
|
||||||
|
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('고른 것 시작'))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (ok == true) live.runOnly(pairs.where((x) => pick.contains(LiveSync.keyOf(x))));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,8 @@ void main() {
|
||||||
// 작업 (MKV · AI) 이 있으면 글만
|
// 작업 (MKV · AI) 이 있으면 글만
|
||||||
expect(AndroidKeepAlive.status('MKV 만들기', true, 0, null), ('MKV 만들기', -1));
|
expect(AndroidKeepAlive.status('MKV 만들기', true, 0, null), ('MKV 만들기', -1));
|
||||||
expect(AndroidKeepAlive.status('AI 자막', true, 1, 0.5), ('AI 자막 · 다운로드 1개 · 50%', -1));
|
expect(AndroidKeepAlive.status('AI 자막', true, 1, 0.5), ('AI 자막 · 다운로드 1개 · 50%', -1));
|
||||||
|
// 백그라운드로 실행: 실시간 동기화를 지켜보는 동안에도
|
||||||
|
expect(AndroidKeepAlive.status(null, false, 0, null, syncs: 2), ('동기화 2개 지켜보는 중', -1));
|
||||||
|
expect(AndroidKeepAlive.status('MKV 만들기', true, 0, null, syncs: 1, syncing: true), ('MKV 만들기 · 동기화 1개 · 맞추는 중', -1));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,56 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('백그라운드 · 다시 켤 때 동기화', () {
|
||||||
|
test('설정 저장 · 읽기, Windows 는 종료 동작과 같은 값', () {
|
||||||
|
final s = AppSettings()
|
||||||
|
..runInBackground = true
|
||||||
|
..liveSyncOnStart = 'ask';
|
||||||
|
final r = AppSettings.fromJson(s.toJson());
|
||||||
|
expect([r.runInBackground, r.liveSyncOnStart], [true, 'ask']);
|
||||||
|
expect(AppSettings.fromJson({'liveSyncOnStart': 'x'}).liveSyncOnStart, 'auto');
|
||||||
|
expect(AppSettings.fromJson({}).runInBackground, isFalse);
|
||||||
|
final w = AppSettings()..backgroundRun = false;
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
expect(w.closeAction, 'quit');
|
||||||
|
w.backgroundRun = true;
|
||||||
|
expect([w.closeAction, w.backgroundRun], ['background', true]);
|
||||||
|
} else {
|
||||||
|
expect(w.runInBackground, isFalse);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('멈춘 채로 시작 · 고른 것만 시작 · 모두 시작', () async {
|
||||||
|
final tmp = Directory.systemTemp.createTempSync('jj_hold_');
|
||||||
|
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||||
|
final a = Directory(p.join(tmp.path, 'a'))..createSync();
|
||||||
|
final b = Directory(p.join(tmp.path, 'b'))..createSync();
|
||||||
|
File(p.join(a.path, '1.txt')).writeAsStringSync('1');
|
||||||
|
File(p.join(b.path, '2.txt')).writeAsStringSync('2');
|
||||||
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||||
|
final pa = LiveSyncPair(a.path, p.join(tmp.path, 'ta')), pb = LiveSyncPair(b.path, p.join(tmp.path, 'tb'));
|
||||||
|
c.settings
|
||||||
|
..liveSyncPairs = [pa, pb]
|
||||||
|
..liveSyncOnStart = 'off';
|
||||||
|
final live = LiveSync(c)..start(hold: c.settings.liveSyncOnStart != 'auto');
|
||||||
|
addTearDown(live.dispose);
|
||||||
|
expect(live.watching, isEmpty);
|
||||||
|
await Future<void>.delayed(const Duration(seconds: 4));
|
||||||
|
expect(Directory(pa.target).existsSync(), isFalse); // 시작 안 함
|
||||||
|
live.runOnly([pa]);
|
||||||
|
expect(live.watching.map((x) => x.source), [a.path]);
|
||||||
|
expect(live.isPaused(pb), isTrue);
|
||||||
|
await Future<void>.delayed(const Duration(seconds: 4));
|
||||||
|
expect(File(p.join(pa.target, '1.txt')).existsSync(), isTrue);
|
||||||
|
expect(Directory(pb.target).existsSync(), isFalse);
|
||||||
|
live.resumeAll();
|
||||||
|
await Future<void>.delayed(const Duration(seconds: 4));
|
||||||
|
expect(File(p.join(pb.target, '2.txt')).existsSync(), isTrue);
|
||||||
|
live.pause(pa);
|
||||||
|
expect(live.watching.map((x) => x.source), [b.path]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('모니터링 화면: 복사 목록 · 옵션 고치기 → 현재 유지 / 반영 후 실행, lsync 탭 · 일정', (tester) async {
|
testWidgets('모니터링 화면: 복사 목록 · 옵션 고치기 → 현재 유지 / 반영 후 실행, lsync 탭 · 일정', (tester) async {
|
||||||
final tmp = Directory.systemTemp.createTempSync('jj_monui_');
|
final tmp = Directory.systemTemp.createTempSync('jj_monui_');
|
||||||
addTearDown(() => tmp.deleteSync(recursive: true));
|
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue