This commit is contained in:
lunaticbum 2026-06-08 14:45:38 +09:00
parent 378f6495be
commit 0400385cf8
7 changed files with 103 additions and 69 deletions

View File

@ -181,7 +181,6 @@ class CompletedFilesFragment : Fragment() {
loadFiles() loadFiles()
} else { } else {
val intentFlags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK val intentFlags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
if (extVideos.contains(file.extension.lowercase())) { if (extVideos.contains(file.extension.lowercase())) {
trackFileAccess(file.name) trackFileAccess(file.name)
loadFiles() loadFiles()
@ -192,6 +191,8 @@ class CompletedFilesFragment : Fragment() {
} }
startActivity(intent) startActivity(intent)
} else if (extImages.contains(file.extension.lowercase())) { } else if (extImages.contains(file.extension.lowercase())) {
trackFileAccess(file.name)
loadFiles()
val intent = Intent(requireContext(), ImageViewerActivity::class.java).apply { val intent = Intent(requireContext(), ImageViewerActivity::class.java).apply {
putExtra("IMAGE_PATH", file.absolutePath) putExtra("IMAGE_PATH", file.absolutePath)
// 💡 이미지 뷰어도 동일하게 적용 // 💡 이미지 뷰어도 동일하게 적용
@ -199,6 +200,8 @@ class CompletedFilesFragment : Fragment() {
} }
startActivity(intent) startActivity(intent)
} else if (extDocs.contains(file.extension.lowercase())) { } else if (extDocs.contains(file.extension.lowercase())) {
trackFileAccess(file.name)
loadFiles()
val intent = Intent(requireContext(), DocumentViewerActivity::class.java).apply { val intent = Intent(requireContext(), DocumentViewerActivity::class.java).apply {
putExtra("FILE_PATH", file.absolutePath) putExtra("FILE_PATH", file.absolutePath)
// 💡 문서 뷰어도 동일하게 적용 // 💡 문서 뷰어도 동일하게 적용

View File

@ -633,7 +633,7 @@ open class NeoRssActivity : CommonActivity() {
// R.id.webtoons -> TokiFragment.newInstanceWebtoons() // R.id.webtoons -> TokiFragment.newInstanceWebtoons()
// R.id.comics -> TokiFragment.newInstanceComics() // R.id.comics -> TokiFragment.newInstanceComics()
R.id.youtube -> TokiFragment.newInstanceYouTube() R.id.youtube -> TokiFragment.newInstanceYouTube()
R.id.perplexity -> TokiFragment.newInstancePerplexity() // R.id.perplexity -> TokiFragment.newInstancePerplexity()
R.id.zzalbang -> BookmarkPagerFragment() R.id.zzalbang -> BookmarkPagerFragment()
R.id.btn_x -> TokiFragment.newInstanceX() R.id.btn_x -> TokiFragment.newInstanceX()
R.id.btn_i -> TokiFragment.newInstanceI() R.id.btn_i -> TokiFragment.newInstanceI()

View File

@ -64,32 +64,24 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
var guideLine : Guideline? = null var guideLine : Guideline? = null
var pageList = mutableListOf<CharSequence>() var pageList = mutableListOf<CharSequence>()
var summaryText : String = "" var summaryText : String = ""
var text : String = "" var text: String = ""
set(new) { set(value) {
field = new field = value
val summary = new.replace(" " ,"").replace("\n" ,"").substring(0,Math.min(30,new.length)) if (value.isNotEmpty()) {
if (summary.equals(summaryText) && summaryText.length > 100) { // 액티비티가 준 한 페이지 분량의 텍스트를 곧바로 자식 텍스트뷰에 바인딩합니다.
mainTextView?.text = value
} else { // 만약 듀얼 페이지 모드를 지원한다면 액티비티에서 2페이지 분량을
if (field.length > 0) { // 각각 나눠서 주입하는 것이 좋으므로, 여기서는 단순 텍스트 셋팅만 수행합니다.
post { if (isDualPage()) {
if (width > 0 && height > 0) { // 필요 시 듀얼 페이지 처리 로직을 액티비티 스펙에 맞게 확장할 수 있습니다.
MainScope().launch { // 현재는 단일 페이지 기반으로 작동하므로 비워두거나 서브 뷰를 비웁니다.
pageList.clear() sencondTextView?.text = ""
val contentWidth =
mainTextView!!.width - (mainTextView!!.paddingLeft + mainTextView!!.paddingRight)
val contentHeight =
mainTextView!!.height - (mainTextView!!.paddingTop + mainTextView!!.paddingBottom)
val pages = paginateAsync(cleanText(text), contentWidth, contentHeight)
pageList.addAll(pages.map { it.trim() })
Blog.LOGE("pages >>> ${pages.size}")
setPageBy(0)
}
}
}
} }
} else {
mainTextView?.text = ""
sencondTextView?.text = ""
} }
summaryText = summary
} }
private fun cleanText(text: String): String { private fun cleanText(text: String): String {

View File

@ -6,6 +6,7 @@ import android.view.KeyEvent
import android.view.View import android.view.View
import android.widget.SeekBar 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.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -107,14 +108,15 @@ class DocumentViewerActivity : AppCompatActivity(), PagedTextViewInterface {
} }
return when (keyCode) { return when (keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_DPAD_LEFT,KeyEvent.KEYCODE_VOLUME_UP -> {
// 볼륨 업 키 -> 이전 페이지로 이동 (기존 온스wipeRight 로직 활용) // 볼륨 업 키 -> 이전 페이지로 이동 (기존 온스wipeRight 로직 활용)
if (currentPageIndex > 0) { if (currentPageIndex > 0) {
showPage(currentPageIndex - 1) showPage(currentPageIndex - 1)
} }
true // 시스템 볼륨 UI가 뜨지 않도록 이벤트를 차단(소비)합니다. true // 시스템 볼륨 UI가 뜨지 않도록 이벤트를 차단(소비)합니다.
} }
KeyEvent.KEYCODE_VOLUME_DOWN -> {
KeyEvent.KEYCODE_DPAD_RIGHT,KeyEvent.KEYCODE_VOLUME_DOWN -> {
// 볼륨 다운 키 -> 다음 페이지로 이동 (기존 온스wipeLeft 로직 활용) // 볼륨 다운 키 -> 다음 페이지로 이동 (기존 온스wipeLeft 로직 활용)
if (currentPageIndex < pageIndexer.pageOffsets.size - 1) { if (currentPageIndex < pageIndexer.pageOffsets.size - 1) {
showPage(currentPageIndex + 1) showPage(currentPageIndex + 1)
@ -174,22 +176,45 @@ class DocumentViewerActivity : AppCompatActivity(), PagedTextViewInterface {
private fun showChapterListDialog() { private fun showChapterListDialog() {
if (!::pageIndexer.isInitialized || pageIndexer.chapters.isEmpty()) { if (!::pageIndexer.isInitialized || pageIndexer.chapters.isEmpty()) {
// 챕터가 아직 파싱되지 않았거나 없는 경우 예외 처리 Toast.makeText(this, "인덱싱된 챕터가 없습니다.", Toast.LENGTH_SHORT).show()
return return
} }
val chapterTitles = pageIndexer.chapters.map { "${it.title} (p.${it.pageIndex + 1})" }.toTypedArray() // 1. 현재 읽고 있는 페이지가 어떤 챕터에 속해 있는지 index를 찾습니다.
var currentChapterIndex = 0
AlertDialog.Builder(this) for (i in pageIndexer.chapters.indices) {
.setTitle("목차 (Chapters)") if (pageIndexer.chapters[i].pageIndex <= currentPageIndex) {
.setItems(chapterTitles) { dialog, which -> currentChapterIndex = i
// 사용자가 선택한 챕터의 pageIndex로 바로 이동 } else {
val targetChapter = pageIndexer.chapters[which] break
showPage(targetChapter.pageIndex)
dialog.dismiss()
} }
.setNegativeButton("닫기", null) }
.show()
// 2. 챕터 제목들을 리스트뷰에 보여주기 위한 배열 변환
val chapterTitles = pageIndexer.chapters.map { it.title }.toTypedArray()
// 3. AlertDialog 생성
val builder = AlertDialog.Builder(this)
builder.setTitle("목차 (현재 위치로 이동)")
// setItems 대신 Adapter를 직접 지정하거나 리스트를 생성해야 스크롤 제어가 가능합니다.
builder.setItems(chapterTitles) { dialog, which ->
// 선택한 챕터의 페이지로 이동
val targetPage = pageIndexer.chapters[which].pageIndex
showPage(targetPage)
dialog.dismiss()
}
val dialog = builder.create()
dialog.show()
// 4. [핵심] 다이얼로그가 화면에 나타난 후, 내부 ListView를 찾아 현재 읽던 챕터 위치로 스크롤을 올립니다.
dialog.listView?.let { listView ->
listView.post {
// 현재 읽고 있는 챕터(currentChapterIndex)가 리스트의 최상단에 보이도록 스크롤 이동
listView.setSelection(currentChapterIndex)
}
}
} }
/** /**

View File

@ -11,10 +11,9 @@ import java.io.RandomAccessFile
import java.nio.charset.Charset import java.nio.charset.Charset
import java.util.regex.Pattern import java.util.regex.Pattern
// 1. 챕터 정보를 저장할 데이터 클래스 정의
data class Chapter( data class Chapter(
val title: String, // 챕터 이름 (예: "제 1화 시작하며") val title: String,
val pageIndex: Int // 해당 챕터가 시작되는 페이지 인덱스 val pageIndex: Int
) )
class PageIndexer( class PageIndexer(
@ -25,14 +24,9 @@ class PageIndexer(
private val viewHeight: Int private val viewHeight: Int
) { ) {
val pageOffsets = ArrayList<Long>() val pageOffsets = ArrayList<Long>()
// 2. 추출된 챕터들을 담을 리스트 생성
val chapters = ArrayList<Chapter>() val chapters = ArrayList<Chapter>()
private val charset = Charset.forName(encoding) private val charset = Charset.forName(encoding)
// 3. 탐지할 챕터 패턴 정규식 정의 (예: "제 1화", "제1장", "Chapter 5", "CH.3" 등 대응)
// 소설이나 텍스트 특성에 맞게 패턴을 수정하시면 됩니다.
private val chapterPattern = Pattern.compile( private val chapterPattern = Pattern.compile(
"(?:제\\s*)?\\d+\\s*[화|장|막|절|편]" "(?:제\\s*)?\\d+\\s*[화|장|막|절|편]"
) )
@ -80,37 +74,58 @@ class PageIndexer(
if (endLine < startLine) endLine = startLine if (endLine < startLine) endLine = startLine
val endCharOffset = layout.getLineEnd(endLine) val endCharOffset = layout.getLineEnd(endLine)
var pageText = chunkText.substring(chunkConsumedOffset, endCharOffset)
val pageText = chunkText.substring(chunkConsumedOffset, endCharOffset) // -------------------------------------------------------------------------
// [수정 구간] 챕터 탐지 및 강제 페이지 분할 로직
// 4. [핵심] 현재 잘라낸 페이지 텍스트 내에 챕터 패턴이 존재하는지 검사 // -------------------------------------------------------------------------
// 페이지의 첫 부분 위주로 검사하거나 문단 단위로 첫 줄을 검사하는 것이 정확합니다.
val matcher = chapterPattern.matcher(pageText) val matcher = chapterPattern.matcher(pageText)
if (matcher.find()) { if (matcher.find()) {
// 해당 페이지 내에서 실제 챕터 제목으로 쓸 만한 한 줄(Line) 전체를 가져옵니다.
// 보통 챕터 제목은 한 줄을 통째로 차지하므로, 패턴이 발견된 위치의 앞뒤 줄바꿈(\n)을 기준으로 잘라냅니다.
val matchStart = matcher.start() val matchStart = matcher.start()
// 패턴 시작점 기준 앞쪽 줄바꿈 찾기 // 챕터 제목 줄의 시작 위치 계산
val lineStart = pageText.lastIndexOf('\n', matchStart).let { if (it == -1) 0 else it + 1 } 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 } val lineEnd = pageText.indexOf('\n', matchStart).let { if (it == -1) pageText.length else it }
val chapterTitle = pageText.substring(lineStart, lineEnd).trim()
var chapterTitle = pageText.substring(lineStart, lineEnd).trim() // 글자수 제한 체크 (40자 미만 정상 챕터인 경우)
// 제목이 너무 길면 본문 문장이 오탐지된 것일 수 있으므로 글자수 제한(예: 40자)을 둡니다.
if (chapterTitle.isNotEmpty() && chapterTitle.length < 40) { if (chapterTitle.isNotEmpty() && chapterTitle.length < 40) {
val currentPageIndex = pageOffsets.size - 1
// 중복 등록 방지 (동일 페이지 내 다중 감지 방어) // 만약 챕터가 페이지의 맨 처음(lineStart == 0)이 아니라면,
if (chapters.isEmpty() || chapters.last().pageIndex != currentPageIndex) { // 즉, 챕터 앞에 이전 장의 본문 내용이 포함되어 있다면 강제로 쪼갭니다.
chapters.add(Chapter(chapterTitle, currentPageIndex)) if (lineStart > 0) {
// 1. 챕터 직전 내용까지만 현재 페이지 텍스트로 인정합니다.
val previousSectionText = pageText.substring(0, lineStart)
val previousSectionBytes = previousSectionText.toByteArray(charset).size
// 2. 현재 오프셋에 이전 내용 크기를 더해 "새 페이지(챕터 시작점)" 오프셋을 구합니다.
currentOffset += previousSectionBytes
// 3. 페이지 목록에 새 오프셋(챕터의 시작점)을 즉시 추가합니다.
pageOffsets.add(currentOffset)
// 4. 다음 루프를 위해 chunk 내 소비된 문자 인덱스를 업데이트합니다.
chunkConsumedOffset += lineStart
// 5. StaticLayout 상에서 이 챕터 줄이 속한 실제 Line을 찾아서 루프의 startLine을 갱신합니다.
// layout.getLineForOffset은 전체 chunkText 기준이므로 chunkConsumedOffset을 대입합니다.
startLine = layout.getLineForOffset(chunkConsumedOffset)
// 중요: 이번 루프는 여기서 끊고, 쪼개진 새 페이지 기준으로 다음 루프에서 처리하도록 넘깁니다.
continue
} else {
// 이미 페이지의 맨 첫 줄이 챕터인 경우 바로 리스트에 등록합니다.
val currentPageIndex = pageOffsets.size - 1
if (chapters.isEmpty() || chapters.last().pageIndex != currentPageIndex) {
chapters.add(Chapter(chapterTitle, currentPageIndex))
}
} }
} }
} }
// -------------------------------------------------------------------------
// 챕터 분할이 일어나지 않은 일반적인 페이지 처리 flow
val pageBytesSize = pageText.toByteArray(charset).size val pageBytesSize = pageText.toByteArray(charset).size
currentOffset += pageBytesSize currentOffset += pageBytesSize
chunkConsumedOffset = endCharOffset chunkConsumedOffset = endCharOffset

View File

@ -210,7 +210,7 @@ var batteryPct = 50
// 2. 파일 다운로드: 계산된 점수(finalScore)가 낮은 순으로 정렬 // 2. 파일 다운로드: 계산된 점수(finalScore)가 낮은 순으로 정렬
if (isCharging && batteryPct > 70) { if (isCharging && batteryPct > 75) {
torrentsWithoutMetadata.forEach { it.swig().resume() } 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 }

View File

@ -58,10 +58,10 @@
app:fab_label="📺" app:fab_label="📺"
style="@style/CommonFabStyle" style="@style/CommonFabStyle"
android:id="@+id/youtube"/> android:id="@+id/youtube"/>
<bums.lunatic.launcher.view.FloatingActionButton <!-- <bums.lunatic.launcher.view.FloatingActionButton-->
app:fab_label="🤖" <!-- app:fab_label="🤖"-->
style="@style/CommonFabStyle" <!-- style="@style/CommonFabStyle"-->
android:id="@+id/perplexity"/> <!-- android:id="@+id/perplexity"/>-->
<bums.lunatic.launcher.view.FloatingActionButton <bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="😂" app:fab_label="😂"
android:visibility="gone" android:visibility="gone"
@ -93,7 +93,6 @@
/> />
<bums.lunatic.launcher.view.FloatingActionButton <bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="📦" app:fab_label="📦"
android:visibility="gone"
style="@style/CommonFabStyle" style="@style/CommonFabStyle"
android:id="@+id/btn_completed_files" android:id="@+id/btn_completed_files"
/> />