This commit is contained in:
2026-08-03 17:52:49 +09:00
parent 24a2a74768
commit ad505863b7
7 changed files with 201 additions and 27 deletions
+14 -5
View File
@@ -125,7 +125,16 @@
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="false"> android:exported="false">
</activity> </activity>
<activity android:name=".home.WebReaderActivity"
android:theme="@style/Theme.Player"
android:launchMode="singleInstance"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density|fontScale|keyboardHidden|keyboard|layoutDirection|navigation"
android:screenOrientation="portrait"
android:excludeFromRecents="false"
android:hardwareAccelerated="true"
android:exported="false">
</activity>
<activity <activity
android:name=".player.DocumentViewerActivity" android:name=".player.DocumentViewerActivity"
android:theme="@style/Theme.Player" android:theme="@style/Theme.Player"
@@ -184,7 +193,7 @@
<action android:name="android.intent.action.PHONE_STATE" /> <action android:name="android.intent.action.PHONE_STATE" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".receiver.SmsReceiver" <receiver android:name=".receiver.SmsReceiver"
android:exported="true"> <intent-filter android:priority="2147483647"> android:exported="true"> <intent-filter android:priority="2147483647">
<action android:name="android.provider.Telephony.SMS_RECEIVED" /> <action android:name="android.provider.Telephony.SMS_RECEIVED" />
<action android:name="android.provider.Telephony.WAP_PUSH_RECEIVED" /> <action android:name="android.provider.Telephony.WAP_PUSH_RECEIVED" />
@@ -240,10 +249,10 @@
</intent-filter> </intent-filter>
</activity> </activity>
<!-- <service--> <!-- <service-->
<!-- android:name=".feeds.rss.RssService"--> <!-- android:name=".feeds.rss.RssService"-->
<!-- android:permission="android.permission.BIND_JOB_SERVICE"--> <!-- android:permission="android.permission.BIND_JOB_SERVICE"-->
<!-- android:exported="false"/>--> <!-- android:exported="false"/>-->
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>$Title$</title>
</head>
<body>
$END$
</body>
</html>
@@ -629,7 +629,7 @@ open class NeoRssActivity : CommonActivity() {
// 처음 호출되는 메뉴라면 인스턴스 생성 및 추가 // 처음 호출되는 메뉴라면 인스턴스 생성 및 추가
targetFragment = when(id) { targetFragment = when(id) {
R.id.feeds -> RssHome() R.id.feeds -> RssHome()
R.id.books -> TokiFragment.newInstanceNovels()
// 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()
@@ -649,6 +649,11 @@ open class NeoRssActivity : CommonActivity() {
R.id.btn_info -> SystemStatusFragment() R.id.btn_info -> SystemStatusFragment()
R.id.btn_completed_files -> CompletedFilesFragment() R.id.btn_completed_files -> CompletedFilesFragment()
R.id.btn_learn -> LearningFragment() R.id.btn_learn -> LearningFragment()
R.id.books -> {
startActivity(Intent(this@NeoRssActivity, WebReaderActivity::class.java))
finish()
return
}
R.id.close -> { R.id.close -> {
finish() finish()
return return
@@ -0,0 +1,132 @@
package bums.lunatic.launcher.home
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.KeyEvent
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import bums.lunatic.launcher.R
class WebReaderActivity : AppCompatActivity() {
private lateinit var webView: WebView
// 웹뷰에서 파일을 선택할 때 결과를 받을 콜백 변수
private var filePathCallback: ValueCallback<Array<Uri>>? = null
// 파일 탐색기 결과를 처리하는 런처 (최신 Activity Result API 사용)
private val fileChooserLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data = result.data
val uriList = if (data?.data != null) {
arrayOf(data.data!!)
} else {
null
}
// 웹뷰로 선택한 파일 URI 전달
filePathCallback?.onReceiveValue(uriList)
} else {
// 취소했을 경우 반드시 null을 전달해야 웹뷰가 멈추지 않음
filePathCallback?.onReceiveValue(null)
}
filePathCallback = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_wr)
webView = findViewById(R.id.webView)
setupWebView()
// assets 폴더에 넣은 HTML 파일 로드
webView.loadUrl("https://git.lunaticbum.kr/reader.html")
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
// 볼륨 다운 버튼 -> 다음 페이지 (오른쪽 화살표)
// 웹뷰에 JavaScript 키보드 이벤트를 강제로 발생시킴
webView.evaluateJavascript(
"window.dispatchEvent(new KeyboardEvent('keydown', {'key': 'ArrowRight'}));",
null
)
return true // true를 반환하면 시스템 볼륨이 변경되지 않고 이벤트가 소비됨
}
KeyEvent.KEYCODE_VOLUME_UP -> {
// 볼륨 업 버튼 -> 이전 페이지 (왼쪽 화살표)
webView.evaluateJavascript(
"window.dispatchEvent(new KeyboardEvent('keydown', {'key': 'ArrowLeft'}));",
null
)
return true // true를 반환하면 시스템 볼륨이 변경되지 않음
}
}
// 볼륨 키가 아닌 다른 키(뒤로가기 등)는 기본 안드로이드 동작을 수행하도록 넘김
return super.onKeyDown(keyCode, event)
}
private fun setupWebView() {
// 1. 웹뷰 기본 설정
webView.settings.apply {
javaScriptEnabled = true // 자바스크립트 필수
domStorageEnabled = true // localStorage, IndexedDB 사용을 위해 필수
allowFileAccess = true // 로컬 파일 접근 허용
allowContentAccess = true // Content URI 접근 허용
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW // 보안 정책 우회 (필요시)
// 모바일 화면에 맞게 뷰포트 설정
useWideViewPort = true
loadWithOverviewMode = true
}
// 2. 새 창 열림 방지 (앱 내부에서만 렌더링)
webView.webViewClient = WebViewClient()
// 3. 파일 업로드(<input type="file">) 처리를 위한 WebChromeClient 설정
webView.webChromeClient = object : WebChromeClient() {
override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams?
): Boolean {
// 이전 콜백이 남아있다면 취소 처리
this@WebReaderActivity.filePathCallback?.onReceiveValue(null)
this@WebReaderActivity.filePathCallback = filePathCallback
// 안드로이드 파일 탐색기 호출 Intent
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
// txt, epub만 선택하게 하려면 아래처럼 mimeTypes 지정 가능
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("text/plain", "application/epub+zip"))
}
fileChooserLauncher.launch(intent)
return true
}
}
}
// 뒤로가기 버튼 처리 (웹뷰 내에서 뒤로 갈 수 있으면 웹뷰 뒤로가기 실행)
override fun onBackPressed() {
if (webView.canGoBack()) {
webView.goBack()
} else {
super.onBackPressed()
}
}
}
@@ -185,13 +185,13 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
putBoolean(ARG_ENABLE_GESTURE, false) putBoolean(ARG_ENABLE_GESTURE, false)
} }
} }
// https://git.lunaticbum.kr/reader.html
fun newInstanceNovels(): TokiFragment = TokiFragment().apply { fun newInstanceNovels(): TokiFragment = TokiFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putString(ARG_TYPE, "web") putString(ARG_TYPE, "web")
putInt(ARG_LAST_NUM, 468) putInt(ARG_LAST_NUM, 468)
putString(ARG_NAME, "sbxh2") putString(ARG_NAME, "git.lunaticbum")
putString(ARG_DOT, "com/novel") putString(ARG_DOT, ".kr/reader.html")
putBoolean(ARG_USE_NUM_URL, false) putBoolean(ARG_USE_NUM_URL, false)
putBoolean(ARG_ENABLE_GESTURE, true) putBoolean(ARG_ENABLE_GESTURE, true)
} }
@@ -60,28 +60,34 @@ class DocumentViewerActivity : AppCompatActivity(), PagedTextViewInterface {
pageIndexer.buildIndex { progress -> pageIndexer.buildIndex { progress ->
runOnUiThread { runOnUiThread {
val currentOffsetsSize = pageIndexer.pageOffsets.size try {
val currentOffsetsSize = pageIndexer.pageOffsets.size
if (!hasRestoredPage) { if (!hasRestoredPage) {
if (targetOffset >= 0L) { if (targetOffset >= 0L) {
// [크기 재조정 케이스] 기억해둔 오프셋 위치가 확보되었는지 확인
val foundIndex = pageIndexer.pageOffsets.indexOfLast { it <= targetOffset } // [크기 재조정 케이스] 기억해둔 오프셋 위치가 확보되었는지 확인
// 마지막 오프셋이거나, 다음 페이지 오프셋까지 리스트에 확보되었을 때 전환 val foundIndex = pageIndexer.pageOffsets.indexOfLast { it <= targetOffset }
if (foundIndex >= 0 && (foundIndex < currentOffsetsSize - 1 || progress >= 100)) { // 마지막 오프셋이거나, 다음 페이지 오프셋까지 리스트에 확보되었을 때 전환
showPage(foundIndex) if (foundIndex >= 0 && (foundIndex < currentOffsetsSize - 1 || progress >= 100)) {
hasRestoredPage = true showPage(foundIndex)
} hasRestoredPage = true
} else { }
// [최초 진입 케이스] SharedPreferences 복구 } else {
val savedPageIndex = sharedPreferences.getInt(file.absolutePath, 0) // [최초 진입 케이스] SharedPreferences 복구
if (currentOffsetsSize > savedPageIndex) { val savedPageIndex = sharedPreferences.getInt(file.absolutePath, 0)
showPage(savedPageIndex) if (currentOffsetsSize > savedPageIndex) {
hasRestoredPage = true showPage(savedPageIndex)
hasRestoredPage = true
}
} }
} }
}
pagedLayout.currentPageTextView?.text = "${currentPageIndex + 1} / $currentOffsetsSize" pagedLayout.currentPageTextView?.text = "${currentPageIndex + 1} / $currentOffsetsSize"
}
catch (e : Exception) {
e.printStackTrace()
}
} }
} }
} }
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>