This commit is contained in:
lunaticbum 2026-05-20 17:57:56 +09:00
parent 257ddb6776
commit 378f6495be
14 changed files with 553 additions and 349 deletions

View File

@ -177,6 +177,8 @@ dependencies {
// implementation 'com.vladsch.flexmark:flexmark-all:0.64.8' // implementation 'com.vladsch.flexmark:flexmark-all:0.64.8'
// implementation("org.opencv:opencv-android:4.11.0") // implementation("org.opencv:opencv-android:4.11.0")
// build.gradle에 추가 // 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("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// implementation ("com.github.aeonSolutions:FloatingActionButtonMenuDrag:1.1") // implementation ("com.github.aeonSolutions:FloatingActionButtonMenuDrag:1.1")
implementation("io.github.junkfood02.youtubedl-android:library:0.17.4") implementation("io.github.junkfood02.youtubedl-android:library:0.17.4")

View File

@ -132,7 +132,7 @@
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize" android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:excludeFromRecents="true" android:excludeFromRecents="false"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:exported="false"> android:exported="false">
</activity> </activity>

View File

@ -1,9 +1,9 @@
(function() { //(function() {
var meta = document.querySelector('meta[name=viewport]'); // var meta = document.querySelector('meta[name=viewport]');
if (!meta) { // if (!meta) {
meta = document.createElement('meta'); // meta = document.createElement('meta');
meta.name = 'viewport'; // meta.name = 'viewport';
document.head.appendChild(meta); // document.head.appendChild(meta);
} // }
meta.setAttribute('content', 'width=device-width, initial-scale=1.0'); // meta.setAttribute('content', 'width=device-width, initial-scale=1.0');
})(); //})();

View File

@ -13,7 +13,7 @@
}, },
"content_scripts": [ "content_scripts": [
{ {
"run_at": "document_end", "run_at": "document_start",
"matches": ["<all_urls>"], "matches": ["<all_urls>"],
"js": ["messaging.js","inject-viewport.js"] "js": ["messaging.js","inject-viewport.js"]
} }

View File

@ -1,5 +1,6 @@
function pubDateNumber(tdateTime) { function pubDateNumber(tdateTime) {
let date = new Date(); let date = new Date();
let dateTime = date.getTime(); let dateTime = date.getTime();
@ -469,13 +470,9 @@ if(document.querySelector(".list-body") !== null) {
var listBody = null var listBody = null
try {listBody = document.querySelector(".list-body");}catch (e) {} try {listBody = document.querySelector(".list-body");}catch (e) {}
getList(listBody.children) getList(listBody.children)
} else if(document.querySelector(".novel-eps") !== null) { }
//document.querySelector(".novel-eps").children
removeSpecificGifs() if(document.querySelector("#novel_content") !== null){
var listBody = null
try {listBody = document.querySelector(".novel-eps");}catch (e) {}
getList2(listBody.children)
} else if(document.querySelector("#novel_content") !== null){
removeSpecificGifs() removeSpecificGifs()
var title = null var title = null
var contents = 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")) { if(document.querySelector("#html_encoder_div")) {
sendMessage( sendMessage(
{ {
@ -814,7 +796,9 @@ document.addEventListener('DOMContentLoaded', function () {
if (document.readyState === 'complete') { if (document.readyState === 'complete') {
sendCookiesToNative(); sendCookiesToNative();
try{removeSpecificGifs();}catch(e){}
} else { } else {
// 2. 아직 로딩 중이라면 window.onload(모든 리소스 로드 완료) 시점에 실행 // 2. 아직 로딩 중이라면 window.onload(모든 리소스 로드 완료) 시점에 실행
window.addEventListener('load', sendCookiesToNative); window.addEventListener('load', sendCookiesToNative);
} }
@ -866,21 +850,30 @@ function scrollToEndAndExtract() {
function step() { function step() {
const prevScrollY = window.scrollY; const prevScrollY = window.scrollY;
window.scrollBy(0, scrollStep); window.scrollBy(0, scrollStep);
console.log("ONSTEPS");
// 💡 0.5초 대기 후 현재 위치가 이전과 같다면(끝에 도달) 추출 시작 // 💡 0.5초 대기 후 현재 위치가 이전과 같다면(끝에 도달) 추출 시작
setTimeout(() => { setTimeout(() => {
if (window.scrollY === prevScrollY || if (window.scrollY === prevScrollY ||
(window.innerHeight + window.scrollY) >= document.body.scrollHeight) { (window.innerHeight + window.scrollY) >= document.body.scrollHeight) {
if (location.host.includes("subtitlecat.com")) {
console.log("✅ 페이지 끝 도달 - 자막 리스트 추출 시작"); console.log("✅ 페이지 끝 도달 - 자막 리스트 추출 시작");
extractSubtitleList(); // 기존에 정의한 추출 함수 호출 extractSubtitleList(); // 기존에 정의한 추출 함수 호출
} else {
}
} else { } else {
step(); // 아직 끝이 아니면 다음 스크롤 진행 step(); // 아직 끝이 아니면 다음 스크롤 진행
} }
}, scrollDelay); }, scrollDelay);
} }
step(); step()
} else {
console.log(`is FAIL NOVEL ${location.href.includes("/novel/")}`);
} }
} }

View File

@ -580,10 +580,10 @@ open class LauncherActivity : CommonActivity() {
} }
}) })
requestSmsPermissionLauncher.launch(arrayOf( // requestSmsPermissionLauncher.launch(arrayOf(
android.Manifest.permission.RECEIVE_SMS, // android.Manifest.permission.RECEIVE_SMS,
android.Manifest.permission.READ_SMS // android.Manifest.permission.READ_SMS
)) // ))
handleSharedIntent(intent) handleSharedIntent(intent)
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@ -614,7 +614,9 @@ open class LauncherActivity : CommonActivity() {
putExtra("WIFI_STATE", isWifiConnected) putExtra("WIFI_STATE", isWifiConnected)
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
startForegroundService(intent) startForegroundService(intent)
} catch (e: Exception){e.printStackTrace()}
} else { } else {
startService(intent) startService(intent)
} }
@ -634,35 +636,35 @@ open class LauncherActivity : CommonActivity() {
} }
connectivityManager.registerNetworkCallback(request, networkCallback!!) connectivityManager.registerNetworkCallback(request, networkCallback!!)
} }
private var smsReceiver: SmsReceiver? = null // private var smsReceiver: SmsReceiver? = null
// 권한 요청 결과 처리기 // 권한 요청 결과 처리기
private val requestSmsPermissionLauncher = registerForActivityResult( // private val requestSmsPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions() // ActivityResultContracts.RequestMultiplePermissions()
) { permissions -> // ) { permissions ->
val granted = permissions[android.Manifest.permission.RECEIVE_SMS] ?: false // val granted = permissions[android.Manifest.permission.RECEIVE_SMS] ?: false
if (granted) { // if (granted) {
Blog.LOGE("SMS 권한 승인됨 -> 리시버 동적 등록 실행") // Blog.LOGE("SMS 권한 승인됨 -> 리시버 동적 등록 실행")
registerSmsDynamicReceiver() // registerSmsDynamicReceiver()
} // }
} // }
//
private fun registerSmsDynamicReceiver() { // private fun registerSmsDynamicReceiver() {
if (smsReceiver == null) { // if (smsReceiver == null) {
smsReceiver = SmsReceiver() // smsReceiver = SmsReceiver()
val filter = IntentFilter("android.provider.Telephony.SMS_RECEIVED").apply { // val filter = IntentFilter("android.provider.Telephony.SMS_RECEIVED").apply {
priority = 2147483647 // 시스템 최우선 순위 // priority = 2147483647 // 시스템 최우선 순위
} // }
//
// Android 14 이상(안드 16 포함) 필수 플래그: RECEIVER_EXPORTED // // Android 14 이상(안드 16 포함) 필수 플래그: RECEIVER_EXPORTED
// 외부 앱(시스템 타워)으로부터 브로드캐스트를 받으려면 필수입니다. // // 외부 앱(시스템 타워)으로부터 브로드캐스트를 받으려면 필수입니다.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(smsReceiver, filter, Context.RECEIVER_EXPORTED) // registerReceiver(smsReceiver, filter, Context.RECEIVER_EXPORTED)
} else { // } else {
registerReceiver(smsReceiver, filter) // registerReceiver(smsReceiver, filter)
} // }
} // }
} // }
private fun updateWidgetOptions(appWidgetId: Int, hostView: AppWidgetHostView) { private fun updateWidgetOptions(appWidgetId: Int, hostView: AppWidgetHostView) {
val width = hostView.layoutParams.width val width = hostView.layoutParams.width
@ -966,10 +968,10 @@ open class LauncherActivity : CommonActivity() {
private lateinit var connectivityManager: ConnectivityManager private lateinit var connectivityManager: ConnectivityManager
private var networkCallback: ConnectivityManager.NetworkCallback? = null private var networkCallback: ConnectivityManager.NetworkCallback? = null
override fun onDestroy() { override fun onDestroy() {
smsReceiver?.let { // smsReceiver?.let {
unregisterReceiver(it) // unregisterReceiver(it)
smsReceiver = null // smsReceiver = null
} // }
networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) } networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) }
super.onDestroy() super.onDestroy()

View File

@ -65,6 +65,7 @@ import okhttp3.Request
import org.json.JSONObject import org.json.JSONObject
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.mozilla.gecko.util.ThreadUtils import org.mozilla.gecko.util.ThreadUtils
import org.mozilla.geckoview.AllowOrDeny
import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoSession import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoSession.CompositorScrollDelegate import org.mozilla.geckoview.GeckoSession.CompositorScrollDelegate
@ -374,7 +375,16 @@ open class GeckoWeb @JvmOverloads constructor(
// [Navigation Delegate] // [Navigation Delegate]
private val navigationDelegate = object : GeckoSession.NavigationDelegate { private val navigationDelegate = object : GeckoSession.NavigationDelegate {
override fun onLoadRequest(
session: GeckoSession,
request: GeckoSession.NavigationDelegate.LoadRequest
): GeckoResult<AllowOrDeny?>? {
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<GeckoSession>? { override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? {
Uri.parse(uri)?.let { Uri.parse(uri)?.let {
@ -388,6 +398,7 @@ open class GeckoWeb @JvmOverloads constructor(
showNewSessionDialog(uri) showNewSessionDialog(uri)
} }
} }
return super.onNewSession(session, uri) return super.onNewSession(session, uri)
} }
@ -547,10 +558,10 @@ open class GeckoWeb @JvmOverloads constructor(
.allowJavascript(true) .allowJavascript(true)
.contextId("JUST_ONE") .contextId("JUST_ONE")
.usePrivateMode(false) .usePrivateMode(false)
.build() .build()
val session = GeckoSession(sessionSettings) val session = GeckoSession(sessionSettings)
session.open(runtime) session.open(runtime)
this.setSession(session) this.setSession(session)

View File

@ -101,6 +101,10 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
} }
} }
override fun usePageInfo(): Boolean {
return false
}
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
Blog.LOGD(log = "onConfigurationChanged ${this::class.java.name} >> newConfig ${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 { fun newInstanceNovels(): TokiFragment = TokiFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putString(ARG_TYPE, "book") putString(ARG_TYPE, "web")
putInt(ARG_LAST_NUM, 468) putInt(ARG_LAST_NUM, 468)
putString(ARG_NAME, "ntk01") putString(ARG_NAME, "sbxh2")
putString(ARG_DOT, "com/novel") putString(ARG_DOT, "com/novel")
putBoolean(ARG_USE_NUM_URL, false) putBoolean(ARG_USE_NUM_URL, false)
putBoolean(ARG_ENABLE_GESTURE, true) putBoolean(ARG_ENABLE_GESTURE, true)
@ -1341,7 +1345,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
} }
fun onLoadedContents(aContents: String) { fun onLoadedContents(aContents: String) {
Blog.LOGE("onLoadedContents ") Blog.LOGE("onLoadedContents $aContents")
binding.pagedLayer.let { view -> binding.pagedLayer.let { view ->
view.post { view.post {
if (aContents.length > 10) { if (aContents.length > 10) {
@ -1421,6 +1425,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
var currentChapter: Int = 0 var currentChapter: Int = 0
fun onFindTitle(contents: String) { fun onFindTitle(contents: String) {
Blog.LOGE("contents $contents")
binding.lunaticBrowser.binding.tvTitle.text = contents binding.lunaticBrowser.binding.tvTitle.text = contents
binding.lunaticBrowser.binding.tvTitle.setOnClickListener { binding.lunaticBrowser.binding.tvTitle.setOnClickListener {
val builder = AlertDialog.Builder(requireContext()) val builder = AlertDialog.Builder(requireContext())

View File

@ -36,6 +36,7 @@ interface PagedTextViewInterface {
fun onSwipeDown(touchCount : Int) fun onSwipeDown(touchCount : Int)
fun onSwipeUp(touchCount : Int) fun onSwipeUp(touchCount : Int)
fun onLongClick() fun onLongClick()
fun usePageInfo() : Boolean
} }
interface PagedTextGenerateInterface { interface PagedTextGenerateInterface {
@ -172,8 +173,11 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
mainTextView = findViewById(R.id.first_view) mainTextView = findViewById(R.id.first_view)
sencondTextView = findViewById(R.id.sencond_view) sencondTextView = findViewById(R.id.sencond_view)
currentPageTextView = findViewById(R.id.current_page) currentPageTextView = findViewById(R.id.current_page)
if (mPagedTextViewInterface?.usePageInfo() ?: false) {
} else {
currentPageTextView?.text = "" currentPageTextView?.text = ""
}
hanler.removeCallbacks(touchTimeover) hanler.removeCallbacks(touchTimeover)
setOnLongClickListener { v -> setOnLongClickListener { v ->
mPagedTextViewInterface?.onLongClick() mPagedTextViewInterface?.onLongClick()
@ -350,7 +354,11 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
} else { } else {
(pageList?.size ?: 0) - 1 (pageList?.size ?: 0) - 1
} }
if (mPagedTextViewInterface?.usePageInfo() ?: false) {
} else {
currentPageTextView?.text = "${realPage + 1}/${pageList?.size ?: 0 + 1}" currentPageTextView?.text = "${realPage + 1}/${pageList?.size ?: 0 + 1}"
}
mainTextView?.text = pageList?.get(realPage) ?: "NONE" mainTextView?.text = pageList?.get(realPage) ?: "NONE"
// Blog.LOGE("pageList.get($realPage) ${pageList?.get(realPage)}") // Blog.LOGE("pageList.get($realPage) ${pageList?.get(realPage)}")

View File

@ -1,43 +1,43 @@
package bums.lunatic.launcher.player package bums.lunatic.launcher.player
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.view.KeyEvent
import android.os.Looper
import android.view.View import android.view.View
import android.widget.SeekBar
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import bums.lunatic.launcher.R import bums.lunatic.launcher.R
import bums.lunatic.launcher.home.tokiz.TouchArea import bums.lunatic.launcher.home.tokiz.TouchArea
import bums.lunatic.launcher.home.tokiz.view.PagedTextLayout import bums.lunatic.launcher.home.tokiz.view.PagedTextLayout
import bums.lunatic.launcher.home.tokiz.view.PagedTextViewInterface import bums.lunatic.launcher.home.tokiz.view.PagedTextViewInterface
import bums.lunatic.launcher.model.Translation import bums.lunatic.launcher.utils.FileUtils.detectFileEncoding
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 kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.RandomAccessFile
import java.nio.charset.Charset import java.nio.charset.Charset
import java.text.SimpleDateFormat import kotlin.math.max
class DocumentViewerActivity : AppCompatActivity(), PagedTextViewInterface {
class DocumentViewerActivity : AppCompatActivity() {
private lateinit var pagedLayout: PagedTextLayout private lateinit var pagedLayout: PagedTextLayout
private lateinit var header: View private lateinit var header: View
private val handler = Handler(Looper.getMainLooper())
private val hideRunnable = Runnable { hideOverlay() }
private var currentFile: File? = null 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -45,248 +45,250 @@ class DocumentViewerActivity : AppCompatActivity() {
pagedLayout = findViewById(R.id.pagedTextLayout) pagedLayout = findViewById(R.id.pagedTextLayout)
header = findViewById(R.id.layoutDocHeader) header = findViewById(R.id.layoutDocHeader)
pagedLayout.setTypeface(android.graphics.Typeface.DEFAULT)
// 필요하다면 줄 간격이나 자간도 기본값으로 조정하여 가독성을 높입니다. pagedLayout.setTypeface(android.graphics.Typeface.DEFAULT)
pagedLayout.setLineSpacing(20f) // 1.2배 정도의 줄간격 pagedLayout.setLineSpacing(20f)
pagedLayout.setLetterSpacing(0f) // 기본 자간 pagedLayout.setLetterSpacing(0f)
val filePath = intent.getStringExtra("FILE_PATH") ?: return finish() val filePath = intent.getStringExtra("FILE_PATH") ?: return finish()
currentFile = File(filePath) currentFile = File(filePath)
currentRawBytes = currentFile?.readBytes() pagedLayout.mPagedTextViewInterface = this
// 초기 자동 로드 pagedLayout.post {
pagedLayout.text = FileUtils.readTextWithEncoding(currentFile!!) currentFile?.let {
initializeReader()
// 💡 인코딩 수동 선택 버튼 (예: 헤더의 특정 아이콘 클릭 시) }
findViewById<View>(R.id.btnChangeEncoding).setOnClickListener {
showAdvancedEncodingDialog()
} }
currentFile?.let { currentFile ->
val content = readTextWithEncoding(currentFile)
pagedLayout.text = content
// 2. 헤더 정보 표시
findViewById<TextView>(R.id.tvDocTitle).text = currentFile.name
val dateStr = SimpleDateFormat("yyyy-MM-dd HH:mm").format(currentFile.lastModified())
findViewById<TextView>(R.id.tvDocMeta).text = "수정일: $dateStr | 크기: ${currentFile.length() / 1024} KB"
} }
showOverlay() 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"
}
}
}
}
}
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가 뜨지 않도록 이벤트를 차단(소비)합니다.
}
KeyEvent.KEYCODE_VOLUME_DOWN -> {
// 볼륨 다운 키 -> 다음 페이지로 이동 (기존 온스wipeLeft 로직 활용)
if (currentPageIndex < pageIndexer.pageOffsets.size - 1) {
showPage(currentPageIndex + 1)
}
true // 시스템 볼륨 UI가 뜨지 않도록 이벤트를 차단(소비)합니다.
}
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}"
}
// 3. 제스처 인터페이스 설정
pagedLayout.mPagedTextViewInterface = object : PagedTextViewInterface {
override fun onTouch(touchArea: TouchArea) { override fun onTouch(touchArea: TouchArea) {
if (header.visibility == View.VISIBLE) hideOverlay() else showOverlay() if (!::pageIndexer.isInitialized || pageIndexer.pageOffsets.isEmpty()) return
if (touchArea == TouchArea.Center) {
// showPageSeekDialog()
showChapterListDialog()
} }
override fun onSwipeLeft(count: Int) { pagedLayout.doNext() } }
override fun onSwipeRight(count: Int) { pagedLayout.doPrev() }
override fun onLongClick() { private fun moveToNextChapter() {
// finish() 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 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 showPageSeekDialog() {
val dialogView = layoutInflater.inflate(R.layout.dialog_page_seek, null)
val tvDialogProgress = dialogView.findViewById<TextView>(R.id.tvDialogProgress)
val seekBarPage = dialogView.findViewById<SeekBar>(R.id.seekBarPage)
val totalPages = pageIndexer.pageOffsets.size
val currentPercent = if (totalPages > 1) {
((currentPageIndex.toFloat() / (totalPages - 1)) * 100).toInt()
} else {
0
}
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 {
0
}
tvDialogProgress.text = "$progress% (${targetPageIndex + 1} / $totalPages)"
}
}
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()
}
override fun onSwipeLeft(count: Int) {
if (!::pageIndexer.isInitialized) return
if (count > 2) {moveToNextChapter()}
val pageSizeToMove = max((count - 1) * 10, 1)
if (currentPageIndex < pageIndexer.pageOffsets.size - 1) {
val targetPage = (currentPageIndex + pageSizeToMove).coerceAtMost(pageIndexer.pageOffsets.size - 1)
showPage(targetPage)
}
}
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 onTimeoverTouch() {}
override fun onSwipeDown(count: Int) {} override fun onSwipeDown(count: Int) {}
override fun onSwipeUp(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) override fun onDestroy() {
.setTitle("인코딩 선택 (화면을 보며 확인하세요)") super.onDestroy()
.setSingleChoiceItems(items, -1) { _, which ->
selectedIndex = which
applyPreviewEncoding(items[which])
}
.setPositiveButton("확정 및 처리") { _, _ ->
// 💡 인코딩 확정 후 다음 액션 선택
showActionSelectionDialog(items[selectedIndex])
}
.setNeutralButton("다음 인코딩") { _, _ ->
selectedIndex = (selectedIndex + 1) % items.size
applyPreviewEncoding(items[selectedIndex])
// 다이얼로그 유지를 위해 재호출 로직 필요 시 추가
}
.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 detectLanguageAndTranslate(charsetName: String) {
val textSample = pagedLayout.text.toString().take(2000) // 💡 정확도를 위해 앞부분 2000자 샘플링
if (textSample.isBlank()) return
val languageIdentifier = LanguageIdentification.getClient()
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)
} else {
Toast.makeText(this, "지원하지 않는 언어($languageCode)입니다.", Toast.LENGTH_SHORT).show()
}
}
}
.addOnFailureListener {
translateAndSaveByParagraph(charsetName, TranslateLanguage.CHINESE) // 실패 시 기본값
}
}
private fun translateAndSaveByParagraph(charsetName: String, sourceLang: String) {
val originalFile = currentFile ?: return
// 💡 감지된 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 { try {
Tasks.await(translator.downloadModelIfNeeded()) raf.close()
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) { } catch (e: Exception) {
withContext(Dispatchers.Main) { e.printStackTrace()
Toast.makeText(this@DocumentViewerActivity, "번역 실패: ${e.message}", Toast.LENGTH_SHORT).show()
} }
} finally {
translator.close()
}
}
}
var lastEncoded = ""
private fun applyPreviewEncoding(charset: String) {
val bytes = currentRawBytes ?: return
lastEncoded = charset
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
} catch (e: Exception) {
Blog.LOGE("미리보기 실패: $charset")
}
}
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()
} }
} }

View File

@ -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<Long>()
// 2. 추출된 챕터들을 담을 리스트 생성
val chapters = ArrayList<Chapter>()
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()
}
}

View File

@ -1,6 +1,9 @@
package bums.lunatic.launcher.utils package bums.lunatic.launcher.utils
import org.mozilla.universalchardet.UniversalDetector
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.RandomAccessFile
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.charset.Charset import java.nio.charset.Charset
import java.nio.charset.CodingErrorAction import java.nio.charset.CodingErrorAction
@ -50,4 +53,22 @@ object FileUtils {
// 전체 텍스트에서 저런 문자가 3% 이상 섞여있다면 깨진 것으로 간주 // 전체 텍스트에서 저런 문자가 3% 이상 섞여있다면 깨진 것으로 간주
return garbageCount < (text.length / 30) 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"
}
} }

View File

@ -55,12 +55,12 @@ class TorrentService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (!isKoreaRegion()) { // if (!isKoreaRegion()) {
Blog.LOGE("해외 지역 접속 감지: 서비스를 종료합니다.") // Blog.LOGE("해외 지역 접속 감지: 서비스를 종료합니다.")
stopForeground(true) // stopForeground(true)
stopSelf() // stopSelf()
return // return
} // }
startForegroundService() startForegroundService()
initLibTorrent() initLibTorrent()
@ -95,7 +95,7 @@ class TorrentService : Service() {
} }
registerReceiver(batteryReceiver, filter) registerReceiver(batteryReceiver, filter)
} }
var batteryPct = 50
private val batteryReceiver = object : BroadcastReceiver() { private val batteryReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
// isCharging = when (intent?.action) { // isCharging = when (intent?.action) {
@ -111,7 +111,7 @@ class TorrentService : Service() {
val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1 val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
val plugged = intent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -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 || isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL || (batteryPct > 95) status == BatteryManager.BATTERY_STATUS_FULL || (batteryPct > 95)
@ -160,12 +160,12 @@ class TorrentService : Service() {
* 핵심 제어 로직: 충전 중일 때만 세션을 열고, Wi-Fi 여부에 따라 슬롯 조절 * 핵심 제어 로직: 충전 중일 때만 세션을 열고, Wi-Fi 여부에 따라 슬롯 조절
*/ */
private fun updateSessionState() { private fun updateSessionState() {
if (!isKoreaRegion()) { // if (!isKoreaRegion()) {
Blog.LOGE("해외 지역 접속 감지: 서비스를 종료합니다.") // Blog.LOGE("해외 지역 접속 감지: 서비스를 종료합니다.")
stopForeground(true) // stopForeground(true)
stopSelf() // stopSelf()
return // return
} // }
checkIpAndStop() checkIpAndStop()
@ -207,10 +207,11 @@ class TorrentService : Service() {
} }
// 1. 메타데이터 미수신: 무조건 유지 // 1. 메타데이터 미수신: 무조건 유지
torrentsWithoutMetadata.forEach { it.swig().resume() }
// 2. 파일 다운로드: 계산된 점수(finalScore)가 낮은 순으로 정렬 // 2. 파일 다운로드: 계산된 점수(finalScore)가 낮은 순으로 정렬
if (isCharging) { if (isCharging && batteryPct > 70) {
torrentsWithoutMetadata.forEach { it.swig().resume() }
val maxSlots = if (isWifiConnected) 6 else 1 val maxSlots = if (isWifiConnected) 6 else 1
val sortedByPriority = torrentsWithMetadata.sortedBy { it.second } val sortedByPriority = torrentsWithMetadata.sortedBy { it.second }
@ -223,6 +224,7 @@ class TorrentService : Service() {
} }
} }
} else { } else {
torrentsWithoutMetadata.forEach { it.swig().pause() }
// 배터리 모드 // 배터리 모드
torrentsWithMetadata.forEach { it.first.pause() } torrentsWithMetadata.forEach { it.first.pause() }
} }
@ -579,7 +581,10 @@ class TorrentService : Service() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
try {
unregisterReceiver(batteryReceiver) unregisterReceiver(batteryReceiver)
} catch (e: Exception){e.printStackTrace()}
serviceScope.cancel() serviceScope.cancel()
} }

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/tvDialogProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="0% (1 / 100)"
android:textColor="#000000"
android:textSize="18sp"
android:textStyle="bold" />
<SeekBar
android:id="@+id/seekBarPage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:max="100" />
</LinearLayout>