diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d70a96b1..ce0998de 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -177,6 +177,8 @@ dependencies { // implementation 'com.vladsch.flexmark:flexmark-all:0.64.8' // implementation("org.opencv:opencv-android:4.11.0") // build.gradle에 추가 + implementation ("androidx.emoji2:emoji2:1.4.0") + implementation ("com.googlecode.juniversalchardet:juniversalchardet:1.0.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") // implementation ("com.github.aeonSolutions:FloatingActionButtonMenuDrag:1.1") implementation("io.github.junkfood02.youtubedl-android:library:0.17.4") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3dc637e8..39483a9e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -132,7 +132,7 @@ android:launchMode="singleInstance" android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize" android:screenOrientation="portrait" - android:excludeFromRecents="true" + android:excludeFromRecents="false" android:hardwareAccelerated="true" android:exported="false"> diff --git a/app/src/main/assets/extensions/my_extension/inject-viewport.js b/app/src/main/assets/extensions/my_extension/inject-viewport.js index 64e77b24..e3c936b3 100644 --- a/app/src/main/assets/extensions/my_extension/inject-viewport.js +++ b/app/src/main/assets/extensions/my_extension/inject-viewport.js @@ -1,9 +1,9 @@ -(function() { - var meta = document.querySelector('meta[name=viewport]'); - if (!meta) { - meta = document.createElement('meta'); - meta.name = 'viewport'; - document.head.appendChild(meta); - } - meta.setAttribute('content', 'width=device-width, initial-scale=1.0'); -})(); +//(function() { +// var meta = document.querySelector('meta[name=viewport]'); +// if (!meta) { +// meta = document.createElement('meta'); +// meta.name = 'viewport'; +// document.head.appendChild(meta); +// } +// meta.setAttribute('content', 'width=device-width, initial-scale=1.0'); +//})(); diff --git a/app/src/main/assets/extensions/my_extension/manifest.json b/app/src/main/assets/extensions/my_extension/manifest.json index c8039d36..87b7ce9e 100644 --- a/app/src/main/assets/extensions/my_extension/manifest.json +++ b/app/src/main/assets/extensions/my_extension/manifest.json @@ -13,7 +13,7 @@ }, "content_scripts": [ { - "run_at": "document_end", + "run_at": "document_start", "matches": [""], "js": ["messaging.js","inject-viewport.js"] } diff --git a/app/src/main/assets/extensions/my_extension/messaging.js b/app/src/main/assets/extensions/my_extension/messaging.js index 5a6d20bf..23f6ca1d 100644 --- a/app/src/main/assets/extensions/my_extension/messaging.js +++ b/app/src/main/assets/extensions/my_extension/messaging.js @@ -1,5 +1,6 @@ + function pubDateNumber(tdateTime) { let date = new Date(); let dateTime = date.getTime(); @@ -55,11 +56,11 @@ port.onMessage.addListener(response => { var type= response["type"]; switch (type) { - case "GO_TO_SUBTITLE_DETAIL": { - const detailUrl = response["url"]; - location.href = detailUrl; // 상세 페이지로 이동 - break; - } + case "GO_TO_SUBTITLE_DETAIL": { + const detailUrl = response["url"]; + location.href = detailUrl; // 상세 페이지로 이동 + break; + } case "SEARCH_SUBTITLE_CAT": { const query = response["query"]; const searchUrl = `https://www.subtitlecat.com/index.php?search=${encodeURIComponent(query)}`; @@ -72,7 +73,7 @@ port.onMessage.addListener(response => { behavior: 'smooth' }); } - break; + break; case "SEEK_NEXT": // 5초 앞으로 { let btn = @@ -469,13 +470,9 @@ if(document.querySelector(".list-body") !== null) { var listBody = null try {listBody = document.querySelector(".list-body");}catch (e) {} getList(listBody.children) -} else if(document.querySelector(".novel-eps") !== null) { - //document.querySelector(".novel-eps").children - removeSpecificGifs() - var listBody = null - try {listBody = document.querySelector(".novel-eps");}catch (e) {} - getList2(listBody.children) -} else if(document.querySelector("#novel_content") !== null){ +} + +if(document.querySelector("#novel_content") !== null){ removeSpecificGifs() var title = null var contents = null @@ -492,24 +489,9 @@ if(document.querySelector(".list-body") !== null) { } ); } -} else if(document.querySelector('[style^="--novel"]') !== null){ - removeSpecificGifs() - var title = null - var contents = null - try {title = toonTitle(document.querySelector(".page-desc")); }catch (e) {} - try {contents = toonContents(document.querySelector('[style^="--novel"]'))}catch (e) {} - if (toonTitle !== undefined && toonContents !== undefined && toonTitle !== "" && toonContents !=="") { - sendMessage( - { - type: "BookContents", - book : { - chapterTitle : title, - bookContents : contents - } - } - ); - } } + + if(document.querySelector("#html_encoder_div")) { sendMessage( { @@ -814,7 +796,9 @@ document.addEventListener('DOMContentLoaded', function () { if (document.readyState === 'complete') { sendCookiesToNative(); + try{removeSpecificGifs();}catch(e){} } else { + // 2. 아직 로딩 중이라면 window.onload(모든 리소스 로드 완료) 시점에 실행 window.addEventListener('load', sendCookiesToNative); } @@ -860,27 +844,36 @@ function extractSubtitleList() { } function scrollToEndAndExtract() { if (location.host.includes("subtitlecat.com")) { - const scrollStep = 800; // 한 번에 스크롤할 양 - const scrollDelay = 500; // 다음 스크롤까지 대기 시간 (ms) + const scrollStep = 800; // 한 번에 스크롤할 양 + const scrollDelay = 500; // 다음 스크롤까지 대기 시간 (ms) - function step() { - const prevScrollY = window.scrollY; - window.scrollBy(0, scrollStep); + function step() { + const prevScrollY = window.scrollY; + window.scrollBy(0, scrollStep); + console.log("ONSTEPS"); + // 💡 0.5초 대기 후 현재 위치가 이전과 같다면(끝에 도달) 추출 시작 + setTimeout(() => { + if (window.scrollY === prevScrollY || + (window.innerHeight + window.scrollY) >= document.body.scrollHeight) { + if (location.host.includes("subtitlecat.com")) { + console.log("✅ 페이지 끝 도달 - 자막 리스트 추출 시작"); + extractSubtitleList(); // 기존에 정의한 추출 함수 호출 + } else { + + } - // 💡 0.5초 대기 후 현재 위치가 이전과 같다면(끝에 도달) 추출 시작 - setTimeout(() => { - if (window.scrollY === prevScrollY || - (window.innerHeight + window.scrollY) >= document.body.scrollHeight) { - console.log("✅ 페이지 끝 도달 - 자막 리스트 추출 시작"); - extractSubtitleList(); // 기존에 정의한 추출 함수 호출 - } else { - step(); // 아직 끝이 아니면 다음 스크롤 진행 - } - }, scrollDelay); + } else { + step(); // 아직 끝이 아니면 다음 스크롤 진행 + } + }, scrollDelay); + } + + step() + + } else { + console.log(`is FAIL NOVEL ${location.href.includes("/novel/")}`); } - step(); - } } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt index d94bbf28..dc1ee01a 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt @@ -580,10 +580,10 @@ open class LauncherActivity : CommonActivity() { } }) - requestSmsPermissionLauncher.launch(arrayOf( - android.Manifest.permission.RECEIVE_SMS, - android.Manifest.permission.READ_SMS - )) +// requestSmsPermissionLauncher.launch(arrayOf( +// android.Manifest.permission.RECEIVE_SMS, +// android.Manifest.permission.READ_SMS +// )) handleSharedIntent(intent) connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager @@ -614,7 +614,9 @@ open class LauncherActivity : CommonActivity() { putExtra("WIFI_STATE", isWifiConnected) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(intent) + try { + startForegroundService(intent) + } catch (e: Exception){e.printStackTrace()} } else { startService(intent) } @@ -634,35 +636,35 @@ open class LauncherActivity : CommonActivity() { } connectivityManager.registerNetworkCallback(request, networkCallback!!) } - private var smsReceiver: SmsReceiver? = null +// private var smsReceiver: SmsReceiver? = null // 권한 요청 결과 처리기 - private val requestSmsPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestMultiplePermissions() - ) { permissions -> - val granted = permissions[android.Manifest.permission.RECEIVE_SMS] ?: false - if (granted) { - Blog.LOGE("SMS 권한 승인됨 -> 리시버 동적 등록 실행") - registerSmsDynamicReceiver() - } - } - - private fun registerSmsDynamicReceiver() { - if (smsReceiver == null) { - smsReceiver = SmsReceiver() - val filter = IntentFilter("android.provider.Telephony.SMS_RECEIVED").apply { - priority = 2147483647 // 시스템 최우선 순위 - } - - // Android 14 이상(안드 16 포함) 필수 플래그: RECEIVER_EXPORTED - // 외부 앱(시스템 타워)으로부터 브로드캐스트를 받으려면 필수입니다. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - registerReceiver(smsReceiver, filter, Context.RECEIVER_EXPORTED) - } else { - registerReceiver(smsReceiver, filter) - } - } - } +// private val requestSmsPermissionLauncher = registerForActivityResult( +// ActivityResultContracts.RequestMultiplePermissions() +// ) { permissions -> +// val granted = permissions[android.Manifest.permission.RECEIVE_SMS] ?: false +// if (granted) { +// Blog.LOGE("SMS 권한 승인됨 -> 리시버 동적 등록 실행") +// registerSmsDynamicReceiver() +// } +// } +// +// private fun registerSmsDynamicReceiver() { +// if (smsReceiver == null) { +// smsReceiver = SmsReceiver() +// val filter = IntentFilter("android.provider.Telephony.SMS_RECEIVED").apply { +// priority = 2147483647 // 시스템 최우선 순위 +// } +// +// // Android 14 이상(안드 16 포함) 필수 플래그: RECEIVER_EXPORTED +// // 외부 앱(시스템 타워)으로부터 브로드캐스트를 받으려면 필수입니다. +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { +// registerReceiver(smsReceiver, filter, Context.RECEIVER_EXPORTED) +// } else { +// registerReceiver(smsReceiver, filter) +// } +// } +// } private fun updateWidgetOptions(appWidgetId: Int, hostView: AppWidgetHostView) { val width = hostView.layoutParams.width @@ -966,10 +968,10 @@ open class LauncherActivity : CommonActivity() { private lateinit var connectivityManager: ConnectivityManager private var networkCallback: ConnectivityManager.NetworkCallback? = null override fun onDestroy() { - smsReceiver?.let { - unregisterReceiver(it) - smsReceiver = null - } +// smsReceiver?.let { +// unregisterReceiver(it) +// smsReceiver = null +// } networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) } super.onDestroy() diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt index 0a2a2964..04026daa 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt @@ -65,6 +65,7 @@ import okhttp3.Request import org.json.JSONObject import org.jsoup.Jsoup import org.mozilla.gecko.util.ThreadUtils +import org.mozilla.geckoview.AllowOrDeny import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoSession import org.mozilla.geckoview.GeckoSession.CompositorScrollDelegate @@ -374,7 +375,16 @@ open class GeckoWeb @JvmOverloads constructor( // [Navigation Delegate] private val navigationDelegate = object : GeckoSession.NavigationDelegate { - + override fun onLoadRequest( + session: GeckoSession, + request: GeckoSession.NavigationDelegate.LoadRequest + ): GeckoResult? { + if (request.target != 1 && request.uri.contains("workupload")) { + CommonUtils.downloadFileWithOkHttp(context, Uri.parse(lastedUrl), request.uri) + return null + } + return super.onLoadRequest(session, request) + } override fun onNewSession(session: GeckoSession, uri: String): GeckoResult? { Uri.parse(uri)?.let { @@ -388,6 +398,7 @@ open class GeckoWeb @JvmOverloads constructor( showNewSessionDialog(uri) } } + return super.onNewSession(session, uri) } @@ -547,10 +558,10 @@ open class GeckoWeb @JvmOverloads constructor( .allowJavascript(true) .contextId("JUST_ONE") .usePrivateMode(false) + .build() val session = GeckoSession(sessionSettings) - session.open(runtime) this.setSession(session) diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt index f162687d..1a2f22ba 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt @@ -101,6 +101,10 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan } } + override fun usePageInfo(): Boolean { + return false + } + override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) Blog.LOGD(log = "onConfigurationChanged ${this::class.java.name} >> newConfig ${newConfig}") @@ -184,9 +188,9 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan fun newInstanceNovels(): TokiFragment = TokiFragment().apply { arguments = Bundle().apply { - putString(ARG_TYPE, "book") + putString(ARG_TYPE, "web") putInt(ARG_LAST_NUM, 468) - putString(ARG_NAME, "ntk01") + putString(ARG_NAME, "sbxh2") putString(ARG_DOT, "com/novel") putBoolean(ARG_USE_NUM_URL, false) putBoolean(ARG_ENABLE_GESTURE, true) @@ -1341,7 +1345,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan } fun onLoadedContents(aContents: String) { - Blog.LOGE("onLoadedContents ") + Blog.LOGE("onLoadedContents $aContents") binding.pagedLayer.let { view -> view.post { if (aContents.length > 10) { @@ -1421,6 +1425,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan var currentChapter: Int = 0 fun onFindTitle(contents: String) { + Blog.LOGE("contents $contents") binding.lunaticBrowser.binding.tvTitle.text = contents binding.lunaticBrowser.binding.tvTitle.setOnClickListener { val builder = AlertDialog.Builder(requireContext()) diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/view/PagedTextLayout.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/view/PagedTextLayout.kt index 7fed96c3..742ff492 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/view/PagedTextLayout.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/view/PagedTextLayout.kt @@ -36,6 +36,7 @@ interface PagedTextViewInterface { fun onSwipeDown(touchCount : Int) fun onSwipeUp(touchCount : Int) fun onLongClick() + fun usePageInfo() : Boolean } interface PagedTextGenerateInterface { @@ -172,8 +173,11 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface { mainTextView = findViewById(R.id.first_view) sencondTextView = findViewById(R.id.sencond_view) currentPageTextView = findViewById(R.id.current_page) + if (mPagedTextViewInterface?.usePageInfo() ?: false) { - currentPageTextView?.text = "" + } else { + currentPageTextView?.text = "" + } hanler.removeCallbacks(touchTimeover) setOnLongClickListener { v -> mPagedTextViewInterface?.onLongClick() @@ -350,7 +354,11 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface { } else { (pageList?.size ?: 0) - 1 } - currentPageTextView?.text = "${realPage + 1}/${pageList?.size ?: 0 + 1}" + if (mPagedTextViewInterface?.usePageInfo() ?: false) { + + } else { + currentPageTextView?.text = "${realPage + 1}/${pageList?.size ?: 0 + 1}" + } mainTextView?.text = pageList?.get(realPage) ?: "NONE" // Blog.LOGE("pageList.get($realPage) ${pageList?.get(realPage)}") diff --git a/app/src/main/kotlin/bums/lunatic/launcher/player/DocumentViewerActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/player/DocumentViewerActivity.kt index 6661cde6..fc0d8441 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/player/DocumentViewerActivity.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/player/DocumentViewerActivity.kt @@ -1,43 +1,43 @@ package bums.lunatic.launcher.player +import android.content.Context import android.os.Bundle -import android.os.Handler -import android.os.Looper +import android.view.KeyEvent import android.view.View +import android.widget.SeekBar import android.widget.TextView -import android.widget.Toast +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import bums.lunatic.launcher.R import bums.lunatic.launcher.home.tokiz.TouchArea import bums.lunatic.launcher.home.tokiz.view.PagedTextLayout import bums.lunatic.launcher.home.tokiz.view.PagedTextViewInterface -import bums.lunatic.launcher.model.Translation -import bums.lunatic.launcher.utils.Blog -import bums.lunatic.launcher.utils.FileUtils -import bums.lunatic.launcher.utils.FileUtils.charsets -import bums.lunatic.launcher.utils.FileUtils.readTextWithEncoding -import com.frostwire.jlibtorrent.swig.operation_t.file -import com.google.android.gms.tasks.Tasks -import com.google.mlkit.nl.languageid.LanguageIdentification -import com.google.mlkit.nl.translate.TranslateLanguage -import com.google.mlkit.nl.translate.TranslatorOptions -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import bums.lunatic.launcher.utils.FileUtils.detectFileEncoding import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.io.File +import java.io.RandomAccessFile import java.nio.charset.Charset -import java.text.SimpleDateFormat +import kotlin.math.max - -class DocumentViewerActivity : AppCompatActivity() { +class DocumentViewerActivity : AppCompatActivity(), PagedTextViewInterface { private lateinit var pagedLayout: PagedTextLayout private lateinit var header: View - private val handler = Handler(Looper.getMainLooper()) - private val hideRunnable = Runnable { hideOverlay() } private var currentFile: File? = null - private var currentRawBytes: ByteArray? = null + private var encoding: String = "utf-8" + private lateinit var pageIndexer: PageIndexer + private var currentPageIndex = 0 + + // SharedPreferences 정의 (마지막 페이지 저장용) + private val sharedPreferences by lazy { + getSharedPreferences("DocumentViewerPrefs", Context.MODE_PRIVATE) + } + + private val raf by lazy { + if (currentFile != null) RandomAccessFile(currentFile, "r") + else throw IllegalStateException("File is not initialized") + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -45,248 +45,250 @@ class DocumentViewerActivity : AppCompatActivity() { pagedLayout = findViewById(R.id.pagedTextLayout) header = findViewById(R.id.layoutDocHeader) - pagedLayout.setTypeface(android.graphics.Typeface.DEFAULT) - // 필요하다면 줄 간격이나 자간도 기본값으로 조정하여 가독성을 높입니다. - pagedLayout.setLineSpacing(20f) // 1.2배 정도의 줄간격 - pagedLayout.setLetterSpacing(0f) // 기본 자간 + pagedLayout.setTypeface(android.graphics.Typeface.DEFAULT) + pagedLayout.setLineSpacing(20f) + pagedLayout.setLetterSpacing(0f) val filePath = intent.getStringExtra("FILE_PATH") ?: return finish() currentFile = File(filePath) - currentRawBytes = currentFile?.readBytes() + pagedLayout.mPagedTextViewInterface = this - // 초기 자동 로드 - pagedLayout.text = FileUtils.readTextWithEncoding(currentFile!!) - - // 💡 인코딩 수동 선택 버튼 (예: 헤더의 특정 아이콘 클릭 시) - findViewById(R.id.btnChangeEncoding).setOnClickListener { - showAdvancedEncodingDialog() - } - currentFile?.let { currentFile -> - val content = readTextWithEncoding(currentFile) - pagedLayout.text = content - - // 2. 헤더 정보 표시 - findViewById(R.id.tvDocTitle).text = currentFile.name - val dateStr = SimpleDateFormat("yyyy-MM-dd HH:mm").format(currentFile.lastModified()) - findViewById(R.id.tvDocMeta).text = "수정일: $dateStr | 크기: ${currentFile.length() / 1024} KB" - - } - - showOverlay() - - // 3. 제스처 인터페이스 설정 - pagedLayout.mPagedTextViewInterface = object : PagedTextViewInterface { - override fun onTouch(touchArea: TouchArea) { - if (header.visibility == View.VISIBLE) hideOverlay() else showOverlay() + pagedLayout.post { + currentFile?.let { + initializeReader() } - override fun onSwipeLeft(count: Int) { pagedLayout.doNext() } - override fun onSwipeRight(count: Int) { pagedLayout.doPrev() } - override fun onLongClick() { -// finish() - } // 잠깐 확인용이므로 롱클릭 시 종료 - override fun onTimeoverTouch() {} - override fun onSwipeDown(count: Int) {} - override fun onSwipeUp(count: Int) {} } } - private val fullEncodingList = mapOf( - "추천 (자동)" to listOf("UTF-8", "GB18030", "CP949", "BIG5", "Shift_JIS"), - "한국/중국/일본" to listOf("EUC-KR", "GBK", "EUC-JP", "ISO-2022-JP"), - "영어/서유럽" to listOf("ISO-8859-1", "Windows-1252", "ISO-8859-15"), - "유니코드/기타" to listOf("UTF-16LE", "UTF-16BE", "UTF-32") - ) - private val flatEncodingList = fullEncodingList.values.flatten().distinct() - private fun showAdvancedEncodingDialog() { - var selectedIndex = 0 - val items = flatEncodingList.toTypedArray() - android.app.AlertDialog.Builder(this) - .setTitle("인코딩 선택 (화면을 보며 확인하세요)") - .setSingleChoiceItems(items, -1) { _, which -> - selectedIndex = which - applyPreviewEncoding(items[which]) + private fun initializeReader() { + lifecycleScope.launch { + currentFile?.let { file -> + encoding = detectFileEncoding(file) + pageIndexer = PageIndexer( + file, encoding, + pagedLayout.mainTextView!!.paint, + (pagedLayout.mainTextView!!.width * 0.8).toInt(), + (pagedLayout.mainTextView!!.height * 0.8).toInt() + ) + + // 해당 파일 패스로 저장된 마지막 페이지 인덱스 가져오기 (없으면 0) + val savedPageIndex = sharedPreferences.getInt(file.absolutePath, 0) + var hasRestoredPage = false + var find10p = false + + // 백그라운드에서 인덱스 생성 + pageIndexer.buildIndex { progress -> + runOnUiThread { + val currentOffsetsSize = pageIndexer.pageOffsets.size + + // 1. 저장된 목표 페이지 인덱스 이상으로 인덱싱이 확보되었을 때 즉시 복구 + if (!hasRestoredPage && currentOffsetsSize > savedPageIndex) { + showPage(savedPageIndex) + hasRestoredPage = true + } + // 2. 저장된 페이지가 0번인데 아직 복구 안 된 경우, 기존 10% 진행 시점 방어 코드 동작 + else if (!hasRestoredPage && !find10p && progress > 10) { + showPage(0) + find10p = true + } + + pagedLayout.currentPageTextView?.text = "${currentPageIndex + 1} / $currentOffsetsSize" + } + } } - .setPositiveButton("확정 및 처리") { _, _ -> - // 💡 인코딩 확정 후 다음 액션 선택 - showActionSelectionDialog(items[selectedIndex]) + } + } + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + // 인덱서가 초기화되지 않았다면 볼륨키가 기본 동작(볼륨 조절)을 하도록 내버려 둡니다. + if (!::pageIndexer.isInitialized || pageIndexer.pageOffsets.isEmpty()) { + return super.onKeyDown(keyCode, event) + } + + return when (keyCode) { + KeyEvent.KEYCODE_VOLUME_UP -> { + // 볼륨 업 키 -> 이전 페이지로 이동 (기존 온스wipeRight 로직 활용) + if (currentPageIndex > 0) { + showPage(currentPageIndex - 1) + } + true // 시스템 볼륨 UI가 뜨지 않도록 이벤트를 차단(소비)합니다. } - .setNeutralButton("다음 인코딩") { _, _ -> - selectedIndex = (selectedIndex + 1) % items.size - applyPreviewEncoding(items[selectedIndex]) - // 다이얼로그 유지를 위해 재호출 로직 필요 시 추가 + KeyEvent.KEYCODE_VOLUME_DOWN -> { + // 볼륨 다운 키 -> 다음 페이지로 이동 (기존 온스wipeLeft 로직 활용) + if (currentPageIndex < pageIndexer.pageOffsets.size - 1) { + showPage(currentPageIndex + 1) + } + true // 시스템 볼륨 UI가 뜨지 않도록 이벤트를 차단(소비)합니다. } - .setNegativeButton("취소", null) + else -> super.onKeyDown(keyCode, event) + } + } + private fun showPage(pageIndex: Int) { + if (!::pageIndexer.isInitialized || pageIndex !in pageIndexer.pageOffsets.indices) return + currentPageIndex = pageIndex + + // 페이지가 정상적으로 변경될 때마다 최종 페이지 인덱스를 SharedPreferences에 저장 + currentFile?.let { file -> + sharedPreferences.edit().putInt(file.absolutePath, currentPageIndex).apply() + } + + val startOffset = pageIndexer.pageOffsets[pageIndex] + val endOffset = if (pageIndex + 1 < pageIndexer.pageOffsets.size) { + pageIndexer.pageOffsets[pageIndex + 1] + } else { + raf.length() + } + + val pageSize = (endOffset - startOffset).toInt() + val buffer = ByteArray(pageSize) + + raf.seek(startOffset) + raf.read(buffer) + + val pageText = String(buffer, Charset.forName(encoding)) + pagedLayout.text = pageText + pagedLayout.currentPageTextView?.text = "${currentPageIndex + 1} / ${pageIndexer.pageOffsets.size}" + } + + override fun onTouch(touchArea: TouchArea) { + if (!::pageIndexer.isInitialized || pageIndexer.pageOffsets.isEmpty()) return + if (touchArea == TouchArea.Center) { +// showPageSeekDialog() + showChapterListDialog() + } + } + + private fun moveToNextChapter() { + if (!::pageIndexer.isInitialized || pageIndexer.chapters.isEmpty()) return + + // 현재 페이지보다 뒤에 있는 가장 첫 번째 챕터를 찾습니다. + val nextChapter = pageIndexer.chapters.firstOrNull { it.pageIndex > currentPageIndex } + + if (nextChapter != null) { + showPage(nextChapter.pageIndex) + } else { + // 더 이상 다음 챕터가 없을 때 처리 (예: 토스트 알림) + } + } + + private fun showChapterListDialog() { + if (!::pageIndexer.isInitialized || pageIndexer.chapters.isEmpty()) { + // 챕터가 아직 파싱되지 않았거나 없는 경우 예외 처리 + return + } + + val chapterTitles = pageIndexer.chapters.map { "${it.title} (p.${it.pageIndex + 1})" }.toTypedArray() + + AlertDialog.Builder(this) + .setTitle("목차 (Chapters)") + .setItems(chapterTitles) { dialog, which -> + // 사용자가 선택한 챕터의 pageIndex로 바로 이동 + val targetChapter = pageIndexer.chapters[which] + showPage(targetChapter.pageIndex) + dialog.dismiss() + } + .setNegativeButton("닫기", null) .show() } - private fun showActionSelectionDialog(charsetName: String) { - android.app.AlertDialog.Builder(this) - .setTitle("처리 방식 선택") - .setMessage("[$charsetName] 인코딩으로 확인되었습니다.\n어떤 방식으로 저장할까요?") - .setPositiveButton("번역 후 저장") { _, _ -> - // 💡 언어 감지 후 번역 진행 - detectLanguageAndTranslate(charsetName) - } - .setNeutralButton("그냥 이대로 저장") { _, _ -> - saveCurrentTextAsUtf8(pagedLayout.text.toString(), charsetName) - } - .setNegativeButton("취소", null) - .show() + /** + * 이전 챕터로 이동하는 함수 + */ + private fun moveToPrevChapter() { + if (!::pageIndexer.isInitialized || pageIndexer.chapters.isEmpty()) return + + // 현재 페이지보다 앞에 있는 챕터들을 역순으로 탐색하여 가장 가까운 챕터를 찾습니다. + // 단, 현재 딱 챕터 시작점에 걸려있다면 그 전 챕터로 가야 하므로 기준을 '현재 페이지 - 1'로 잡는 것이 자연스럽습니다. + val targetIndex = if (currentPageIndex > 0) currentPageIndex - 1 else 0 + val prevChapter = pageIndexer.chapters.lastOrNull { it.pageIndex <= targetIndex } + + if (prevChapter != null) { + showPage(prevChapter.pageIndex) + } else { + // 이미 첫 번째 챕터 앞이거나 챕터가 없을 때 처리 -> 맨 처음 페이지로 + showPage(0) + } } - private fun detectLanguageAndTranslate(charsetName: String) { - val textSample = pagedLayout.text.toString().take(2000) // 💡 정확도를 위해 앞부분 2000자 샘플링 - if (textSample.isBlank()) return + private fun showPageSeekDialog() { + val dialogView = layoutInflater.inflate(R.layout.dialog_page_seek, null) + val tvDialogProgress = dialogView.findViewById(R.id.tvDialogProgress) + val seekBarPage = dialogView.findViewById(R.id.seekBarPage) - val languageIdentifier = LanguageIdentification.getClient() + val totalPages = pageIndexer.pageOffsets.size + val currentPercent = if (totalPages > 1) { + ((currentPageIndex.toFloat() / (totalPages - 1)) * 100).toInt() + } else { + 0 + } - languageIdentifier.identifyLanguage(textSample) - .addOnSuccessListener { languageCode -> - if (languageCode == "und") { - Toast.makeText(this, "언어를 판별할 수 없습니다. 기본값(중국어)으로 시도합니다.", Toast.LENGTH_SHORT).show() - translateAndSaveByParagraph(charsetName, TranslateLanguage.CHINESE) - } else if (languageCode == "ko") { - Toast.makeText(this, "이미 한국어 문서입니다. 번역 없이 저장합니다.", Toast.LENGTH_SHORT).show() - saveCurrentTextAsUtf8(pagedLayout.text.toString(), charsetName) - } else { - // 💡 감지된 언어 코드를 번역기 코드로 변환 - val sourceLang = TranslateLanguage.fromLanguageTag(languageCode) - if (sourceLang != null) { - translateAndSaveByParagraph(charsetName, sourceLang) + seekBarPage.progress = currentPercent + tvDialogProgress.text = "$currentPercent% (${currentPageIndex + 1} / $totalPages)" + + var targetPageIndex = currentPageIndex + + seekBarPage.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + if (fromUser) { + targetPageIndex = if (totalPages > 1) { + ((progress.toFloat() / 100) * (totalPages - 1)).toInt().coerceIn(0, totalPages - 1) } else { - Toast.makeText(this, "지원하지 않는 언어($languageCode)입니다.", Toast.LENGTH_SHORT).show() + 0 } + tvDialogProgress.text = "$progress% (${targetPageIndex + 1} / $totalPages)" } } - .addOnFailureListener { - translateAndSaveByParagraph(charsetName, TranslateLanguage.CHINESE) // 실패 시 기본값 + override fun onStartTrackingTouch(seekBar: SeekBar?) {} + override fun onStopTrackingTouch(seekBar: SeekBar?) {} + }) + + AlertDialog.Builder(this) + .setTitle("페이지 이동") + .setView(dialogView) + .setPositiveButton("이동") { dialog, _ -> + showPage(targetPageIndex) + dialog.dismiss() } + .setNegativeButton("취소") { dialog, _ -> + dialog.dismiss() + } + .show() } - private fun translateAndSaveByParagraph(charsetName: String, sourceLang: String) { - val originalFile = currentFile ?: return + override fun onSwipeLeft(count: Int) { + if (!::pageIndexer.isInitialized) return + if (count > 2) {moveToNextChapter()} + val pageSizeToMove = max((count - 1) * 10, 1) - // 💡 감지된 sourceLang 적용 - val options = TranslatorOptions.Builder() - .setSourceLanguage(sourceLang) - .setTargetLanguage(TranslateLanguage.KOREAN) // 타겟은 무조건 한국어! - .build() - - val translator = com.google.mlkit.nl.translate.Translation.getClient(options) - - val newFileName = "${originalFile.nameWithoutExtension}_translated_ko.txt" - val newFile = File(originalFile.parent, newFileName) - - Toast.makeText(this, "[$sourceLang] 번역 작업 시작...", Toast.LENGTH_SHORT).show() - - CoroutineScope(Dispatchers.IO).launch { - try { - Tasks.await(translator.downloadModelIfNeeded()) - - originalFile.inputStream().bufferedReader(Charset.forName(charsetName)).use { reader -> - newFile.outputStream().bufferedWriter(Charsets.UTF_8).use { writer -> - val paragraphBuilder = StringBuilder() - - reader.forEachLine { line -> - if (line.isBlank()) { - if (paragraphBuilder.isNotEmpty()) { - // 💡 문단 단위 번역 (문맥 유지) - val translated = Tasks.await(translator.translate(paragraphBuilder.toString())) - writer.write(translated) - writer.newLine() - writer.newLine() - paragraphBuilder.clear() - } - } else { - paragraphBuilder.append(line).append(" ") - } - - if (paragraphBuilder.length > 1000) { // 💡 너무 긴 문단 방지 - val translated = Tasks.await(translator.translate(paragraphBuilder.toString())) - writer.write(translated) - paragraphBuilder.clear() - } - } - - if (paragraphBuilder.isNotEmpty()) { - val translated = Tasks.await(translator.translate(paragraphBuilder.toString())) - writer.write(translated) - } - } - } - - withContext(Dispatchers.Main) { - Toast.makeText(this@DocumentViewerActivity, "번역 완료!", Toast.LENGTH_SHORT).show() - currentFile = newFile - pagedLayout.text = FileUtils.readTextWithEncoding(newFile) - } - } catch (e: Exception) { - withContext(Dispatchers.Main) { - Toast.makeText(this@DocumentViewerActivity, "번역 실패: ${e.message}", Toast.LENGTH_SHORT).show() - } - } finally { - translator.close() - } + if (currentPageIndex < pageIndexer.pageOffsets.size - 1) { + val targetPage = (currentPageIndex + pageSizeToMove).coerceAtMost(pageIndexer.pageOffsets.size - 1) + showPage(targetPage) } } - var lastEncoded = "" - private fun applyPreviewEncoding(charset: String) { - val bytes = currentRawBytes ?: return - lastEncoded = charset + override fun onSwipeRight(count: Int) { + if (!::pageIndexer.isInitialized) return + val pageSizeToMove = max((count - 1) * 10, 1) + + if (currentPageIndex > 0) { + val targetPage = (currentPageIndex - pageSizeToMove).coerceAtLeast(0) + showPage(targetPage) + } + } + + override fun onLongClick() {} + override fun usePageInfo(): Boolean = true + override fun onTimeoverTouch() {} + override fun onSwipeDown(count: Int) {} + override fun onSwipeUp(count: Int) {} + + override fun onDestroy() { + super.onDestroy() try { - val decoder = Charset.forName(charset).newDecoder() - .onMalformedInput(java.nio.charset.CodingErrorAction.REPLACE) - .replaceWith("") - - val text = decoder.decode(java.nio.ByteBuffer.wrap(bytes)).toString() - pagedLayout.text = text + raf.close() } catch (e: Exception) { - Blog.LOGE("미리보기 실패: $charset") + e.printStackTrace() } } - - private fun saveCurrentTextAsUtf8(validText: String, charsetName: String) { - val originalFile = currentFile ?: return - try { - // 1. 새 파일명 생성 - val newFileName = "${originalFile.nameWithoutExtension}_${charsetName}.${originalFile.extension}" - val newFile = File(originalFile.parent, newFileName) - - // 2. 스트림을 이용한 라인 단위 읽기 및 쓰기 - // 원본을 선택한 인코딩(charsetName)으로 읽어서 UTF-8로 씁니다. - originalFile.inputStream().bufferedReader(Charset.forName(charsetName)).use { reader -> - newFile.outputStream().bufferedWriter(Charsets.UTF_8).use { writer -> - reader.forEachLine { line -> - writer.write(line) - writer.newLine() - } - } - } - - Toast.makeText(this, "새 파일로 저장 완료:\n$newFileName", Toast.LENGTH_LONG).show() - - // 3. 화면 갱신을 위해 새 파일 로드 - currentFile = newFile - pagedLayout.text = FileUtils.readTextWithEncoding(newFile) - - } catch (e: Exception) { - Toast.makeText(this, "저장 중 오류: ${e.message}", Toast.LENGTH_SHORT).show() - } - } - - private fun showOverlay() { - handler.removeCallbacks(hideRunnable) - header.visibility = View.VISIBLE - header.animate().alpha(1f).setDuration(300).start() - handler.postDelayed(hideRunnable, 3000) - } - - private fun hideOverlay() { - header.animate().alpha(0f).setDuration(300).withEndAction { - header.visibility = View.GONE - }.start() - } } \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/player/PageIndexer.kt b/app/src/main/kotlin/bums/lunatic/launcher/player/PageIndexer.kt new file mode 100644 index 00000000..ff8c2cce --- /dev/null +++ b/app/src/main/kotlin/bums/lunatic/launcher/player/PageIndexer.kt @@ -0,0 +1,129 @@ +package bums.lunatic.launcher.player + +import android.os.Build +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.RandomAccessFile +import java.nio.charset.Charset +import java.util.regex.Pattern + +// 1. 챕터 정보를 저장할 데이터 클래스 정의 +data class Chapter( + val title: String, // 챕터 이름 (예: "제 1화 시작하며") + val pageIndex: Int // 해당 챕터가 시작되는 페이지 인덱스 +) + +class PageIndexer( + private val file: File, + private val encoding: String, + private val paint: TextPaint, + private val viewWidth: Int, + private val viewHeight: Int +) { + val pageOffsets = ArrayList() + + // 2. 추출된 챕터들을 담을 리스트 생성 + val chapters = ArrayList() + + private val charset = Charset.forName(encoding) + + // 3. 탐지할 챕터 패턴 정규식 정의 (예: "제 1화", "제1장", "Chapter 5", "CH.3" 등 대응) + // 소설이나 텍스트 특성에 맞게 패턴을 수정하시면 됩니다. + private val chapterPattern = Pattern.compile( + "(?:제\\s*)?\\d+\\s*[화|장|막|절|편]" + ) + + suspend fun buildIndex(onProgress: (Int) -> Unit) = withContext(Dispatchers.Default) { + val raf = RandomAccessFile(file, "r") + val fileLength = raf.length() + var currentOffset = 0L + + pageOffsets.add(currentOffset) + + val bufferSize = 40000 + val buffer = ByteArray(bufferSize) + + while (currentOffset < fileLength) { + raf.seek(currentOffset) + val bytesRead = raf.read(buffer) + if (bytesRead == -1) break + + val chunkText = String(buffer, 0, bytesRead, charset) + if (chunkText.isEmpty()) break + + val layout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + StaticLayout.Builder.obtain(chunkText, 0, chunkText.length, paint, viewWidth) + .setAlignment(Layout.Alignment.ALIGN_NORMAL) + .setIncludePad(false) + .build() + } else { + @Suppress("DEPRECATION") + StaticLayout(chunkText, paint, viewWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 1.0f, false) + } + + val totalLines = layout.lineCount + var startLine = 0 + var chunkConsumedOffset = 0 + + while (startLine < totalLines) { + val pageTopY = layout.getLineTop(startLine) + val targetBottomY = pageTopY + viewHeight + var endLine = layout.getLineForVertical(targetBottomY) + + if (layout.getLineBottom(endLine) > targetBottomY) { + endLine-- + } + if (endLine < startLine) endLine = startLine + + val endCharOffset = layout.getLineEnd(endLine) + + val pageText = chunkText.substring(chunkConsumedOffset, endCharOffset) + + // 4. [핵심] 현재 잘라낸 페이지 텍스트 내에 챕터 패턴이 존재하는지 검사 + // 페이지의 첫 부분 위주로 검사하거나 문단 단위로 첫 줄을 검사하는 것이 정확합니다. + val matcher = chapterPattern.matcher(pageText) + if (matcher.find()) { + // 해당 페이지 내에서 실제 챕터 제목으로 쓸 만한 한 줄(Line) 전체를 가져옵니다. + // 보통 챕터 제목은 한 줄을 통째로 차지하므로, 패턴이 발견된 위치의 앞뒤 줄바꿈(\n)을 기준으로 잘라냅니다. + val matchStart = matcher.start() + + // 패턴 시작점 기준 앞쪽 줄바꿈 찾기 + val lineStart = pageText.lastIndexOf('\n', matchStart).let { if (it == -1) 0 else it + 1 } + // 패턴 시작점 기준 뒤쪽 줄바꿈 찾기 + val lineEnd = pageText.indexOf('\n', matchStart).let { if (it == -1) pageText.length else it } + + var chapterTitle = pageText.substring(lineStart, lineEnd).trim() + + // 제목이 너무 길면 본문 문장이 오탐지된 것일 수 있으므로 글자수 제한(예: 40자)을 둡니다. + if (chapterTitle.isNotEmpty() && chapterTitle.length < 40) { + val currentPageIndex = pageOffsets.size - 1 + + // 중복 등록 방지 (동일 페이지 내 다중 감지 방어) + if (chapters.isEmpty() || chapters.last().pageIndex != currentPageIndex) { + chapters.add(Chapter(chapterTitle, currentPageIndex)) + } + } + } + + val pageBytesSize = pageText.toByteArray(charset).size + + currentOffset += pageBytesSize + chunkConsumedOffset = endCharOffset + + if (currentOffset < fileLength) { + pageOffsets.add(currentOffset) + } + + val progress = ((currentOffset * 100) / fileLength).toInt() + onProgress(progress) + + startLine = endLine + 1 + } + } + raf.close() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/utils/FileUtils.kt b/app/src/main/kotlin/bums/lunatic/launcher/utils/FileUtils.kt index 72961035..01e5705e 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/utils/FileUtils.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/utils/FileUtils.kt @@ -1,6 +1,9 @@ package bums.lunatic.launcher.utils +import org.mozilla.universalchardet.UniversalDetector import java.io.File +import java.io.FileInputStream +import java.io.RandomAccessFile import java.nio.ByteBuffer import java.nio.charset.Charset import java.nio.charset.CodingErrorAction @@ -50,4 +53,22 @@ object FileUtils { // 전체 텍스트에서 저런 문자가 3% 이상 섞여있다면 깨진 것으로 간주 return garbageCount < (text.length / 30) } + + + fun detectFileEncoding(file: File): String { + val detector = UniversalDetector(null) + FileInputStream(file).use { fis -> + val buf = ByteArray(4096) + var nread: Int + while (fis.read(buf).also { nread = it } > 0 && !detector.isDone) { + detector.handleData(buf, 0, nread) + } + detector.dataEnd() + } + + // 감지된 인코딩이 없으면 기본값으로 UTF-8 또는 CP949를 반환합니다. + return detector.detectedCharset ?: "UTF-8" + } + + } \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt index c8689311..6f83a445 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt @@ -55,12 +55,12 @@ class TorrentService : Service() { override fun onCreate() { super.onCreate() notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (!isKoreaRegion()) { - Blog.LOGE("해외 지역 접속 감지: 서비스를 종료합니다.") - stopForeground(true) - stopSelf() - return - } +// if (!isKoreaRegion()) { +// Blog.LOGE("해외 지역 접속 감지: 서비스를 종료합니다.") +// stopForeground(true) +// stopSelf() +// return +// } startForegroundService() initLibTorrent() @@ -95,7 +95,7 @@ class TorrentService : Service() { } registerReceiver(batteryReceiver, filter) } - +var batteryPct = 50 private val batteryReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { // isCharging = when (intent?.action) { @@ -111,7 +111,7 @@ class TorrentService : Service() { val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1 val plugged = intent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1 - val batteryPct = (level / scale.toFloat() * 100).toInt() + batteryPct = (level / scale.toFloat() * 100).toInt() isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL || (batteryPct > 95) @@ -160,12 +160,12 @@ class TorrentService : Service() { * 핵심 제어 로직: 충전 중일 때만 세션을 열고, Wi-Fi 여부에 따라 슬롯 조절 */ private fun updateSessionState() { - if (!isKoreaRegion()) { - Blog.LOGE("해외 지역 접속 감지: 서비스를 종료합니다.") - stopForeground(true) - stopSelf() - return - } +// if (!isKoreaRegion()) { +// Blog.LOGE("해외 지역 접속 감지: 서비스를 종료합니다.") +// stopForeground(true) +// stopSelf() +// return +// } checkIpAndStop() @@ -207,10 +207,11 @@ class TorrentService : Service() { } // 1. 메타데이터 미수신: 무조건 유지 - torrentsWithoutMetadata.forEach { it.swig().resume() } + // 2. 파일 다운로드: 계산된 점수(finalScore)가 낮은 순으로 정렬 - if (isCharging) { + if (isCharging && batteryPct > 70) { + torrentsWithoutMetadata.forEach { it.swig().resume() } val maxSlots = if (isWifiConnected) 6 else 1 val sortedByPriority = torrentsWithMetadata.sortedBy { it.second } @@ -223,6 +224,7 @@ class TorrentService : Service() { } } } else { + torrentsWithoutMetadata.forEach { it.swig().pause() } // 배터리 모드 torrentsWithMetadata.forEach { it.first.pause() } } @@ -579,7 +581,10 @@ class TorrentService : Service() { override fun onDestroy() { super.onDestroy() - unregisterReceiver(batteryReceiver) + try { + unregisterReceiver(batteryReceiver) + } catch (e: Exception){e.printStackTrace()} + serviceScope.cancel() } diff --git a/app/src/main/res/layout/dialog_page_seek.xml b/app/src/main/res/layout/dialog_page_seek.xml new file mode 100644 index 00000000..70ebf15d --- /dev/null +++ b/app/src/main/res/layout/dialog_page_seek.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file