This commit is contained in:
lunaticbum 2026-01-30 16:52:36 +09:00
parent 16640559b3
commit 9f6c424e04
10 changed files with 1917 additions and 2477 deletions

View File

@ -155,6 +155,7 @@ dependencies {
// implementation 'com.vladsch.flexmark:flexmark-all:0.64.8'
// implementation("org.opencv:opencv-android:4.11.0")
// build.gradle에 추가
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")
implementation("io.github.junkfood02.youtubedl-android:ffmpeg:0.17.4")

View File

@ -91,8 +91,6 @@
android:screenOrientation="userPortrait"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|screenLayout|layoutDirection|navigation"
android:windowSoftInputMode="adjustResize"
android:noHistory="true"
android:excludeFromRecents="true"
android:exported="false">
<intent-filter>

View File

@ -291,7 +291,9 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
if (item.type == "APP") {
val app = realm.query<AppInfo>("pkgName == $0", item.key).first().find()
if (app != null && !app.blockRecommend) {
unifiedList.add(RecommendationItem.AppItem(realm.copyFromRealm(app)))
try { if(pm.getLaunchIntentForPackage(app.pkgName ?: "") != null) {
unifiedList.add(RecommendationItem.AppItem(realm.copyFromRealm(app)))
}} catch (e: Exception) { }
}
} else if (item.type == "CONTACT") {
// 연락처 ID나 전화번호로 조회 (Log 저장 시 key가 무엇인지에 따라 다름)

View File

@ -82,6 +82,40 @@ open class GeckoWeb @JvmOverloads constructor(
// --- 1. Properties & Initialization ---
// 1. 세션 상태를 저장할 SharedPreferences 키
private val PREF_SESSION_STATE = "gecko_session_state"
// 2. 현재 세션 상태 저장 메서드
fun saveCurrentSessionState() {
lastSessionState?.let { state ->
// SessionState.toString()은 내부적으로 JSON 문자열을 반환합니다.
val stateJson = state.toString()
context.getSharedPreferences("GeckoPrefs", Context.MODE_PRIVATE)
.edit()
.putString("gecko_session_state", stateJson)
.apply()
Log.d("GeckoWeb", "Session State Saved: $stateJson")
}
}
// 3. 저장된 세션 상태 복구 메서드
fun restoreSessionState(session: GeckoSession) {
val stateJson = context.getSharedPreferences("GeckoPrefs", Context.MODE_PRIVATE)
.getString("gecko_session_state", null)
if (!stateJson.isNullOrEmpty()) {
// 문자열에서 SessionState 객체 생성
val state = GeckoSession.SessionState.fromString(stateJson)
if (state != null) {
session.restoreState(state)
Log.d("GeckoWeb", "Session State Restored")
}
}
}
// UI & State
var decoViews = arrayListOf<View>()
var progress: ProgressBar? = null
@ -138,8 +172,8 @@ open class GeckoWeb @JvmOverloads constructor(
override fun onLongPress(targetView: View, fingers: Int) = true
})
class GKCookie { var COOKIES: String? = null }
var mGKCookie: GKCookie? = null
// class GKCookie { var COOKIES: String? = null }
// var mGKCookie: GKCookie? = null
// Markdown/Scraping
var markdownContents: String? = null
@ -192,23 +226,23 @@ open class GeckoWeb @JvmOverloads constructor(
CoroutineScope(Dispatchers.IO).launch {
try {
val request = YoutubeDLRequest(url)
mGKCookie?.COOKIES?.let { cookieStr ->
val cookieFile = File(context.filesDir, "cookies.txt")
val cookies = cookieStr.split(";").mapNotNull {
val p = it.trim().split("=", limit = 2)
if (p.size == 2) p[0] to p[1] else null
}.toMap()
val expires = (System.currentTimeMillis() / 1000) + 3600 * 24 * 7
val content = buildString {
appendLine("# Netscape HTTP Cookie File")
cookies.forEach { (k, v) ->
appendLine(".${url.toUri().host}\tTRUE\t/\tTRUE\t$expires\t$k\t$v")
}
}
cookieFile.writeText(content)
request.addOption("--cookies", cookieFile.absolutePath)
}
// mGKCookie?.COOKIES?.let { cookieStr ->
// val cookieFile = File(context.filesDir, "cookies.txt")
// val cookies = cookieStr.split(";").mapNotNull {
// val p = it.trim().split("=", limit = 2)
// if (p.size == 2) p[0] to p[1] else null
// }.toMap()
// val expires = (System.currentTimeMillis() / 1000) + 3600 * 24 * 7
//
// val content = buildString {
// appendLine("# Netscape HTTP Cookie File")
// cookies.forEach { (k, v) ->
// appendLine(".${url.toUri().host}\tTRUE\t/\tTRUE\t$expires\t$k\t$v")
// }
// }
// cookieFile.writeText(content)
// request.addOption("--cookies", cookieFile.absolutePath)
// }
val videoInfo = YoutubeDL.getInstance().getInfo(request)
if (videoInfo != null && !videoInfo.title.isNullOrEmpty()) {
@ -321,12 +355,14 @@ open class GeckoWeb @JvmOverloads constructor(
}
override fun onPageStop(session: GeckoSession, success: Boolean) {
onPageStopCallback?.invoke(success)
saveCurrentSessionState()
}
override fun onSessionStateChange(session: GeckoSession, sessionState: GeckoSession.SessionState) {
onSessionStateChangeCallback?.invoke(sessionState)
}
}
private var lastSessionState: GeckoSession.SessionState? = null
// [Extension Delegates]
private val addonManagerDelegate = object : AddonManagerDelegate {
override fun onReady(extension: WebExtension) { mExtension = extension }
@ -386,6 +422,7 @@ open class GeckoWeb @JvmOverloads constructor(
private fun buildWeb() {
getRuntime()?.let { runtime ->
val session = GeckoSession()
restoreSessionState(session)
session.open(runtime)
this.setSession(session)

View File

@ -41,12 +41,7 @@ import bums.lunatic.launcher.home.adapters.BookmarkPagerFragment
import bums.lunatic.launcher.model.RssData
import bums.lunatic.launcher.model.RssDataType
import bums.lunatic.launcher.settings.SettingsActivity
import bums.lunatic.launcher.home.tokiz.Comics
import bums.lunatic.launcher.home.tokiz.Novels
import bums.lunatic.launcher.home.tokiz.Perplexity
import bums.lunatic.launcher.home.tokiz.TokiFragment
import bums.lunatic.launcher.home.tokiz.Webtoons
import bums.lunatic.launcher.home.tokiz.YouTube
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.utils.beforeDay
import bums.lunatic.launcher.workers.WorkersDb
@ -63,6 +58,7 @@ import org.mozilla.geckoview.ExperimentDelegate
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings
import java.io.File
open class NeoRssActivity : CommonActivity() {
@ -116,7 +112,7 @@ open class NeoRssActivity : CommonActivity() {
var onExit = false
var lastAction = MotionEvent.ACTION_HOVER_EXIT
override fun dispatchKeyEvent(ev: KeyEvent): Boolean {
val currentFragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
Blog.LOGE("dispatchKeyEvent >>> ${ev}")
if (ev?.device?.name?.contains("SM-031N Mouse") == true) {
when(ev.action) {
ACTION_UP -> {
@ -162,36 +158,35 @@ open class NeoRssActivity : CommonActivity() {
}
else {
val currentFragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
when(currentFragment) {
is Novels -> {
if(MotionEvent.ACTION_UP.equals(ev?.action ?: MotionEvent.ACTION_CANCEL) == true) {
return when (ev.keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if(currentFragment is Novels){ currentFragment.actionNextEvent() }
true
}
KeyEvent.KEYCODE_VOLUME_UP -> {
if(currentFragment is Novels){ currentFragment.actionPrevEvent() }
true
}
else -> false
Blog.LOGE("currentFragment >>> ${currentFragment} ${currentFragment is TokiFragment && currentFragment.isbooktoki()}")
if (currentFragment is TokiFragment && currentFragment.isbooktoki()) {
if(MotionEvent.ACTION_UP.equals(ev?.action ?: MotionEvent.ACTION_CANCEL) == true) {
return when (ev.keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if(currentFragment is TokiFragment){ currentFragment.actionNextEvent() }
true
}
} else {
return when (ev.keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
true
}
KeyEvent.KEYCODE_VOLUME_UP -> {
true
}
else -> false
KeyEvent.KEYCODE_VOLUME_UP -> {
if(currentFragment is TokiFragment){ currentFragment.actionPrevEvent() }
true
}
else -> false
}
} else {
return when (ev.keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
true
}
KeyEvent.KEYCODE_VOLUME_UP -> {
true
}
else -> false
}
}
else -> return super.dispatchKeyEvent(ev)
}
else { return super.dispatchKeyEvent(ev) }
}
return super.dispatchKeyEvent(ev)
}
@ -293,67 +288,6 @@ open class NeoRssActivity : CommonActivity() {
showContents(v.id)
}
// override fun onNewIntent(intent: Intent) {
// Blog.LOGE("onNewIntent intent >> ${intent}")
// if(intent?.action?.equals(Intent.ACTION_SEND) == true &&
// intent?.hasExtra(Intent.EXTRA_TEXT) == true) {
// intent?.getStringExtra(Intent.EXTRA_TEXT)?.let {
// if(it.startsWith("http") == false) {
// it.split("http").forEach { string ->
// if(string.startsWith("http")) {
// try {
// string.toUri()?.let { uri ->
// Blog.LOGE("onNewIntent string uri.lastPathSegment >>>>> ${uri.host}")
// Blog.LOGE("onNewIntent string uri.lastPathSegment >>>>> ${uri.lastPathSegment}")
// }
// } catch (e: Exception) {
//
// }
// }
// }
// } else {
// try {
// it.toUri()?.let { uri ->
// Blog.LOGE("onNewIntent it uri.lastPathSegment >>>>> ${uri.host}")
// Blog.LOGE("onNewIntent it uri.lastPathSegment >>>>> ${uri.lastPathSegment}")
// }
// } catch (e: Exception) {
//
// }
// }
// }
// } else if (intent?.action == Intent.ACTION_WEB_SEARCH) {
// openWithIntent(intent)
// } else {
// Blog.LOGE("onNewIntent intent?.hasExtra >> ${intent?.hasExtra(Intent.EXTRA_STREAM)}")
//
//
// if (intent?.action?.equals(Intent.ACTION_MAIN) == true && intent.categories.contains(
// Intent.CATEGORY_HOME
// ) && intent.hasExtra("android.intent.extra.EXTRA_START_REASON") && intent.getStringExtra(
// "android.intent.extra.EXTRA_START_REASON"
// ).equals("startDockOrHome")
// ) {
//
// } else {
// intent?.extras?.keySet()?.forEach {
// try {
// Blog.LOGE(
// "onNewIntent :: key >> ${it} :: value >> ${
// intent?.extras?.getString(
// it
// )
// }"
// )
// } catch (e: Exception) {
// e.printStackTrace()
// }
//
// }
// }
// }
// super.onNewIntent(intent)
// }
private fun openWithIntent(intent: Intent){
Blog.LOGE("intent >> ${intent}")
@ -528,7 +462,13 @@ open class NeoRssActivity : CommonActivity() {
private fun initGeckoRuntime() {
if (sRuntime == null) {
try {
val profileDir = File(this.filesDir, "geckoview_profile")
if (!profileDir.exists()) profileDir.mkdirs()
sRuntime = GeckoRuntime.create(this, GeckoRuntimeSettings.Builder()
.configFilePath(profileDir.absolutePath)
.aboutConfigEnabled(true)
.javaScriptEnabled(true)
.extensionsProcessEnabled(true)
.extensionsWebAPIEnabled(true)
.experimentDelegate(experimentDelegate)
@ -559,16 +499,9 @@ open class NeoRssActivity : CommonActivity() {
sRuntime = null
} catch (e: Exception) { e.printStackTrace() }
// appWidgetHost?.stopListening() // 이 줄은 제거하고 onStop으로 이동
super.onDestroy()
}
override fun onUserLeaveHint() {
showContents(R.id.close)
super.onUserLeaveHint()
}
@RequiresApi(Build.VERSION_CODES.O_MR1)
override fun onResume() {
@ -605,12 +538,12 @@ open class NeoRssActivity : CommonActivity() {
currentFragment.doNextPage()
}
}
is YouTube -> {
currentFragment.back()
}
is Novels -> {
currentFragment.actionNextEvent(false)
}
// is YouTube -> {
// currentFragment.back()
// }
// is Novels -> {
// currentFragment.actionNextEvent(false)
// }
else -> {
showContents(R.id.close)
}

View File

@ -33,7 +33,6 @@ import androidx.fragment.app.Fragment
import bums.lunatic.launcher.R
import bums.lunatic.launcher.databinding.BooktokiBinding
import bums.lunatic.launcher.home.GeckoWeb
import bums.lunatic.launcher.home.GeckoWeb.GKCookie
import bums.lunatic.launcher.home.GeckoWeb.JxEvent
import bums.lunatic.launcher.home.NeoRssActivity
import bums.lunatic.launcher.home.NeoRssActivity.Companion.getRuntime
@ -224,6 +223,9 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
}
}
fun isbooktoki() : Boolean {
return webcontentsName.contains("booktoki")
}
override fun onSwipeLeft(count: Int) {
if (!enableGestures) return
Blog.LOGD(log = "onSwipeLeft ${count}")
@ -1060,7 +1062,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
view.visibility = VISIBLE
binding.menuWeb.visibility = GONE
}
view.forceUpdateUI()
// view.forceUpdateUI()
lastedUrl?.let {
Uri.parse(it)?.let { uri ->
uri.path?.let {

View File

@ -1,336 +0,0 @@
//package bums.lunatic.launcher.home.tokiz.view
//
//import android.annotation.SuppressLint
//import android.content.Context
//import android.content.Intent
//import android.os.Build
//import android.util.AttributeSet
//import android.view.MotionEvent
//import android.view.PointerIcon
//import android.view.View
//import androidx.core.net.toUri
//import androidx.core.view.isVisible
//import bums.lunatic.launcher.R
//import bums.lunatic.launcher.helpers.ForeGroundService
//import bums.lunatic.launcher.helpers.ForeGroundService.Companion.ACTION_VIDEO_DOWNLOAD
//import bums.lunatic.launcher.helpers.ForeGroundService.Companion.EXTRA_TARGET_URL
//import bums.lunatic.launcher.utils.Blog
//import bums.lunatic.launcher.utils.SimpleFingerGestures
//import com.yausername.youtubedl_android.YoutubeDL
//import com.yausername.youtubedl_android.YoutubeDLRequest
//import kotlinx.coroutines.CoroutineScope
//import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.launch
//import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
//import org.mozilla.geckoview.GeckoView
//import java.io.File
//import java.util.Base64
//import kotlin.collections.iterator
//
//enum class JxEvent {
// SCROLL_UP,
// SCROLL_DOWN,
// SWIPE_LEFT,
// SWIPE_RIGHT,
// ON_CLICK,
//}
//typealias JxInteface = (JxEvent)->Unit
//open class BWebview : GeckoView {
// var decoViews = arrayListOf<View>()
// @SuppressLint("ClickableViewAccessibility")
// constructor(context: Context?) : super(context) {
// this.setOnTouchListener { v, event ->
// if (event.device.name?.contains(
// "JX-12",
// true
// ) == true || event.device.name?.equals("J06", true) == true
// ) {
// return@setOnTouchListener mSimpleFingerGestures.onTouch(v, event)
// } else {
// return@setOnTouchListener super.onTouchEvent(event)
// }
// }
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// val nullCursor = PointerIcon.getSystemIcon(context!!, PointerIcon.TYPE_NULL)
// this.setPointerIcon(nullCursor)
// }
// }
//
//
// @SuppressLint("ClickableViewAccessibility")
// constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
// this.setOnTouchListener { v,event ->
// if (event.device.name?.contains("JX-12",true) == true|| event.device.name?.equals("J06",true) == true) {
// return@setOnTouchListener mSimpleFingerGestures.onTouch(v,event)
// } else {
// return@setOnTouchListener super.onTouchEvent(event)
// }
// }
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// val nullCursor = PointerIcon.getSystemIcon(context!!, PointerIcon.TYPE_NULL)
// this.setPointerIcon(nullCursor)
// }
// }
// fun videoDlownLoad(videoUrl : String) {
// val actionIntent = Intent(context, ForeGroundService::class.java).apply {
// action = ACTION_VIDEO_DOWNLOAD
// putExtra(EXTRA_TARGET_URL, videoUrl) // 전달할 데이터
// }
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// context.startForegroundService(actionIntent)
// } else {
// context.startService(actionIntent)
// }
// }
//
// class GKCookie {
// var COOKIES : String? = null
// }
//
// var mGKCookie : GKCookie? = null
//
// fun checkIfDownloadable(url: String) {
// CoroutineScope(Dispatchers.Main).launch {
// runOnUiThread {
// decoViews.filter { it.id == R.id.dl_video }.firstOrNull()?.let {
// it.setOnClickListener {}
// it.visibility = GONE
// }}}
// Blog.LOGE("checkIfDownloadable ${url}")
// CoroutineScope(Dispatchers.IO).launch {
// try {
// var request = YoutubeDLRequest(url)
// (mGKCookie?.COOKIES)?.let{
// Blog.LOGE(it)
// val cookies = it.split(";")
// .map { it.trim() }
// .mapNotNull {
// val parts = it.split("=", limit = 2)
// if (parts.size == 2) parts[0] to parts[1] else null
// }
// .toMap()
// val expires = (System.currentTimeMillis() / 1000) + 3600 * 24 * 7 // 일주일 후 만료 예시
//
// val cookieFileContent = buildString {
// appendLine("# Netscape HTTP Cookie File")
// for ((name, value) in cookies) {
// appendLine(".${url.toUri().host}\tTRUE\t/\tTRUE\t$expires\t$name\t$value")
// }
// }
// val cookieFile = File(context.filesDir, "cookies.txt")
// cookieFile.writeText(cookieFileContent)
// request.addOption("--cookies", cookieFile.absolutePath)
// }
//
// val videoInfo = YoutubeDL.getInstance().getInfo(request)
// // videoInfo 가 null 아니고, 필요한 키(예: title, url 등)가 있으면 다운로드 가능
// Blog.LOGE("checkIfDownloadable ${url}\n videoInfo : ${videoInfo}")
// var canVideoDown = videoInfo != null && !videoInfo.title.isNullOrEmpty()
// CoroutineScope(Dispatchers.Main).launch {
// runOnUiThread {
// decoViews.filter { it.id == R.id.dl_video }.firstOrNull()?.let {
// it.setOnClickListener {
// videoDlownLoad(url)
// }
// it.visibility = if (canVideoDown){
// VISIBLE
// } else{
// GONE
// }
// }
// }
// }
//
// } catch (e: Exception) {
// e.printStackTrace()
// Blog.LOGE("checkIfDownloadable ${url} ${e}")
// CoroutineScope(Dispatchers.Main).launch {
// runOnUiThread {
// decoViews.filter { it.id == R.id.dl_video }.firstOrNull()?.let {
// it.setOnClickListener {}
// it.visibility = GONE
// }}}
// }
// }
// }
// val mSimpleFingerGestures = SimpleFingerGestures(omfgl = object : SimpleFingerGestures.OnFingerGestureListener{
//
// override fun onSwipeUp(
// targetView: View,
// fingers: Int,
// gestureDuration: Long,
// gestureDistance: Double
// ): Boolean {
// Blog.LOGE("")
// jxInteface?.invoke(JxEvent.SCROLL_UP )
// return true
// }
//
// override fun onSwipeDown(
// targetView: View,
// fingers: Int,
// gestureDuration: Long,
// gestureDistance: Double
// ): Boolean {
// Blog.LOGE("")
// jxInteface?.invoke(JxEvent.SCROLL_DOWN)
// return true
// }
//
// override fun onSwipeLeft(
// targetView: View,
// fingers: Int,
// gestureDuration: Long,
// gestureDistance: Double
// ): Boolean {
// Blog.LOGE("")
// jxInteface?.invoke(JxEvent.SWIPE_LEFT)
// return true
// }
//
// override fun onSwipeRight(
// targetView: View,
// fingers: Int,
// gestureDuration: Long,
// gestureDistance: Double
// ): Boolean {
// Blog.LOGE("")
// jxInteface?.invoke(JxEvent.SWIPE_RIGHT)
// return true
// }
//
// override fun onPinch(
// targetView: View,
// fingers: Int,
// gestureDuration: Long,
// gestureDistance: Double
// ): Boolean {
// Blog.LOGE("onPinch")
// return true
// }
//
// override fun onUnpinch(
// targetView: View,
// fingers: Int,
// gestureDuration: Long,
// gestureDistance: Double
// ): Boolean {
// Blog.LOGE("")
// return true
// }
//
// override fun onDoubleTap(
// targetView: View,
// fingers: Int
// ): Boolean {
// Blog.LOGE("")
// return true
// }
//
// override fun onLongPress(
// targetView: View,
// fingers: Int
// ): Boolean {
// Blog.LOGE("onLongPress")
// return true
// }
//
// override fun onClick(
// targetView: View,
// fingers: Int
// ): Boolean {
// Blog.LOGE("onClick")
// jxInteface?.invoke(JxEvent.ON_CLICK)
// return true
// }
//
//
// })
// companion object {
// var currentRetryCount = 0
// }
//
// var jxInteface : JxInteface? = null
//
// var lastDomain : String = ""
//
// open fun loadUrl(url: String, param : String? = null) {
// var nUrl = url
// Blog.LOGE("url >>>> ${url}")
// if (url.endsWith("=")) {
// nUrl = String(Base64.getMimeDecoder().decode(url.toByteArray()))
// param?.let {
// nUrl = nUrl.plus(param)
// }
// } else if (url.startsWith("http") == false) {
// nUrl = lastDomain
// }
// if (this.isVisible == false) {
// this.visibility = VISIBLE
// }
// Blog.LOGE("nUrl >>>> ${nUrl}")
//
//
// nUrl?.let { url ->
// if (url.split("//").size > 1) {
// url.replace("//","/").replace("https:/","https://").let {
// Blog.LOGE("url >> ${url} , it >>> ${it}")
// this.session?.loadUri(it)
// }
// } else {
// this.session?.loadUri(url)
// }
// }
// currentRetryCount = 0;
// }
// private var lastX = 0f
// private var lastY = 0f
//
//
// override fun onTouchEvent(event: MotionEvent): Boolean {
// Blog.LOGE("event.device.name >>> ${event.device.name}")
// if (event.device.name?.contains("JX-12",true) == true || event.device.name?.equals("J06",true) == true) {
// Blog.LOGE("BWebview onTouchEvent $event")
// when (event.action) {
// MotionEvent.ACTION_DOWN -> {
// lastX = event.x
// lastY = event.y
// return true
// }
//
// MotionEvent.ACTION_MOVE -> {
// val deltaX = event.x - lastX
// val deltaY = event.y - lastY
// // 상하 이동이 더 크면(즉, 거의 수직 이동이면)만 처리
// if (Math.abs(deltaY) > Math.abs(deltaX)) {
// // 원하는 감도 적용
// val scrollFactor = 0.1f
//// scrollBy(0, (-deltaY * scrollFactor).toInt())
//
// jxInteface?.invoke(if ((-deltaY * scrollFactor).toInt() > 0){
// JxEvent.SCROLL_DOWN
// } else {
// JxEvent.SCROLL_UP
// })
// lastY = event.y
// lastX = event.x
// Blog.LOGE("return true for scroll")
// return true
// }
// // 좌우 이동은 무시
// lastY = event.y
// lastX = event.x
// Blog.LOGE("return false")
// return false
// }
//
// else -> {
// Blog.LOGE("call super")
// return super.onTouchEvent(event)
// }
// }
// } else {
// return super.onTouchEvent(event)
// }
// }
//}

View File

@ -3,11 +3,13 @@ package bums.lunatic.launcher.home.tokiz.view
import android.annotation.TargetApi
import android.content.Context
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.os.Build
import android.os.Handler
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
@ -19,8 +21,10 @@ import bums.lunatic.launcher.R
import bums.lunatic.launcher.home.tokiz.TouchArea
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.utils.SimpleFingerGestures
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.min
@ -33,248 +37,6 @@ interface PagedTextViewInterface {
fun onSwipeUp(touchCount : Int)
fun onLongClick()
}
class PagedTextView : AppCompatTextView {
private var needPaginate = false
private var isPaginating = false
private val pageList = arrayListOf<CharSequence>()
private var pageIndex: Int = 0
private var pageHeight: Int = 0
private var originalText: CharSequence = ""
var mPagedTextGenerateInterface : PagedTextGenerateInterface? = null
constructor(context: Context?) : super(context!!){initView(context)}
constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs){initView(context)}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context!!, attrs, defStyleAttr){initView(context)}
fun initView(context: Context?){
}
fun size(): Int = pageList.size
fun current() : Int = pageIndex
fun doPrev() {
if (pageIndex > 0 )
pageIndex = pageIndex - 1
setPageText()
}
fun doNext() {
if (pageIndex < pageList.size)
pageIndex = pageIndex + 1
setPageText()
}
fun next(index: Int) {
pageIndex = index
setPageText()
}
private fun setPageText() {
if(pageList.size > 0) {
isPaginating = true
text = pageList[pageIndex]
isPaginating = false
}
}
fun setTxtF(text: CharSequence?) {
needPaginate = true
this.setText(text , null)
}
override fun setText(text: CharSequence?, type: BufferType?) {
if (!isPaginating) {
needPaginate = true
originalText = text ?: ""
}
super.setText(text, type)
}
override fun setTextSize(size: Float) {
setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
override fun setTextSize(unit: Int, size: Float) {
super.setTextSize(unit, size)
paint.textSize = TypedValue.applyDimension(unit, size, context.resources.getDisplayMetrics())
needPaginate = true
}
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
super.setPadding(left, top, right, bottom)
needPaginate = true
}
override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
super.setPaddingRelative(start, top, end, bottom)
needPaginate = true
}
override fun setTextScaleX(size: Float) {
if (size != textScaleX) {
needPaginate = true
}
super.setTextScaleX(size)
}
override fun setTypeface(tf: Typeface?) {
if (typeface != null && tf != typeface) {
needPaginate = true
paint.typeface = tf
}
super.setTypeface(tf)
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
override fun setLetterSpacing(letterSpacing: Float) {
if (letterSpacing != this.letterSpacing) {
needPaginate = true
}
super.setLetterSpacing(letterSpacing)
}
override fun setHorizontallyScrolling(whether: Boolean) {
super.setHorizontallyScrolling(false)
}
override fun setLineSpacing(add: Float, mult: Float) {
if (add != lineSpacingExtra || mult != lineSpacingMultiplier) {
needPaginate = true
}
super.setLineSpacing(add, mult)
}
override fun setMaxLines(maxLines: Int) {
if (maxLines != this.maxLines) {
needPaginate = true
}
super.setMaxLines(maxLines)
}
override fun setLines(lines: Int) {
super.setLines(lines)
if (lines != this.lineCount) {
needPaginate = true
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
pageHeight = h - (paddingTop + paddingBottom) // 마진 제외
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
Blog.LOGD(log = "onLayout>> ${this::class.java.name} changed >> ${changed}")
if (changed || needPaginate) {
paginate()
setPageText()
needPaginate = false
}
}
fun doUpdate() {
if (needPaginate && layout != null) {
paginate()
setPageText()
needPaginate = false
}
}
private fun paginate() {
if (layout != null) {
MainScope().launch {
pageList.clear()
// Blog.LOGD(log = "paginate>> ${this::class.java.name} && ${layout.text}")
val layout = from(layout)
val lines = if(min(maxLines, layout.lineCount) > 10) {min(maxLines, layout.lineCount) - 1} else {min(maxLines, layout.lineCount)}
var startOffset = 0
val heightWithoutPaddings = pageHeight //- (marginTop + marginBottom + paddingTop + paddingBottom)
var height = heightWithoutPaddings
for (i in 0 until lines) {
if (height < layout.getLineBottom(i)) {
pageList.add(
layout.text.subSequence(startOffset, layout.getLineStart(i))
)
startOffset = layout.getLineStart(i)
height = layout.getLineTop(i) + heightWithoutPaddings
}
if (i == lines - 1) {
pageList.add(
if(layout.lineCount > i) {
layout.text.subSequence(startOffset, layout.getLineEnd(i + 1))
} else {
layout.text.subSequence(startOffset, layout.getLineEnd(i))
}
)
}
}
mPagedTextGenerateInterface?.completePagination(pageList)
}
}
}
private fun from(layout: Layout): Layout =
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
@Suppress("DEPRECATION")
StaticLayout(
originalText,
paint,
layout.width - paddingLeft - paddingRight, // margin 제외
layout.alignment,
lineSpacingMultiplier,
lineSpacingExtra,
includeFontPadding
)
} else {
StaticLayout.Builder
.obtain(
originalText, 0, originalText.length, paint,
(layout.width - paddingLeft - paddingRight)
)
.setAlignment(layout.alignment)
.setLineSpacing(lineSpacingExtra, lineSpacingMultiplier)
.setIncludePad(includeFontPadding)
.setUseLineSpacingFromFallbacks()
.setBreakStrategy(breakStrategy)
.setHyphenationFrequency(hyphenationFrequency)
.setMaxLines(maxLines)
.build()
}
private fun StaticLayout.Builder.setUseLineSpacingFromFallbacks(): StaticLayout.Builder {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
this.setUseLineSpacingFromFallbacks(isFallbackLineSpacing)
}
return this
}
private fun StaticLayout.Builder.setJustificationMode(): StaticLayout.Builder {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
this.setJustificationMode(justificationMode)
}
return this
}
}
interface PagedTextGenerateInterface {
fun completePagination(pageList: ArrayList<CharSequence>)
@ -297,26 +59,95 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
var mainTextView : TextView? = null
var sencondTextView : TextView? = null
var demp : View? = null
var hiddenTextView : PagedTextView? = null
// var hiddenTextView : PagedTextView? = null
var guideLine : Guideline? = null
var pageList: ArrayList<CharSequence>? = null
var pageList = mutableListOf<CharSequence>()
var summaryText : String = ""
var text : String = ""
set(new) {
// Blog.LOGE("field >> ${field}")
// Blog.LOGE("new >> ${new}")
field = new
val summary = new.replace(" " ,"").replace("\n" ,"").substring(0,Math.min(30,new.length))
if (summary.equals(summaryText)) {
if (summary.equals(summaryText) && summaryText.length > 100) {
} else {
// Blog.LOGE("field >> ${field}")
hiddenTextView?.setTxtF(field)
hiddenTextView?.visibility = VISIBLE
if (field.length > 0) {
post {
if (width > 0 && height > 0) {
MainScope().launch {
pageList.clear()
val contentWidth =
mainTextView!!.width - (mainTextView!!.paddingLeft + mainTextView!!.paddingRight)
val contentHeight =
mainTextView!!.height - (mainTextView!!.paddingTop + mainTextView!!.paddingBottom)
val pages = paginateAsync(text, contentWidth, contentHeight)
pageList.addAll(pages)
Blog.LOGE("pages >>> ${pages.size}")
setPageBy(0)
}
}
}
}
}
summaryText = summary
}
suspend fun paginateAsync(text: CharSequence, width: Int, height: Int): List<CharSequence> = withContext(Dispatchers.Default) {
val resultPages = ArrayList<CharSequence>()
val paint = mainTextView?.paint ?: TextPaint()
val lineSpacingMult = mainTextView?.lineSpacingMultiplier ?: 1.0f
val lineSpacingAdd = mainTextView?.lineSpacingExtra ?: 1.0f
val letterSpacing = mainTextView?.letterSpacing ?: 1.0f
val includePad = false
Blog.LOGE("text ${text.length}, width $width, height $height")
// 1. 전체 텍스트에 대한 StaticLayout 생성 (API 버전에 따른 분기 처리 권장)
val layout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
StaticLayout.Builder.obtain(text, 0, text.length, paint, width)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(lineSpacingAdd, lineSpacingMult)
.setIncludePad(includePad)
.build()
} else {
@Suppress("DEPRECATION")
StaticLayout(text, paint, width, Layout.Alignment.ALIGN_NORMAL, lineSpacingMult, lineSpacingAdd, includePad)
}
val totalLines = layout.lineCount
var startLine = 0
var startOffset = 0
// 2. 라인 정보를 순회하며 높이 계산
while (startLine < totalLines) {
// 현재 페이지의 시작 y 좌표
val pageTopY = layout.getLineTop(startLine)
// 현재 페이지가 끝날 수 있는 최대 y 좌표
val targetBottomY = pageTopY + height
// 이 페이지에 들어갈 수 있는 마지막 라인 찾기
var endLine = layout.getLineForVertical(targetBottomY)
// getLineForVertical은 정확한 좌표가 없으면 마지막 라인을 반환하므로 검증 필요
if (layout.getLineBottom(endLine) > targetBottomY) {
endLine--
}
// 한 줄도 못 들어가는 경우(폰트가 뷰보다 큰 경우 등) 방어 코드
if (endLine < startLine) endLine = startLine
// 페이지 범위의 텍스트 추출
// endLine의 끝 오프셋을 가져옴
val endOffset = layout.getLineEnd(endLine)
// 텍스트 자르기 (subSequence는 메모리 복사를 최소화함)
if (startOffset < endOffset) {
resultPages.add(text.subSequence(startOffset, endOffset))
}
// 다음 페이지 준비
startOffset = endOffset
startLine = endLine + 1
}
return@withContext resultPages
}
private val hanler = Handler()
var mPagedTextViewInterface : PagedTextViewInterface? = null
val touchTimeover = Runnable {
@ -328,11 +159,8 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
inflate(context, R.layout.layout_textviewer, this)
mainTextView = findViewById(R.id.first_view)
sencondTextView = findViewById(R.id.sencond_view)
demp = findViewById(R.id.demp)
currentPageTextView = findViewById(R.id.current_page)
hiddenTextView = findViewById(R.id.hidden_view)
hiddenTextView?.mPagedTextGenerateInterface = this
currentPageTextView?.text = ""
hanler.removeCallbacks(touchTimeover)
setOnLongClickListener { v ->
@ -450,8 +278,7 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
super.onLayout(changed, left, top, right, bottom)
Blog.LOGD(log = "onLayout>> ${this::class.java.name} changed >> ${changed}")
if(changed) {
hiddenTextView?.text = text
forceUpdateUI()
// forceUpdateUI()
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
@ -470,14 +297,12 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
var currentPage = 0
override fun completePagination(pageList: ArrayList<CharSequence>) {
// Blog.LOGD(log = "completePagination>> ${this::class.java.name} >> pageList ${pageList}")
if(text.length > 0 && pageList!= null && pageList.size == 0) {
} else {
this.pageList = pageList
setPageBy(0)
}
hiddenTextView?.visibility = GONE
}
var defaultAlpha = 171
fun setColorStyle(colors : Array<String>) {
@ -490,11 +315,7 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
sencondTextView?.setTextColor(Color.parseColor("#FFFFFF"))
}
// fun setPagedTextViewInterface(pagedTextViewInterface: PagedTextViewInterface) = hiddenTextView?.setPagedTextViewInterface(pagedTextViewInterface)
// fun setText(replace: String) = hiddenTextView?.setText(replace)
fun setTextSize(fl: Float) {
hiddenTextView?.setTextSize(fl)
mainTextView?.setTextSize(fl)
sencondTextView?.setTextSize(fl)
}
@ -508,22 +329,38 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
}
fun setPageBy(num : Int) {
this@PagedTextLayout.currentPage = num
var realPage = if(isDualPage()) this@PagedTextLayout.currentPage * 2 else this@PagedTextLayout.currentPage
Blog.LOGE("realPage = if(${pageList?.size} ?: 0 > ${realPage}) { realPage} else { ${(pageList?.size ?: 0) - 1 }}")
realPage = if(pageList?.size ?: 0 > realPage) { realPage} else { (pageList?.size ?: 0) - 1 }
currentPageTextView?.text = "${realPage + 1 }/${ pageList?.size ?: 0 + 1}"
if (pageList.size > 0) {
this@PagedTextLayout.currentPage = num
var realPage = if (isDualPage()) this@PagedTextLayout.currentPage * 2 else this@PagedTextLayout.currentPage
Blog.LOGE("realPage = if(${pageList?.size} ?: 0 > ${realPage}) { realPage} else { ${(pageList?.size ?: 0) - 1}}")
realPage = if (pageList?.size ?: 0 > realPage) {
realPage
} else {
(pageList?.size ?: 0) - 1
}
currentPageTextView?.text = "${realPage + 1}/${pageList?.size ?: 0 + 1}"
mainTextView?.text = pageList?.get(realPage) ?: "NONE"
if(isDualPage()) {
realPage = realPage.inc()
sencondTextView?.text = if(pageList?.size ?: 0 > realPage) { pageList?.get(realPage)} else { ""}
} else {
sencondTextView?.text = ""
mainTextView?.text = pageList?.get(realPage) ?: "NONE"
Blog.LOGE("pageList.get($realPage) ${pageList?.get(realPage)}")
if (isDualPage()) {
realPage = realPage.inc()
sencondTextView?.text = if (pageList?.size ?: 0 > realPage) {
pageList?.get(realPage)
} else {
""
}
} else {
sencondTextView?.text = ""
}
}
}
fun size(): Int = if(isDualPage()) Math.round((hiddenTextView?.size() ?:0) * 0.5f) else hiddenTextView?.size() ?: 0
fun size(): Int = (if(isDualPage()) {
Math.round(pageList.size * 0.5f)
}else {
pageList.size
})
fun getFastPageCount() = if(isDualPage()) 3 else 6
fun current(): Int = this@PagedTextLayout.currentPage
fun doNext(fast : Boolean = false) {
@ -547,33 +384,26 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
}
}
fun forceUpdateUI() {
hiddenTextView?.doUpdate()
}
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
hiddenTextView?.setPadding(left,top,right, bottom)
mainTextView?.setPadding(left,top,right, bottom)
sencondTextView?.setPadding(left,top,right, bottom)
}
fun setTypeface(tf: Typeface?) {
hiddenTextView?.setTypeface(tf)
mainTextView?.setTypeface(tf)
sencondTextView?.setTypeface(tf)
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
fun setLetterSpacing(letterSpacing: Float) {
hiddenTextView?.letterSpacing = letterSpacing.times(0.01f)
mainTextView?.letterSpacing = letterSpacing.times(0.01f)
sencondTextView?.letterSpacing = letterSpacing.times(0.01f)
}
fun setLineSpacing(mult: Float) {
hiddenTextView?.setLineSpacing(1f, 1f.plus(mult.times(0.01f)))
mainTextView?.setLineSpacing(1f, 1f.plus(mult.times(0.01f)))
sencondTextView?.setLineSpacing(1f, 1f.plus(mult.times(0.01f)))
}

View File

@ -6,32 +6,6 @@
android:layout_margin="5dp"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_margin="5dp"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<bums.lunatic.launcher.home.tokiz.view.PagedTextView
android:layout_margin="5dp"
android:id="@+id/hidden_view"
android:visibility="visible"
android:layout_width="0dp"
android:layout_weight="1"
android:includeFontPadding="false"
android:lineSpacingExtra="0dp"
style="@style/sss"
android:layout_height="match_parent"/>
<View
android:id="@+id/demp"
android:visibility="invisible"
android:layout_margin="5dp"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent"/>
</LinearLayout>
<LinearLayout
android:layout_margin="5dp"
android:layout_alignParentStart="true"
@ -55,7 +29,7 @@
android:gravity="start"
android:includeFontPadding="false"
android:lineSpacingExtra="0dp"
android:visibility="invisible"
android:visibility="gone"
android:id="@+id/sencond_view"
android:layout_width="0dp"
android:layout_weight="1"