diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt index 1f39e00a..7933061e 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt @@ -1,6 +1,7 @@ package bums.lunatic.launcher.home import CustomVideoNodeRenderer +import android.annotation.SuppressLint import android.app.Dialog import android.content.Context import android.content.DialogInterface @@ -8,22 +9,14 @@ import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.net.Uri +import android.os.Build import android.os.Handler import android.os.Looper import android.util.AttributeSet import android.util.Base64 import android.util.Log import android.view.KeyEvent -import android.view.KeyEvent.ACTION_UP -import android.view.KeyEvent.KEYCODE_BUTTON_A -import android.view.KeyEvent.KEYCODE_BUTTON_B -import android.view.KeyEvent.KEYCODE_BUTTON_SELECT -import android.view.KeyEvent.KEYCODE_BUTTON_START -import android.view.KeyEvent.KEYCODE_BUTTON_X -import android.view.KeyEvent.KEYCODE_BUTTON_Y -import android.view.KeyEvent.KEYCODE_DPAD_DOWN -import android.view.KeyEvent.KEYCODE_DPAD_UP -import android.view.LayoutInflater +import android.view.MotionEvent import android.view.PointerIcon import android.view.View import android.widget.CheckBox @@ -39,32 +32,36 @@ import androidx.core.net.toUri import androidx.core.view.isVisible import bums.lunatic.launcher.BookmarkUploader 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.home.NeoRssActivity.Companion.getRuntime +import bums.lunatic.launcher.home.tokiz.PortMessage import bums.lunatic.launcher.model.Dotax import bums.lunatic.launcher.model.DotaxArticles import bums.lunatic.launcher.model.getRssData -import bums.lunatic.launcher.home.tokiz.PortMessage -import bums.lunatic.launcher.home.tokiz.view.BWebview import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.utils.CommonUtils +import bums.lunatic.launcher.utils.SimpleFingerGestures import bums.lunatic.launcher.workers.WorkersDb import com.google.android.material.textfield.TextInputEditText import com.google.gson.Gson import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter 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 kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request import org.json.JSONException import org.json.JSONObject import org.jsoup.Jsoup -import org.jsoup.nodes.Document import org.mozilla.gecko.util.ThreadUtils -import org.mozilla.geckoview.ExperimentDelegate import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.GeckoView import org.mozilla.geckoview.MediaSession import org.mozilla.geckoview.WebExtension import org.mozilla.geckoview.WebExtension.MessageDelegate @@ -78,1127 +75,488 @@ import java.io.InputStream import java.text.SimpleDateFormat import java.util.Date -class GeckoWeb : BWebview { - constructor(context: Context?) : super(context) { - buildWeb() +// BWebview와 GeckoWeb을 통합한 클래스 +open class GeckoWeb @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : GeckoView(context, attrs) { + + // --- 1. Properties & Initialization --- + + // UI & State + var decoViews = arrayListOf() + var progress: ProgressBar? = null + var lastedUrl: String? = null + var lastDomain: String? = null + var canGoBack: Boolean? = null + var privateMode = false + var currentTitle = "" + var dialog: Dialog? = null + + // Callbacks + interface OnSave { fun saved() } + var mOnSave: OnSave? = null + + enum class JxEvent { SCROLL_UP, SCROLL_DOWN, SWIPE_LEFT, SWIPE_RIGHT, ON_CLICK } + var jxInteface: ((JxEvent) -> Unit)? = null + + // Callbacks for external linkage (UniversalToki 등에서 사용) + var onPageStartCallback: ((String) -> Unit)? = null + var onPageStopCallback: ((Boolean) -> Unit)? = null + var onLocationChangeCallback: ((String) -> Unit)? = null + var onSessionStateChangeCallback: ((GeckoSession.SessionState) -> Unit)? = null + var onPortMessageCallback: ((PortMessage) -> Unit)? = null + + // Extension Info + private val mPortNam = "browser" + private val extPath = "resource://android/assets/extensions/my_extension/" + private val extId = "messaging@booktoki468.com" + var mPort: WebExtension.Port? = null + var mExtension: WebExtension? = null + + // Touch Handling + private var lastX = 0f + private var lastY = 0f + private val mSimpleFingerGestures = SimpleFingerGestures(object : SimpleFingerGestures.OnFingerGestureListener { + override fun onSwipeUp(targetView: View, fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean { + jxInteface?.invoke(JxEvent.SCROLL_UP); return true + } + override fun onSwipeDown(targetView: View, fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean { + jxInteface?.invoke(JxEvent.SCROLL_DOWN); return true + } + override fun onSwipeLeft(targetView: View, fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean { + jxInteface?.invoke(JxEvent.SWIPE_LEFT); return true + } + override fun onSwipeRight(targetView: View, fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean { + jxInteface?.invoke(JxEvent.SWIPE_RIGHT); return true + } + override fun onClick(targetView: View, fingers: Int): Boolean { + jxInteface?.invoke(JxEvent.ON_CLICK); return true + } + override fun onPinch(targetView: View, fingers: Int, gestureDuration: Long, gestureDistance: Double) = true + override fun onUnpinch(targetView: View, fingers: Int, gestureDuration: Long, gestureDistance: Double) = true + override fun onDoubleTap(targetView: View, fingers: Int) = true + override fun onLongPress(targetView: View, fingers: Int) = true + }) + + class GKCookie { var COOKIES: String? = null } + var mGKCookie: GKCookie? = null + + // Markdown/Scraping + var markdownContents: String? = null + var markdownUri: Uri? = null + + companion object { + var currentRetryCount = 0 } + + + + // --- 2. Public API & Features --- + override fun setVisibility(visibility: Int) { super.setVisibility(visibility) - decoViews.filter { it != null && it.id > -1 && it.id != R.id.dl_video }.forEach { it.visibility = visibility } - } - interface OnSave { - fun saved() - } - var mOnSave : OnSave? = null - constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { - buildWeb() - } - var progress : ProgressBar? = null - val mPortNam = "browser" - val extPath = "resource://android/assets/extensions/my_extension/" - val extId = "messaging@booktoki468.com" - - private fun buildWeb() { - - getRuntime()?.let { - val session: GeckoSession = GeckoSession() - session.open(it) - this.setSession(session) - - session.contentDelegate = contentDelegate - session.progressDelegate = progressDelegate - session.navigationDelegate = navigationDelegate - it.webExtensionController.setAddonManagerDelegate(addonManagerDelegate) - session.mediaDelegate = mediaDelegate - session.promptDelegate = promptDelegate - session.mediaSessionDelegate = mediaSessionDelegate - - it.webExtensionController - .ensureBuiltIn(extPath, extId) - .accept( // Register message delegate for background script - { extension: WebExtension? -> - ThreadUtils.runOnUiThread( - Runnable { - if (extension != null) { - session.webExtensionController.setMessageDelegate( - extension, - messageDelegate, - mPortNam - ) - } - }) - }, - { e: Throwable? -> Log.e("MessageDelegate", "Error registering WebExtension", e) }) - } + decoViews.filter { it.id > -1 && it.id != R.id.dl_video }.forEach { it.visibility = visibility } } - var lastedUrl: String? = null - var canGoBack: Boolean? = null - var mPort: WebExtension.Port? = null - var mCaache : WebExtension.Port? = null - var privateMode = false - set(value) { - Blog.LOGE("Current Mode = $field") - Blog.LOGE("Current Mode = $value") - field = value - } - object WebExtensionInfo { - val mPortNam = "browser" - val extPath = "resource://android/assets/extensions/my_extension/" - val extId = "messaging@booktoki468.com" - } - fun sendScrollDown(isUp: Boolean) { - val message: JSONObject = JSONObject() - try { - message.put("type", "scrollDown") - message.put("isUpDown", if (isUp) +1 else -1) - } catch (ex: JSONException) { - throw RuntimeException(ex) - } - Blog.LOGE(Gson().toJson(message)) - mPort?.postMessage(message) - } - fun sendSearch(keyword: String) { - val message: JSONObject = JSONObject() - try { - message.put("type", "search") - message.put("keyword", keyword) - } catch (ex: JSONException) { - throw RuntimeException(ex) - } - Blog.LOGE(Gson().toJson(message)) - mPort?.postMessage(message) - } - - fun sendSearchDo() { - val message: JSONObject = JSONObject() - try { - message.put("type", "searchDo") - } catch (ex: JSONException) { - throw RuntimeException(ex) - } - Blog.LOGE(Gson().toJson(message)) - mPort?.postMessage(message) - } - - var mExtension: WebExtension? = null - var mSession: GeckoSession? = null - val addonManagerDelegate = object : AddonManagerDelegate { - override fun onReady(extension: WebExtension) { - Blog.LOGE("onReady ${extension.id} from WebExtension") - mExtension = extension + open fun loadUrl(url: String, param: String? = null) { + var nUrl = url + if (url.endsWith("=")) { + try { + nUrl = String(Base64.decode(url, Base64.DEFAULT)) + param?.let { nUrl += it } + } catch (e: Exception) { nUrl = url } + } else if (!url.startsWith("http")) { + nUrl = lastedUrl ?: url + } else if (param != null && param.length > 1) { + nUrl += param } - override fun onEnabling(extension: WebExtension) { - Blog.LOGE("onEnabling ${extension.id} from WebExtension") - } - - override fun onEnabled(extension: WebExtension) { - Blog.LOGE("onEnabled ${extension.id} from WebExtension") - mExtension = extension + if (!privateMode && !this.isVisible) this.visibility = VISIBLE + nUrl.let { + val finalUrl = if (it.split("//").size > 1) it.replace("//", "/").replace("https:/", "https://") else it + Blog.LOGE("Loading: $finalUrl") + this.session?.loadUri(finalUrl) } + currentRetryCount = 0 } - val promptDelegate = object : GeckoSession.PromptDelegate { - override fun onAlertPrompt( - session: GeckoSession, - prompt: GeckoSession.PromptDelegate.AlertPrompt - ): GeckoResult? { - if(lastedUrl?.contains("daum.net/dotax") == true) { - try { - Gson().fromJson(prompt.message,DotaxArticles::class.java)?.let { - Blog.LOGE("it.Articles >>> ${it.Articles?.size}") - it.Articles?.forEach { - Dotax("${it.fldid}/${it.dataid}","dotax",it.articleElapsedTime!!,it.title!!,it.thumbnailImageUrl!!)?.let { - it.getRssData().let { - WorkersDb.insertData(it) - } - } + + fun checkIfDownloadable(url: String) { + // UI 초기화 + post { + decoViews.firstOrNull { it.id == R.id.dl_video }?.let { + it.setOnClickListener {} + it.visibility = GONE + } + } + + 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") } } - } catch (e: Exception) { - + cookieFile.writeText(content) + request.addOption("--cookies", cookieFile.absolutePath) } - } - Blog.LOGE("prompt >>> ${prompt.message}") - prompt.dismiss() - return super.onAlertPrompt(session, prompt) - } - } - val mediaDelegate = object : GeckoSession.MediaDelegate { - override fun onRecordingStatusChanged( - session: GeckoSession, - devices: Array - ) { - super.onRecordingStatusChanged(session, devices) - } - } - val mediaSessionDelegate = object : MediaSession.Delegate { - override fun onActivated( - session: GeckoSession, - mediaSession: MediaSession - ) { - Blog.LOGE("onActivated") - super.onActivated(session, mediaSession) - } - override fun onDeactivated( - session: GeckoSession, - mediaSession: MediaSession - ) { - Blog.LOGE("onDeactivated") - super.onDeactivated(session, mediaSession) - } - - override fun onMetadata( - session: GeckoSession, - mediaSession: MediaSession, - meta: MediaSession.Metadata - ) { - Blog.LOGE("onMetadata ${Gson().toJson(meta)}") - super.onMetadata(session, mediaSession, meta) - } - - override fun onFeatures( - session: GeckoSession, - mediaSession: MediaSession, - features: Long - ) { - Blog.LOGE("onFeatures $features") - super.onFeatures(session, mediaSession, features) - } - - override fun onPlay( - session: GeckoSession, - mediaSession: MediaSession - ) { - Blog.LOGE("onPlay $mediaSession") - super.onPlay(session, mediaSession) - } - - override fun onPause( - session: GeckoSession, - mediaSession: MediaSession - ) { - Blog.LOGE("onPause") - super.onPause(session, mediaSession) - } - - override fun onStop( - session: GeckoSession, - mediaSession: MediaSession - ) { - Blog.LOGE("onStop $mediaSession") - super.onStop(session, mediaSession) - } - - override fun onPositionState( - session: GeckoSession, - mediaSession: MediaSession, - state: MediaSession.PositionState - ) { - Blog.LOGE("onPositionState $state") - super.onPositionState(session, mediaSession, state) - } - - override fun onFullscreen( - session: GeckoSession, - mediaSession: MediaSession, - enabled: Boolean, - meta: MediaSession.ElementMetadata? - ) { - Blog.LOGE("onFullscreen $Boolean ${Gson().toJson(meta)}") - super.onFullscreen(session, mediaSession, enabled, meta) - } - } - - val experimentDelegate = object : ExperimentDelegate { - override fun onGetExperimentFeature(feature: String): GeckoResult { - Blog.LOGE("onGetExperimentFeature $feature") - return super.onGetExperimentFeature(feature) - } - - override fun onRecordExposureEvent(feature: String): GeckoResult { - Blog.LOGE("onRecordExposureEvent $feature") - return super.onRecordExposureEvent(feature) - } - - override fun onRecordExperimentExposureEvent( - feature: String, - slug: String - ): GeckoResult { - Blog.LOGE("onRecordExperimentExposureEvent $feature , $slug") - return super.onRecordExperimentExposureEvent(feature, slug) - } - - override fun onRecordMalformedConfigurationEvent( - feature: String, - part: String - ): GeckoResult { - Blog.LOGE("onRecordMalformedConfigurationEvent $feature , $part") - return super.onRecordMalformedConfigurationEvent(feature, part) - } - } - - fun showImageDownloadDialog(context: Context, imageUrl: Uri) { - - } - - fun savePdfStreamToFile(pdfStream: InputStream?, outputFile: File) { - pdfStream?.let { - FileOutputStream(outputFile).use { output -> - val buffer = ByteArray(8 * 1024) - var bytesRead: Int - while (pdfStream.read(buffer).also { bytesRead = it } != -1) { - output.write(buffer, 0, bytesRead) + val videoInfo = YoutubeDL.getInstance().getInfo(request) + if (videoInfo != null && !videoInfo.title.isNullOrEmpty()) { + post { + decoViews.firstOrNull { it.id == R.id.dl_video }?.let { view -> + view.setOnClickListener { videoDownload(url) } + view.visibility = VISIBLE + } + } } - output.flush() + } catch (e: Exception) { + // Log.e("GeckoWeb", "Download Check Error", e) } - pdfStream.close() } } - // 실제 사용 예시 - - fun scrapWebToMd(context: Context, url: Uri?, markdownText: String) { - // 경로 선언 (예: /storage/emulated/0/fff_gg/pdfs) - val dir = File("/storage/emulated/0/bums_ob/BUM'S PACED /scraped/md") - ///BUM'S PACED/pdfs - if (!dir.exists()) { - dir.mkdirs() - } else { -// dir.listFiles().forEach { Blog.LOGE("child -> ${it.absolutePath}") } + fun videoDownload(videoUrl: String) { + val intent = Intent(context, ForeGroundService::class.java).apply { + action = ACTION_VIDEO_DOWNLOAD + putExtra(EXTRA_TARGET_URL, videoUrl) } - val outputFile = File(dir, "${url?.host ?: "UnKnown"}_scraped_${SimpleDateFormat("yyyyMMdd-HHmm").format(Date())}.md") - outputFile.writeText(markdownText, Charsets.UTF_8) - // 저장 성공 알림 등 후속 처리 - println("PDF 저장 완료: ${outputFile.absolutePath}") - - mOnSave?.saved() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent) + else context.startService(intent) } - fun replaceDcUrl(origin: String): String { - var result = origin - for (i in 0..19) { - result = result.replace(String.format("dcimg%d.", i), "dcimg2.") - } - return result - } + // --- 3. Delegates (선언 위치를 위로 올려 초기화 보장) --- -// fun copyToClipboard(text: String?) { -// if (text == null) return -// val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager -// val clip = ClipData.newPlainText("Media URL", text) -// clipboard.setPrimaryClip(clip) -// Toast.makeText(context, "주소가 복사되었습니다.", Toast.LENGTH_SHORT).show() -// } - - - suspend fun getFormatList(url: String): List = withContext(Dispatchers.IO) { - val command = YoutubeDLRequest(lastedUrl!!) - command.addOption("--list-formats", url) - val output = YoutubeDL.getInstance().execute(command) - // output.stdout에 포맷 리스트가 문자열로 들어있음 - // 줄 단위로 분리 후 리턴 - Blog.LOGE("output.out >>> ${output.out}") - return@withContext output.out.split("\n").filter { it.isNotBlank() } - } - - fun showFormatSelectionDialog(formats: List, onFormatSelected: (String) -> Unit) { - val builder = AlertDialog.Builder(context) - builder.setTitle("포맷 선택") - builder.setItems(formats.toTypedArray()) { _, which -> - onFormatSelected(formats[which]) - } - builder.setNegativeButton("취소", null) - builder.show() - } - - - - - var dialog : Dialog? = null - fun getFilterF() = String(java.util.Base64.getMimeDecoder().decode("aHR0cHM6Ly9pamF2dG9ycmVudC5jb20=".toByteArray())) - var currentTitle = "" - val contentDelegate = object : GeckoSession.ContentDelegate { - override fun onTitleChange( - session: GeckoSession, - title: String? - ) { - Blog.LOGE("onTitleChange $title") + // [Content Delegate] + private val contentDelegate = object : GeckoSession.ContentDelegate { + override fun onTitleChange(session: GeckoSession, title: String?) { currentTitle = title ?: "" - super.onTitleChange(session, title) } - - override fun onCrash(session: GeckoSession) { - Blog.LOGE("onCrash") - super.onCrash(session) - } - - override fun onPaintStatusReset(session: GeckoSession) { - super.onPaintStatusReset(session) - } - - override fun onFirstContentfulPaint(session: GeckoSession) { - - super.onFirstContentfulPaint(session) - } - - override fun onExternalResponse(session: GeckoSession, response: WebResponse) { - Blog.LOGE("response >>> ${response.uri} ") if (response.uri.contains(".apk")) return - - val url = response.uri - val filename = "${url.substringAfterLast("/")}_${System.currentTimeMillis()}.mp4" // 파일명 추출, 없으면 URL에서 가져옴 - - // 저장 경로 결정 - val savePath = File(context.getExternalFilesDir(null), filename) - - // OkHttp로 파일 다운로드 - val client = OkHttpClient() - val request = Request.Builder() - .url(url) - .addHeader("Referer", lastedUrl ?: "") - .addHeader("User-Agent", "Mozilla/5.0") - // 필요시 Referer, 쿠키 등 헤더 추가 - .build() - - Thread { // 네트워크로 인한 별도 스레드 필요(코루틴도 가능) - val responseOk = client.newCall(request).execute() - if (responseOk.isSuccessful) { - responseOk.body?.byteStream()?.use { input -> - FileOutputStream(savePath).use { output -> - input.copyTo(output) - } - } - - // 다운로드 완료 알림 - Handler(Looper.getMainLooper()).post { - Toast.makeText(context, "파일 저장 완료: ${savePath.name}", Toast.LENGTH_SHORT).show() - } - } else { - Handler(Looper.getMainLooper()).post { - Toast.makeText(context, "다운로드 실패: ${responseOk.message}", Toast.LENGTH_SHORT).show() - } - } - dialog?.dismiss() - }.start() - super.onExternalResponse(session, response) + downloadFile(response.uri) } + override fun onContextMenu(session: GeckoSession, screenX: Int, screenY: Int, element: GeckoSession.ContentDelegate.ContextElement) { + val pageUrl = element.baseUri ?: lastedUrl ?: return + val mediaUrl = element.srcUri ?: return -// private fun startBookmarkSaveProcess(pageUrl: String, mediaUrl: String) { -// // 1. 로그인 상태 확인 -// if (BookmarkUploader.isUserLoggedIn) { -// // 이미 로그인 되어 있으면, 바로 북마크 정보 입력창 표시 -// showBookmarkDetailsDialog(pageUrl, mediaUrl) -// } else { -// // 로그인이 안 되어 있으면, 로그인 창을 먼저 표시 -// showLoginDialog { -// // 로그인 성공 시 콜백으로 북마크 정보 입력창 표시 -// showBookmarkDetailsDialog(pageUrl, mediaUrl) -// } -// } -// } + if (element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE || + element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_VIDEO) { - - - override fun onContextMenu( - session: GeckoSession, - screenX: Int, - screenY: Int, - element: GeckoSession.ContentDelegate.ContextElement - ) { - - - val pageUrl = element.baseUri ?: lastedUrl ?: return // 페이지 URL - var mediaUrl = element.srcUri ?: return // 이미지 또는 비디오 URL - - // 컨텍스트 메뉴에 '북마크 저장' 옵션 추가 - val menuItems = listOf("이 미디어만 북마크", "페이지의 모든 이미지 북마크", "다운로드") - - val isMediaElement = element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE || - element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_VIDEO - if (!isMediaElement) { - super.onContextMenu(session, screenX, screenY, element) - return - } -// if (isMediaElement) { -// menuItems.add("이 미디어 북마크하기") -// menuItems.add("이 미디어 다운로드하기") -// } -// -// if (menuItems.isEmpty()) { -// super.onContextMenu(session, screenX, screenY, element) -// return -// } - fun show(pageUrl : String,mediaUrl : String) { - AlertDialog.Builder(context) - .setTitle("작업 선택") - .setItems(menuItems.toTypedArray()) { _, which -> - when (menuItems[which]) { - "이 미디어만 북마크" -> { - // 기존 로직: 단일 이미지 북마크 프로세스 시작 - startBookmarkSaveProcessForSingleImage(pageUrl, mediaUrl) - } - "페이지의 모든 이미지 북마크" -> { - // [신규 로직] WebExtension에 모든 이미지 URL 요청 - val message = JSONObject() - message.put("type", "fetchAllImages") - message.put("targetSrc", mediaUrl) // 기준이 되는 이미지 URL 전달 - mPort?.postMessage(message) - } - "다운로드" -> { - CommonUtils.downloadFileWithOkHttp(context, Uri.parse(pageUrl), mediaUrl) - } - } - } - .show() - } - - - - if (element.baseUri?.contains("youtube") == true) { -// lastedUrl?.let { videoUrl -> -// lastedUrl?.let { -// videoDlownLoad(it) -// } -// } + // 로그인 확인 후 메뉴 표시 + BookmarkUploader.loginAndGetToken( + userId = "lunaticbum", userPw = "VioPup*383", + onSuccess = { showContextMenu(pageUrl, mediaUrl); context.toast("로그인 성공") }, + onError = { context.toast("로그인 실패: $it") } + ) } else { - Blog.LOGE("onContextMenu:: x = ${x}, y = ${y} , element = ${Gson().toJson(element)}") -// if (!BookmarkUploader.isUserLoggedIn) { - BookmarkUploader.loginAndGetToken( - userId = "lunaticbum", - userPw = "VioPup*383", - onSuccess = { - show(pageUrl, mediaUrl) - Toast.makeText(context, "로그인 성공!", Toast.LENGTH_SHORT).show() - }, - onError = { errorMessage -> - Toast.makeText(context, "로그인 실패: $errorMessage", Toast.LENGTH_LONG).show() - } - ) -// } else { -//// show(pageUrl, mediaUrl) -// } - } - super.onContextMenu(session, screenX, screenY, element) - } - - - } - - /** - * 사용자에게 로그인을 요청하는 다이얼로그를 표시하는 함수 - * @param onLoginSuccess 로그인 성공 시 실행될 콜백 함수 - */ - private fun showLoginDialog(onLoginSuccess: () -> Unit) { - val view = LayoutInflater.from(context).inflate(R.layout.dialog_login, null) // dialog_login.xml 레이아웃 필요 - val userIdInput = view.findViewById(R.id.et_user_id) - val userPwInput = view.findViewById(R.id.et_user_pw) - - AlertDialog.Builder(context) - .setTitle("로그인 필요") - .setMessage("북마크를 저장하려면 로그인이 필요합니다.") - .setView(view) - .setPositiveButton("로그인") { dialog, _ -> - val userId = userIdInput.text.toString().trim() - val userPw = userPwInput.text.toString().trim() - - if (userId.isNotEmpty() && userPw.isNotEmpty()) { - // 입력된 정보로 로그인 시도 - BookmarkUploader.loginAndGetToken( - userId = userId, - userPw = userPw, - onSuccess = { - Toast.makeText(context, "로그인 성공!", Toast.LENGTH_SHORT).show() - onLoginSuccess() // 로그인 성공 콜백 실행 - }, - onError = { errorMessage -> - Toast.makeText(context, "로그인 실패: $errorMessage", Toast.LENGTH_LONG).show() - } - ) - } else { - Toast.makeText(context, "아이디와 비밀번호를 모두 입력해주세요.", Toast.LENGTH_SHORT).show() - } - } - .setNegativeButton("취소", null) - .show() - } - - /** - * 북마크 저장을 위해 사용자로부터 코멘트와 공개 여부를 입력받는 다이얼로그 (수정된 버전) - */ - private fun showBookmarkDetailsDialog(pageUrl: String, mediaUrls: List) { - val context = this@GeckoWeb.context ?: return - val visibilityOptions = arrayOf("PUBLIC", "MEMBERS", "PRIVATE") - - // --- UI 요소들을 담을 컨테이너 레이아웃 --- - val containerLayout = LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - val padding = (20 * resources.displayMetrics.density).toInt() // 패딩 값 조정 - setPadding(padding, padding, padding, padding) - } - - // --- 1. 코멘트 입력 EditText --- - val commentInput = EditText(context).apply { - hint = "코멘트를 입력하세요 (선택)" - } - containerLayout.addView(commentInput) // 컨테이너에 추가 - - // --- 2. 공개 범위 선택 RadioGroup --- - val radioGroup = RadioGroup(context).apply { - orientation = RadioGroup.VERTICAL - val marginTop = (16 * resources.displayMetrics.density).toInt() - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ).apply { - this.topMargin = marginTop - } - } - - // visibilityOptions 배열을 기반으로 라디오 버튼 동적 생성 - visibilityOptions.forEachIndexed { index, option -> - val radioButton = RadioButton(context).apply { - text = option - id = index // 각 라디오 버튼에 고유 ID 부여 - } - radioGroup.addView(radioButton) - } - radioGroup.check(0) // 첫 번째 옵션(PUBLIC)을 기본값으로 선택 - - containerLayout.addView(radioGroup) // 컨테이너에 RadioGroup 추가 - - - // --- 다이얼로그 생성 --- - AlertDialog.Builder(context) - .setTitle("북마크 저장") - .setView(containerLayout) // 직접 만든 레이아웃을 View로 설정 - .setPositiveButton("저장") { _, _ -> - val comment = commentInput.text.toString() - - // 선택된 라디오 버튼의 텍스트를 가져와 visibility로 사용 - val checkedRadioButtonId = radioGroup.checkedRadioButtonId - val visibility = (radioGroup.findViewById(checkedRadioButtonId))?.text.toString() - - if (visibility.isNotEmpty()) { - BookmarkUploader.saveBookmarkWithContent(pageUrl, mediaUrls, comment, visibility) - Toast.makeText(context, "[$visibility] 북마크로 ${mediaUrls.size}개 이미지를 저장했습니다.", Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(context, "공개 범위가 선택되지 않았습니다.", Toast.LENGTH_SHORT).show() - } - } - .setNegativeButton("취소", null) - .show() - } - - private fun startBookmarkSaveProcessForSingleImage(pageUrl: String, mediaUrl: String) { - if (BookmarkUploader.isUserLoggedIn) { - showBookmarkDetailsDialog(pageUrl, listOf(mediaUrl)) // URL을 리스트로 감싸서 전달 - } else { - showLoginDialog { - showBookmarkDetailsDialog(pageUrl, listOf(mediaUrl)) + super.onContextMenu(session, screenX, screenY, element) } } } - // [신규 추가] 여러 이미지 북마크 저장 프로세스를 시작하는 함수 - private fun startBookmarkSaveProcessForMultipleImages(pageUrl: String, mediaUrls: List) { - if (BookmarkUploader.isUserLoggedIn) { - showBookmarkDetailsDialog(pageUrl, mediaUrls) - } else { - showLoginDialog { - showBookmarkDetailsDialog(pageUrl, mediaUrls) - } - } - } - // [신규 추가] 단일 이미지 북마크 저장 프로세스를 시작하는 함수 - - - val progressDelegate = object : GeckoSession.ProgressDelegate { - override fun onSecurityChange( - session: GeckoSession, - securityInfo: GeckoSession.ProgressDelegate.SecurityInformation - ) { - Blog.LOGE("onSecurityChange $securityInfo from WebExtension") - super.onSecurityChange(session, securityInfo) - } - - override fun onSessionStateChange( - session: GeckoSession, - sessionState: GeckoSession.SessionState - ) { - Blog.LOGE("onSessionStateChange $sessionState from WebExtension") - super.onSessionStateChange(session, sessionState) - } - - override fun onProgressChange(session: GeckoSession, progress: Int) { - super.onProgressChange(session, progress) - this@GeckoWeb.progress?.setProgress(progress,true) - } - override fun onPageStart(session: GeckoSession, url: String) { - super.onPageStart(session, url) - markdownContents = null - markdownUri = null - if (url.contains(getFilterF()) && privateMode) { - this@GeckoWeb.visibility = View.INVISIBLE - } - if (url?.startsWith("magnet:?") == true) { - Uri.parse(url)?.let { - context.startActivity(Intent().apply { - action = Intent.ACTION_VIEW - flags = - Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS.or(FLAG_ACTIVITY_CLEAR_TOP) - .or( - FLAG_ACTIVITY_NEW_TASK - ) - data = it - }) - } - } - } - - override fun onPageStop(session: GeckoSession, success: Boolean) { - Blog.LOGE("onPageStop $success from WebExtension") - super.onPageStop(session, success) - if (success && mPort != null) { - if (mPort == null) { - return - } - - } - - - } - } - - val navigationDelegate = object : GeckoSession.NavigationDelegate { - override fun onLoadError( - session: GeckoSession, - uri: String?, - error: WebRequestError - ): GeckoResult? { - error.printStackTrace() - Blog.LOGE("onLoadError >>> ${uri} ::>> ${error.category} , ${error.code}") - if (error.code == WebRequestError.ERROR_NET_RESET) { - - } - return super.onLoadError(session, uri, error) - } - - override fun onNewSession( - session: GeckoSession, - uri: String - ): GeckoResult? { - Blog.LOGE("GeckoView", "onNewSession: $session from WebExtension") - + // [Navigation Delegate] + private val navigationDelegate = object : GeckoSession.NavigationDelegate { + override fun onNewSession(session: GeckoSession, uri: String): GeckoResult? { Uri.parse(uri)?.let { - if(it.host?.let { it1 -> - lastedUrl?.contains( - it1, - true - ) - } == true || - ((it.host?.contains("x.com") ?: false) == true) || - ((it.host?.contains("www.instagram.com") ?: false) == true) - ) { + if (it.host?.let { h -> lastedUrl?.contains(h, true) } == true || + it.host?.contains("x.com") == true || + it.host?.contains("instagram.com") == true) { loadUrl(uri) - } else if(uri.contains("googlevideo.com")) { - CommonUtils.downloadFileWithOkHttp(context, Uri.parse(lastedUrl),uri) + } else if (uri.contains("googlevideo.com")) { + CommonUtils.downloadFileWithOkHttp(context, Uri.parse(lastedUrl), uri) } else { - val builder: AlertDialog.Builder = AlertDialog.Builder(context) - builder.setTitle("Move To\n${uri}") - val viewInflated: View = LayoutInflater.from(context) - .inflate(R.layout.text_inpu_password, null, false) - val input = viewInflated.findViewById(R.id.input) as EditText - (viewInflated.findViewById(R.id.add_vote) as CheckBox)?.let { it.visibility = View.GONE } - (viewInflated.findViewById(R.id.add_read) as CheckBox)?.let { it.visibility = View.GONE } - (viewInflated.findViewById(R.id.private_mode) as CheckBox)?.let { it.visibility = View.GONE } - input.visibility = View.GONE - builder.setView(viewInflated) - builder.setPositiveButton( - "브라우저로 이동", - DialogInterface.OnClickListener { dialog, which -> - dialog.dismiss() - context.startActivity(Intent().apply { - action = Intent.ACTION_VIEW - flags = Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS.or(FLAG_ACTIVITY_CLEAR_TOP).or( - FLAG_ACTIVITY_NEW_TASK) - data = it - }) - }) - builder.setNeutralButton( - "페이지 이동", - DialogInterface.OnClickListener { dialog, which -> - loadUrl(uri) - dialog.cancel() - }) - builder.setNegativeButton( - android.R.string.cancel, - DialogInterface.OnClickListener { dialog, which -> dialog.cancel() }) - - builder.show() - + showNewSessionDialog(uri) } } return super.onNewSession(session, uri) } - override fun onLocationChange( - session: GeckoSession, - url: String?, - perms: MutableList, - hasUserGesture: Boolean - ) { - // url이 현재 로드된 주소입니다. - Blog.LOGE("GeckoView", "현재 주소: $url") - Blog.LOGE("GeckoView", "현재 session: $session") - val nullCursor = PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL) - this@GeckoWeb.setPointerIcon(nullCursor) - url?.let { url -> - if (url?.contains(getFilterF()) == true && privateMode) { - this@GeckoWeb.visibility = View.INVISIBLE - } - if (url.split("//").size > 1) { - url.replace("//", "/").replace("https:/", "https://").let { - Blog.LOGE("url >> ${url} , it >>> ${it}") - lastedUrl = url - } - } else { - lastedUrl = url - } - checkIfDownloadable(url) + override fun onLocationChange(session: GeckoSession, url: String?, perms: MutableList, hasUserGesture: Boolean) { + url?.let { + if (it.contains(getFilterF()) && privateMode) this@GeckoWeb.visibility = View.INVISIBLE + lastedUrl = if (it.split("//").size > 1) it.replace("//", "/").replace("https:/", "https://") else it + checkIfDownloadable(it) + onLocationChangeCallback?.invoke(it) // 외부 콜백 } - - decoViews.filter { it != null && it.id > -1 }.forEach { - if (it != null && it.id > -1) { - if (it.id == R.id.back) { - it.setOnClickListener { session.goBack() } - } else if (it.id == R.id.current_address) { - (it as? TextView)?.let { - it.tag = currentTitle - it.text = url - } - }else if (it.id == R.id.reload) { - it.setOnClickListener { session.reload() } - } + // 외부 뷰 업데이트 + decoViews.forEach { view -> + when(view.id) { + R.id.back -> view.setOnClickListener { session.goBack() } + R.id.current_address -> (view as? TextView)?.apply { tag = currentTitle; text = url } + R.id.reload -> view.setOnClickListener { session.reload() } } } - } override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) { - super.onCanGoBack(session, canGoBack) this@GeckoWeb.canGoBack = canGoBack - if (canGoBack) { + } + } + // [Progress Delegate] + private val progressDelegate = object : GeckoSession.ProgressDelegate { + override fun onProgressChange(session: GeckoSession, progress: Int) { + this@GeckoWeb.progress?.setProgress(progress, true) + } + override fun onPageStart(session: GeckoSession, url: String) { + markdownContents = null + markdownUri = null + if (url.startsWith("magnet:?")) { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { + flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TOP + }) + } + onPageStartCallback?.invoke(url) + } + override fun onPageStop(session: GeckoSession, success: Boolean) { + onPageStopCallback?.invoke(success) + } + override fun onSessionStateChange(session: GeckoSession, sessionState: GeckoSession.SessionState) { + onSessionStateChangeCallback?.invoke(sessionState) + } + } + + // [Extension Delegates] + private val addonManagerDelegate = object : AddonManagerDelegate { + override fun onReady(extension: WebExtension) { mExtension = extension } + override fun onEnabled(extension: WebExtension) { mExtension = extension } + } + + private val messageDelegate = object : MessageDelegate { + override fun onConnect(port: WebExtension.Port) { + mPort = port + mPort!!.setDelegate(portDelegate) + } + } + + private val portDelegate = object : PortDelegate { + override fun onPortMessage(message: Any, port: WebExtension.Port) { + if (message is String && message.contains("type")) { + try { + val msg = Gson().fromJson(message, PortMessage::class.java) + handlePortMessage(msg) + onPortMessageCallback?.invoke(msg) // 외부 콜백 + } catch (e: Exception) { e.printStackTrace() } } } - + override fun onDisconnect(port: WebExtension.Port) { mPort = null } } - fun saveMd(fast : Boolean? = false) { - val message: JSONObject = JSONObject() - try { - message.put("type", "saveContent") - message.put("fast", fast) - } catch (ex: JSONException) { - throw RuntimeException(ex) + + // [Other Delegates] + private val promptDelegate = object : GeckoSession.PromptDelegate { + override fun onAlertPrompt(session: GeckoSession, prompt: GeckoSession.PromptDelegate.AlertPrompt): GeckoResult? { + if (lastedUrl?.contains("daum.net/dotax") == true) { + // Dotax 로직 + try { + Gson().fromJson(prompt.message, DotaxArticles::class.java)?.Articles?.forEach { + Dotax("${it.fldid}/${it.dataid}", "dotax", it.articleElapsedTime!!, it.title!!, it.thumbnailImageUrl!!)?.let { dotax -> + WorkersDb.insertData(dotax.getRssData()) + } + } + } catch (e: Exception) {} + } + prompt.dismiss() + return super.onAlertPrompt(session, prompt) } - Blog.LOGE(Gson().toJson(message)) - mPort?.postMessage(message) + } + private val mediaDelegate = object : GeckoSession.MediaDelegate {} + private val mediaSessionDelegate = object : MediaSession.Delegate {} + + + init { + // 초기화 시 터치 리스너 등 설정 + setupTouchListener() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + this.pointerIcon = PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL) + } + buildWeb() } - val portDelegate: PortDelegate = - object : PortDelegate { - override fun onPortMessage( - message: Any, port: WebExtension.Port - ) { + private fun buildWeb() { + getRuntime()?.let { runtime -> + val session = GeckoSession() + session.open(runtime) + this.setSession(session) + // Delegate 연결 (NullPointerException 방지를 위해 상단 선언된 변수 사용) + session.contentDelegate = contentDelegate + Blog.LOGE("session.contentDelegate = $contentDelegate") + session.progressDelegate = progressDelegate + Blog.LOGE("session.progressDelegate = $progressDelegate") + session.navigationDelegate = navigationDelegate + Blog.LOGE("session.navigationDelegate = $navigationDelegate") + session.mediaDelegate = mediaDelegate + session.promptDelegate = promptDelegate + session.mediaSessionDelegate = mediaSessionDelegate - Blog.LOGE("PortDelegate", "Received message from extension: $message") - if (message is String && message.contains("type")) { - try { - var lPortMessage = - Gson().fromJson(message, PortMessage::class.java) - when(lPortMessage.type) { - "allImagesFound" -> { - lastedUrl?.let { - startBookmarkSaveProcessForMultipleImages(it, lPortMessage.urls) - } - } - "SINGLE_IMAGE_DATA"-> { - if (lPortMessage.imgSrc != null && lPortMessage.base64Data != null) { - // *** 핵심: 수신한 Base64 데이터를 커스텀 캐시에 저장 *** - Log.d("GeckoImageCache", "캐싱 저장 시도: ${lPortMessage.imgSrc}") - Base64ImageCache.put(lPortMessage.imgSrc!!, lPortMessage.base64Data!!) - } - } - "getListResult" -> { - } - "BookContents"->{ - - } - "NotRegistered" -> { - } - "WebtoonContents"-> { - } - "MSG" -> { - context.toast("Received Msg privates form ${lPortMessage.msg}") - } - "SHOWVIEWER" -> { - } - "DOTAX"->{ - val script = "alert(JSON.stringify({Articles : window.articles}))" - val encodedJs = Uri.encode(script) - val jsUri = "javascript:$encodedJs" - session?.loadUri(jsUri) - } - "PRIVATES"->{ - lPortMessage.privates?.let { - context.toast("Received Msg privates form ${lPortMessage.currentPage} data => ${it?.size ?: 0}") - WorkersDb.insertBulkData(it) - } - } - "Cookies"->{ - Blog.LOGE("cookies >>> ${lPortMessage.cookies}") - } - "PagerContents" -> { - if (lPortMessage.contents?.isNotEmpty() == true) { - - } - } - "MainContentsEl"->{ - try { - val doc: Document = Jsoup.parse(lPortMessage.contents) - val combinedHtml = doc.html() - val converter = FlexmarkHtmlConverter.builder().htmlNodeRendererFactory { options -> CustomVideoNodeRenderer(options) }.build() - markdownContents = converter.convert(combinedHtml) - markdownUri = lPortMessage.currentPage?.toUri() - markdownContents?.let { - if (it.length > 40) { - scrapWebToMd(context, markdownUri, it) - } - } - } catch (e: Exception) { - markdownContents = null - markdownUri = null - } - - -// scrapWepToMd(context, Uri.parse(lPortMessage.currentPage), markdown) - - } - else -> { - + runtime.webExtensionController.setAddonManagerDelegate(addonManagerDelegate) + runtime.webExtensionController + .ensureBuiltIn(extPath, extId) + .accept( + { extension -> + ThreadUtils.runOnUiThread { + if (extension != null) { + session.webExtensionController.setMessageDelegate(extension, messageDelegate, mPortNam) } } - } catch (e: Exception) { - e.printStackTrace() - } - - } - } - - - override fun onDisconnect(port: WebExtension.Port) { - // This port is not usable anymore. - - - mPort = null - - - } - } - - var markdownContents : String? = null - var markdownUri : Uri? = null - - val cacheDelegate: PortDelegate = - object : PortDelegate { - override fun onPortMessage( - message: Any, port: WebExtension.Port - ) { - Blog.LOGE("cacheDelegate", "cacheDelegate message : $message") - - } - - - override fun onDisconnect(port: WebExtension.Port) { - // This port is not usable anymore. - if (port === mCaache) { - mCaache = null - } - } - } - - fun onReceiveFromExtension(message: JSONObject) { - val url = message.getString("url") - val dataBase64 = message.getString("data") - val host = Uri.parse(url).host!! - val cacheDir = File(context.filesDir, "webcache/$host") - if (!cacheDir.exists()) cacheDir.mkdirs() - - // 파일명 생성 (중복처리 필요) - val fileName = filenameFromUrl(url) - val resourceFile = File(cacheDir, fileName) - val rawData = Base64.decode(dataBase64, Base64.DEFAULT) - resourceFile.writeBytes(rawData) - } - - - - fun filenameFromUrl(url: String): String { - // URL을 파싱 - val uri = Uri.parse(url) - // 경로 마지막 세그먼트 (예시: /images/logo.png → logo.png) - var filename = uri.lastPathSegment ?: "index.html" - - // 쿼리 파라미터 등 URL이 붙은 경우 처리 (예: index.html?version=2) - if (filename.contains("?")) { - filename = filename.substringBefore("?") - } - if (filename.isEmpty() || filename.endsWith("/")) { - filename = "index.html" - } - - // 파일명에 사용할 수 없는 문자 제거(윈도우, 리눅스, 맥 등 호환) - filename = filename.replace(Regex("[\\\\/:*?\"<>|]"), "_") - - // 너무 긴 파일명은 자르기 - val maxLength = 100 - if (filename.length > maxLength) { - val ext = filename.substringAfterLast('.', "") - filename = filename.take(maxLength - ext.length - 1) + - if (ext.isNotBlank()) ".$ext" else "" - } - - return filename - } - - - val messageDelegate: MessageDelegate = - object : MessageDelegate { - override fun onConnect(port: WebExtension.Port) { - Blog.LOGE("onConnect port >>> ${port.name}") - if (port != null) { - mPort = port - mPort!!.setDelegate(portDelegate) - } - } - - override fun onMessage( - nativeApp: String, - message: Any, - sender: WebExtension.MessageSender - ): GeckoResult? { - Blog.LOGE( - "messageDelegate", - "onMessage from WebExtension: ${nativeApp} , $message , ${sender.webExtension.id}" + }, + { e -> Log.e("GeckoWeb", "Error registering WebExtension", e) } ) - return super.onMessage(nativeApp, message, sender) + } + } + // --- 4. Helper Methods --- + + private fun handlePortMessage(msg: PortMessage) { + when (msg.type) { + "allImagesFound" -> lastedUrl?.let { startBookmarkSaveProcessForMultipleImages(it, msg.urls) } + "SINGLE_IMAGE_DATA" -> if (msg.imgSrc != null && msg.base64Data != null) { + Base64ImageCache.put(msg.imgSrc!!, msg.base64Data!!) + } + "MSG" -> context.toast("Msg: ${msg.msg}") + "DOTAX" -> session?.loadUri("javascript:${Uri.encode("alert(JSON.stringify({Articles : window.articles}))")}") + "PRIVATES" -> msg.privates?.let { WorkersDb.insertBulkData(it) } + "MainContentsEl" -> { + try { + val doc = Jsoup.parse(msg.contents) + val converter = FlexmarkHtmlConverter.builder().htmlNodeRendererFactory { CustomVideoNodeRenderer(it) }.build() + markdownContents = converter.convert(doc.html()) + markdownUri = msg.currentPage?.toUri() + markdownContents?.let { if (it.length > 40) scrapWebToMd(context, markdownUri, it) } + } catch (e: Exception) { markdownContents = null } } } - - fun onStart() { - } - fun onResume() { - + @SuppressLint("ClickableViewAccessibility") + private fun setupTouchListener() { + this.setOnTouchListener { v, event -> + if (isSpecialDevice(event)) mSimpleFingerGestures.onTouch(v, event) + else super.onTouchEvent(event) + } + } + private fun isSpecialDevice(event: MotionEvent): Boolean { + val name = event.device.name + return name?.contains("JX-12", true) == true || name.equals("J06", true) } override fun dispatchKeyEvent(ev: KeyEvent): Boolean { - Blog.LOGE("dispatch ev?.device?.name >>> ${ev?.device?.name}") - if (ev?.device?.name?.contains("SM-031N Mouse") == true) { - when (ev.action) { - ACTION_UP -> { - Blog.LOGE("dispatch dispatchKeyEvent>>> ${ev}") - when (ev.keyCode) { - KEYCODE_BUTTON_Y -> { - } - - KEYCODE_BUTTON_X -> { - - } - - KEYCODE_BUTTON_A -> { - } - - KEYCODE_BUTTON_B -> { - - } - - KEYCODE_DPAD_DOWN -> { - - } - - KEYCODE_DPAD_UP -> { - - } - - KEYCODE_BUTTON_START -> { - } - - KEYCODE_BUTTON_SELECT -> { - - } - - else -> {} - } - } - - else -> {} - } + if (ev.device?.name?.contains("SM-031N Mouse") == true) { + // Mouse key handling if needed return true } return super.dispatchKeyEvent(ev) } - override fun loadUrl(url: String, param : String?) { - var nUrl = url - Blog.LOGE("url >>>> ${url}") - if (url.endsWith("=")) { + // Message Sending Helpers + fun sendScrollDown(isUp: Boolean) = sendJsonMsg("scrollDown", "isUpDown" to if(isUp) 1 else -1) + fun sendSearch(keyword: String) = sendJsonMsg("search", "keyword" to keyword) + fun sendSearchDo() = sendJsonMsg("searchDo") + fun saveMd(fast: Boolean? = false) = sendJsonMsg("saveContent", "fast" to fast) + + private fun sendJsonMsg(type: String, vararg params: Pair) { + val json = JSONObject().put("type", type) + params.forEach { json.put(it.first, it.second) } + mPort?.postMessage(json) + } + + // File/Download Helpers + private fun downloadFile(url: String) { + val filename = "${url.substringAfterLast("/")}_${System.currentTimeMillis()}.mp4" + val savePath = File(context.getExternalFilesDir(null), filename) + Thread { try { - nUrl = String(java.util.Base64.getMimeDecoder().decode(url.toByteArray())) - param?.let { - nUrl = nUrl.plus(param) + val response = OkHttpClient().newCall(Request.Builder().url(url).addHeader("Referer", lastedUrl?:"").build()).execute() + if (response.isSuccessful) { + response.body?.byteStream()?.use { input -> FileOutputStream(savePath).use { output -> input.copyTo(output) } } + post { context.toast("파일 저장 완료: ${savePath.name}") } } - }catch (e: Exception) { - nUrl = url - } - } else if (url.startsWith("http") == false) { - nUrl = lastDomain - } else if(param != null && param?.length ?: 0 > 1) { - nUrl = url.plus(param) - } else { - nUrl = url - } - - if (!privateMode && this.isVisible == false) { - this.visibility = View.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) - } - } - BWebview.Companion.currentRetryCount = 0; + } catch (e: Exception) { e.printStackTrace() } + }.start() } - companion object { - private const val TAG = "DualScreenStatus" - var currentRetryCount = 0 + fun scrapWebToMd(context: Context, url: Uri?, markdownText: String) { + val dir = File("/storage/emulated/0/bums_ob/BUM'S PACED /scraped/md") + if (!dir.exists()) dir.mkdirs() + File(dir, "${url?.host ?: "UnKnown"}_scraped_${SimpleDateFormat("yyyyMMdd-HHmm").format(Date())}.md") + .writeText(markdownText, Charsets.UTF_8) + mOnSave?.saved() } + + // Dialog Helpers + private fun showNewSessionDialog(uri: String) { + AlertDialog.Builder(context) + .setTitle("Move To\n$uri") + .setPositiveButton("브라우저로 이동") { _, _ -> + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(uri)).apply { flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TOP }) + } + .setNeutralButton("페이지 이동") { _, _ -> loadUrl(uri) } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun showContextMenu(pageUrl: String, mediaUrl: String) { + val menuItems = arrayOf("이 미디어만 북마크", "페이지의 모든 이미지 북마크", "다운로드") + AlertDialog.Builder(context).setTitle("작업 선택").setItems(menuItems) { _, which -> + when (menuItems[which]) { + "이 미디어만 북마크" -> startBookmarkSaveProcessForSingleImage(pageUrl, mediaUrl) + "페이지의 모든 이미지 북마크" -> sendJsonMsg("fetchAllImages", "targetSrc" to mediaUrl) + "다운로드" -> CommonUtils.downloadFileWithOkHttp(context, Uri.parse(pageUrl), mediaUrl) + } + }.show() + } + + private fun showLoginDialog(onSuccess: () -> Unit) { + val view = View.inflate(context, R.layout.dialog_login, null) + val idInput = view.findViewById(R.id.et_user_id) + val pwInput = view.findViewById(R.id.et_user_pw) + AlertDialog.Builder(context).setTitle("로그인").setView(view) + .setPositiveButton("로그인") { _, _ -> + BookmarkUploader.loginAndGetToken(idInput.text.toString(), pwInput.text.toString(), { onSuccess() }, { context.toast("실패: $it") }) + } + .setNegativeButton("취소", null).show() + } + + private fun showBookmarkDetailsDialog(pageUrl: String, mediaUrls: List) { + val container = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL; setPadding(50,50,50,50) } + val commentInput = EditText(context).apply { hint = "코멘트" } + container.addView(commentInput) + val rg = RadioGroup(context).apply { orientation = RadioGroup.VERTICAL } + listOf("PUBLIC", "MEMBERS", "PRIVATE").forEachIndexed { i, s -> rg.addView(RadioButton(context).apply { text = s; id = i }) } + rg.check(0) + container.addView(rg) + + AlertDialog.Builder(context).setTitle("북마크 저장").setView(container) + .setPositiveButton("저장") { _, _ -> + val vis = rg.findViewById(rg.checkedRadioButtonId).text.toString() + BookmarkUploader.saveBookmarkWithContent(pageUrl, mediaUrls, commentInput.text.toString(), vis) + } + .setNegativeButton("취소", null).show() + } + + private fun startBookmarkSaveProcessForSingleImage(pageUrl: String, mediaUrl: String) { + if(BookmarkUploader.isUserLoggedIn) showBookmarkDetailsDialog(pageUrl, listOf(mediaUrl)) + else showLoginDialog { showBookmarkDetailsDialog(pageUrl, listOf(mediaUrl)) } + } + + private fun startBookmarkSaveProcessForMultipleImages(pageUrl: String, mediaUrls: List) { + if(BookmarkUploader.isUserLoggedIn) showBookmarkDetailsDialog(pageUrl, mediaUrls) + else showLoginDialog { showBookmarkDetailsDialog(pageUrl, mediaUrls) } + } + + private fun getFilterF() = String(Base64.decode("aHR0cHM6Ly9pamF2dG9ycmVudC5jb20=", Base64.DEFAULT)) + private fun Context.toast(msg: String) = Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() + private fun View.post(action: () -> Unit) = this.post(Runnable(action)) } \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt index e5a6df6e..93564b05 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt @@ -44,6 +44,7 @@ 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 @@ -479,28 +480,28 @@ open class NeoRssActivity : CommonActivity() { } R.id.books ->{ supportFragmentManager.beginTransaction() - .replace(R.id.fragment_container, Novels()) + .replace(R.id.fragment_container, TokiFragment.newInstanceNovels()) .commit() } R.id.webtoons ->{ supportFragmentManager.beginTransaction() - .replace(R.id.fragment_container, Webtoons()) + .replace(R.id.fragment_container, TokiFragment.newInstanceWebtoons()) .commit() } R.id.comics ->{ supportFragmentManager.beginTransaction() - .replace(R.id.fragment_container, Comics()) + .replace(R.id.fragment_container, TokiFragment.newInstanceComics()) .commit() } R.id.youtube ->{ supportFragmentManager.beginTransaction() - .replace(R.id.fragment_container, YouTube()) + .replace(R.id.fragment_container, TokiFragment.newInstanceYouTube()) .commit() } R.id.perplexity ->{ supportFragmentManager.beginTransaction() - .replace(R.id.fragment_container, Perplexity()) + .replace(R.id.fragment_container, TokiFragment.newInstancePerplexity()) .commit() } R.id.zota ->{ diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt index b4d1f77a..debf8adf 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt @@ -47,6 +47,7 @@ import bums.lunatic.launcher.R import bums.lunatic.launcher.common.letTrue import bums.lunatic.launcher.databinding.LauncherHomeBinding import bums.lunatic.launcher.helpers.Constants.Companion.PREFS_SETTINGS +import bums.lunatic.launcher.home.GeckoWeb.JxEvent import bums.lunatic.launcher.home.NeoRssActivity.Companion.lActivity import bums.lunatic.launcher.home.SearchBottomSheet.OnSearchListener import bums.lunatic.launcher.home.adapters.RssItemAdapter @@ -54,7 +55,6 @@ import bums.lunatic.launcher.home.adapters.SwipeToDeleteCallback import bums.lunatic.launcher.model.RssData import bums.lunatic.launcher.model.RssDataType import bums.lunatic.launcher.model.WeatherForcast -import bums.lunatic.launcher.home.tokiz.view.JxEvent import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.utils.SimpleFingerGestures import bums.lunatic.launcher.utils.beforeDay @@ -581,7 +581,7 @@ internal class RssHome : Fragment() { binding.geckoWeb.decoViews.add(activity.findViewById(R.id.reload)) binding.geckoWeb.decoViews.add(activity.findViewById(R.id.dl_video)) } - + Blog.LOGE("binding.geckoWeb.decoViews >>> ${binding.geckoWeb.decoViews.size}") return binding.root } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/UniversalToki.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/UniversalToki.kt new file mode 100644 index 00000000..d2b78170 --- /dev/null +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/UniversalToki.kt @@ -0,0 +1,698 @@ +//package bums.lunatic.launcher.home.tokiz +// +//import android.content.Context +//import android.content.Intent +//import android.content.res.Configuration +//import android.net.Uri +//import android.os.Bundle +//import android.os.Handler +//import android.os.Looper +//import android.os.Message +//import android.text.InputType +//import android.text.SpannableStringBuilder +//import android.text.style.RelativeSizeSpan +//import android.view.KeyEvent +//import android.view.LayoutInflater +//import android.view.PointerIcon +//import android.view.View +//import android.view.View.GONE +//import android.view.View.VISIBLE +//import android.view.ViewGroup +//import android.widget.ArrayAdapter +//import android.widget.EditText +//import android.widget.ImageButton +//import android.widget.TextView +//import android.widget.Toast +//import androidx.activity.OnBackPressedCallback +//import androidx.appcompat.app.AlertDialog +//import androidx.core.net.toUri +//import androidx.core.view.isVisible +//import androidx.fragment.app.Fragment +//import bums.lunatic.launcher.R +//import bums.lunatic.launcher.databinding.BooktokiBinding +//import bums.lunatic.launcher.home.JxEvent +//import bums.lunatic.launcher.home.NeoRssActivity +//import bums.lunatic.launcher.home.NeoRssActivity.Companion.getRuntime +//import bums.lunatic.launcher.home.tokiz.view.JxEvent +//import bums.lunatic.launcher.home.tokiz.view.PagedTextLayout +//import bums.lunatic.launcher.home.tokiz.view.PagedTextViewInterface +//import bums.lunatic.launcher.utils.Blog +//import bums.lunatic.launcher.utils.DefaultList +//import bums.lunatic.launcher.workers.WorkersDb +//import io.realm.kotlin.Realm +//import io.realm.kotlin.UpdatePolicy +//import io.realm.kotlin.ext.copyFromRealm +//import io.realm.kotlin.ext.query +//import org.json.JSONException +//import org.json.JSONObject +//import org.mozilla.gecko.util.ThreadUtils +//import org.mozilla.geckoview.GeckoResult +//import org.mozilla.geckoview.GeckoSession +//import org.mozilla.geckoview.GeckoSessionSettings +//import org.mozilla.geckoview.WebExtension +//import java.net.URL +//import java.text.SimpleDateFormat +//import java.util.Date +//import kotlin.random.Random +// +//class UniversalToki : Fragment(), PagedTextViewInterface { +// +// // --- Configuration Arguments (각 하위 클래스의 특성을 정의) --- +// private var contentsType: String = "book" // 기본값: Novels +// private var lastNumber: Int = 468 +// private var webContentsName: String = "booktoki" +// private var afterDot: String = "com" +// private var isPrivateMode: Boolean = false +// private var includeNumberInDomain: Boolean = true // 도메인에 숫자 포함 여부 +// private var enableGestures: Boolean = true // 제스처 사용 여부 +// private var showWebOnSwipeDown: Boolean = false // 스와이프 다운 시 웹뷰 보이기 여부 +// +// // --- State Variables --- +// private var lastInfo: LastInfo? = null +// private var currentPage: ContentsPageInfo? = null +// private var saveContinuation = false +// private var lastedUrl: String? = null +// private var canGoBack: Boolean = false +// private var _binding: BooktokiBinding? = null +// private val binding get() = _binding!! +// +// private var currentTitle: String = "" +// private var currentChapter: Int = 0 +// +// // WebExtension +// private var mPort: WebExtension.Port? = null +// private val mPortNam = "browser" +// private val extPath = "resource://android/assets/extensions/my_extension/" +// private val extId = "messaging@booktoki468.com" +// +// // --- Companion Object (Factory Methods for Subclass Replacement) --- +// companion object { +// private const val ARG_CONTENTS_TYPE = "type" +// private const val ARG_LAST_NUMBER = "last_num" +// private const val ARG_WEB_NAME = "web_name" +// private const val ARG_AFTER_DOT = "after_dot" +// private const val ARG_PRIVATE = "private" +// private const val ARG_INC_NUM = "inc_num" +// private const val ARG_GESTURES = "gestures" +// private const val ARG_WEB_ON_SWIPE = "web_swipe" +// +// // [팩토리 메서드] 기존 클래스들을 대체합니다. +// fun newNovels() = create("book", 468, "booktoki", "com", webOnSwipe = true) +// fun newWebtoons() = create("webtoon", 468, "newtoki", "com") +// fun newComics() = create("comics", 468, "manatoki", "net") +// fun newYoutube() = create("youtube", 143, "youtube", "com", isPrivate = true, incNum = false, gestures = false) +// fun newMagnet() = create("btsearch", 143, "btsearch", "love", incNum = false) +// fun newPerplexity() = create("perplexity", 143, "www.perplexity", "ai", incNum = false) +// +// private fun create( +// type: String, lastNum: Int, name: String, dot: String, +// isPrivate: Boolean = false, incNum: Boolean = true, +// gestures: Boolean = true, webOnSwipe: Boolean = false +// ): UniversalToki { +// return UniversalToki().apply { +// arguments = Bundle().apply { +// putString(ARG_CONTENTS_TYPE, type) +// putInt(ARG_LAST_NUMBER, lastNum) +// putString(ARG_WEB_NAME, name) +// putString(ARG_AFTER_DOT, dot) +// putBoolean(ARG_PRIVATE, isPrivate) +// putBoolean(ARG_INC_NUM, incNum) +// putBoolean(ARG_GESTURES, gestures) +// putBoolean(ARG_WEB_ON_SWIPE, webOnSwipe) +// } +// } +// } +// } +// +// override fun onCreate(savedInstanceState: Bundle?) { +// super.onCreate(savedInstanceState) +// arguments?.let { +// contentsType = it.getString(ARG_CONTENTS_TYPE, "book")!! +// lastNumber = it.getInt(ARG_LAST_NUMBER, 468) +// webContentsName = it.getString(ARG_WEB_NAME, "booktoki")!! +// afterDot = it.getString(ARG_AFTER_DOT, "com")!! +// isPrivateMode = it.getBoolean(ARG_PRIVATE, false) +// includeNumberInDomain = it.getBoolean(ARG_INC_NUM, true) +// enableGestures = it.getBoolean(ARG_GESTURES, true) +// showWebOnSwipeDown = it.getBoolean(ARG_WEB_ON_SWIPE, false) +// } +// } +// +// // --- Back Press Handling (기존 BaseToki의 뒤로가기 로직 통합) --- +// override fun onAttach(context: Context) { +// super.onAttach(context) +// requireActivity().onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { +// override fun handleOnBackPressed() { +// handleBackAction() +// } +// }) +// } +// +// private fun handleBackAction() { +// // 1. 뷰어(텍스트 레이어)가 켜져 있으면 끈다. +// if (binding.pagedLayer.isVisible) { +// binding.pagedLayer.visibility = GONE +// // Novels(booktoki)는 스와이프 다운이나 뒤로가기 시 웹뷰를 다시 보여줌 +// if (showWebOnSwipeDown || !binding.menuWeb.isVisible) { +// binding.menuWeb.visibility = VISIBLE +// } +// onTouch(TouchArea.Center) // 터치 상태 초기화 +// return +// } +// +// // 2. 웹 브라우저 뒤로가기가 가능하면 실행 +// // (GeckoWeb 내부 상태가 canGoBack이면) +// if (canGoBack) { +// binding.menuWeb.session?.goBack() +// return +// } +// +// // 3. 더 이상 뒤로 갈 곳이 없으면 시스템 기본 동작(액티비티 종료 등) +// isEnabled = false // 콜백 비활성화 +// requireActivity().onBackPressed() +// isEnabled = true // 다시 활성화 +// } +// +// override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { +// _binding = BooktokiBinding.inflate(inflater, container, false) +// return binding.root +// } +// +// override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +// super.onViewCreated(view, savedInstanceState) +// +// setupGeckoWeb() +// setupButtons() +// setupExternalViews() // 액티비티 상단 UI 연결 +// +// val nullCursor = PointerIcon.getSystemIcon(requireContext(), PointerIcon.TYPE_NULL) +// binding.root.pointerIcon = nullCursor +// } +// +// private fun setupGeckoWeb() { +// binding.menuWeb.apply { +// lastDomain = getLastedDoamin() +// privateMode = isPrivateMode +// +// setOnLongClickListener { +// if (enableGestures) onTouch(TouchArea.Center) +// return@setOnLongClickListener false +// } +// +// // --- Callbacks (GeckoWeb에서 발생한 이벤트를 여기서 처리) --- +// onLocationChangeCallback = { url -> +// val cleanUrl = if (url.split("//").size > 1) { +// url.replace("//", "/").replace("https:/", "https://") +// } else url +// +// lastedUrl = cleanUrl +// +// // 외부 뷰 업데이트 및 다운로드 가능 여부 체크 +// updateAddressBar(cleanUrl) +// binding.menuWeb.checkIfDownloadable(cleanUrl) +// +// // 페이지 로드 완료 처리 (히스토리 저장 등) +// completePageLoad(LastInfo().apply { +// this.pageUrl = cleanUrl.toUri().path ?: getLastedDoamin() +// this.contentsName = webContentsName +// this.contentsType = contentsType +// this.pageIndex = 0 +// }) +// } +// +// onPageStartCallback = { +// binding.progress.visibility = VISIBLE +// } +// +// onPageStopCallback = { success -> +// if (success) { +// // 페이지 로드 완료 시 확장 프로그램에 리스트 요청 메시지 전송 +// val message = JSONObject().put("type", "getList").put("tab", session?.settings?.screenId) +// mPort?.postMessage(message) +// } +// binding.progress.visibility = GONE +// } +// +// onSessionStateChangeCallback = { state -> onStateChange(state) } +// onPortMessageCallback = { msg -> handlePortMessage(msg) } +// +// // 제스처 연결 +// jxInteface = { jxEvent -> +// if (enableGestures) { +// when (jxEvent) { +// JxEvent.SCROLL_UP -> sendScrollDown(true) +// JxEvent.SCROLL_DOWN -> sendScrollDown(false) +// JxEvent.SWIPE_LEFT -> if (contentsType == "comics") sendViewerTouch("left") else actionNextEvent() +// JxEvent.SWIPE_RIGHT -> if (contentsType == "comics") sendViewerTouch("right") else actionPrevEvent() +// else -> {} +// } +// } +// } +// } +// } +// +// private fun setupButtons() { +// // [리스트 보기 버튼] +// binding.btnList.setOnClickListener { +// val url = lastedUrl ?: getLastedDoamin() +// val path = Uri.parse(url).path ?: "" +// val processedPath = processPageUrl(path) +// +// if (processedPath.isNotEmpty()) { +// HistoryManager.getBookInfos(contentsType, processedPath) { info -> +// if (info != null && info.pages.isNotEmpty()) { +// info.pages.sortBy { it.pathUrl } +// activity?.runOnUiThread { showList(info) } +// } else { +// // DB에 없으면 현재 페이지 정보를 기반으로 재시도하거나 토스트 +// showToast("목록을 불러올 수 없습니다.") +// } +// } +// } else { +// showToast("유효하지 않은 주소입니다.") +// } +// } +// +// binding.btnSetting.setOnClickListener { startActivity(Intent(requireContext(), Settings::class.java)) } +// binding.btnHistory.setOnClickListener { getHistory()?.let { showHistory(it) } } +// binding.btnHome.setOnClickListener { +// binding.pagedLayer.visibility = GONE +// goToHome() +// } +// } +// +// private fun setupExternalViews() { +// (activity as? NeoRssActivity)?.let { act -> +// // 뒤로가기 버튼 +// act.findViewById(R.id.back)?.setOnClickListener { handleBackAction() } +// // 새로고침 +// act.findViewById(R.id.reload)?.setOnClickListener { binding.menuWeb.session?.reload() } +// +// // GeckoWeb decoViews에 등록 (주소창 텍스트 업데이트용) +// binding.menuWeb.decoViews.clear() +// binding.menuWeb.decoViews.add(act.findViewById(R.id.current_address)) +// binding.menuWeb.decoViews.add(act.findViewById(R.id.dl_video)) +// } +// } +// +// private fun updateAddressBar(url: String) { +// (activity as? NeoRssActivity)?.findViewById(R.id.current_address)?.let { +// it.tag = currentTitle +// it.text = url +// } +// } +// +// // --- Helper Functions --- +// +// fun openRealm(): Realm = HistoryManager.openRealm +// +// fun getLastedDoamin(): String { +// return if (includeNumberInDomain) String.format("https://%s%d.%s", webContentsName, lastNumber, afterDot) +// else String.format("https://%s.%s", webContentsName, afterDot) +// } +// +// // --- Browser State & History Logic --- +// +// fun onStateChange(sessionState: GeckoSession.SessionState) { +// // GeckoWeb의 canGoBack 상태 동기화 +// canGoBack = binding.menuWeb.canGoBack ?: false +// +// if (sessionState.last().uri.length > 10) { +// lastedUrl?.let { url -> +// val path = Uri.parse(url).path ?: return@let +// HistoryManager.getBookPageInfo(contentsType, path) { info -> +// info?.let { +// currentPage = it +// currentChapter = it.chapterNum ?: 0 +// HistoryManager.save(HistoryItem().putHistory(it, lastedUrl!!)) +// +// updateLastInfoRealm(it) +// } +// } +// if (saveContinuation) { +// binding.menuWeb.postDelayed({ moveToNext(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path) }, 10000) +// } +// } +// } +// } +// +// private fun updateLastInfoRealm(info: ContentsPageInfo) { +// openRealm().writeBlocking { +// val query = this.query("contentsType == $0", contentsType).find() +// if (query.isNotEmpty()) { +// query.last()?.let { last -> +// last.title = currentTitle +// last.chapter = currentChapter +// last.contentsType = info.contentsType +// this@UniversalToki.lastInfo = copyFromRealm(last) +// } +// } else { +// LastInfo().let { last -> +// last.title = currentTitle +// last.chapter = currentChapter +// last._id = contentsType +// last.contentsType = info.contentsType +// copyToRealm(last, UpdatePolicy.ALL) +// this@UniversalToki.lastInfo = last +// } +// } +// } +// } +// +// private fun processPageUrl(pageUrl: String): String { +// try { +// if (pageUrl.isEmpty()) return "" +// var workingUrl = pageUrl +// if (workingUrl.startsWith("http")) { +// try { workingUrl = URL(workingUrl).path } catch (e: Exception) {} +// } +// // URL 경로 처리 (숫자만 있는 세그먼트 유지 등 기존 로직) +// val paths = workingUrl.split("/").filter { it.isNotEmpty() }.toMutableList() +// if (paths.isNotEmpty()) { +// val last = paths.last() +// // 기존 로직: 마지막이 숫자가 아니면 제거 (목록 페이지 등에서 필요) +// if (!last.all { it.isDigit() }) { +// // paths.removeAt(paths.size - 1) // 필요 시 주석 해제하여 사용 +// } +// } +// return "/" + paths.joinToString("/") +// } catch (e: Exception) { +// e.printStackTrace() +// return pageUrl +// } +// } +// +// // --- Book Info & List Handling (오버로딩 포함) --- +// +// // [중요] PageInfosJ를 처리하는 오버로딩 (기존 로직 누락 방지) +// fun onBookInfos(infosj: PageInfosJ) { +// val realm = openRealm() +// realm.writeBlocking { +// try { +// infosj.bookPageUrl = processPageUrl(infosj.bookPageUrl ?: "") +// infosj.pages.forEach { it.pathUrl = processPageUrl(it.pathUrl ?: "") } +// +// // DB에 정보 업데이트/저장 로직 +// // (기존 BaseToki.kt의 복잡한 로직을 간소화하여 호출) +// HistoryManager.getBookInfos(contentsType, infosj.bookPageUrl!!) { savedInfos -> +// if (savedInfos != null) { +// // 기존 정보가 있으면 병합 +// val managedInfos = copyToRealm(savedInfos, UpdatePolicy.ALL) +// for (item in infosj.pages) { +// if (!managedInfos.hasItem(item.getRealm())) { +// managedInfos.pages.add(item.getRealm()) +// } +// } +// } else { +// // 없으면 새로 생성 +// val newInfos = infosj.getR() +// if (newInfos != null) { +// val managedInfos = copyToRealm(newInfos, UpdatePolicy.ALL) +// infosj.pages.forEach { managedInfos.pages.add(it.getRealm()) } +// } +// } +// } +// } catch(e: Exception) { e.printStackTrace() } +// } +// // 저장 후 리스트 표시 +// onBookInfos(infosj.getR()) +// } +// +// fun onBookInfos(aInfos: ContentsCollection) { +// activity?.runOnUiThread { showList(aInfos) } +// } +// +// fun showList(infos: ContentsCollection) { +// if (infos.pages.isEmpty()) return +// +// val items = ArrayList(infos.pages).apply { sortBy { it.chapterID } } +// +// DefaultList.showDefaultList( +// requireContext(), +// "현재: $currentTitle ($currentChapter 화)", +// items, +// currentChapter, +// { pos -> items[pos].chapterTitle ?: "제목 없음" }, +// { pos -> moveTo(items[pos]) }, +// { state -> +// if (state < 0) { +// saveContinuation = true +// moveToNext(currentPage?.pathUrl) +// } +// } +// ) +// } +// +// private fun moveTo(item: ContentsPageInfo?) { +// item?.pathUrl?.let { contentsLoad(it) } +// } +// +// fun contentsLoad(pathUrl: String) { +// val fullUrl = if (pathUrl.startsWith("http")) pathUrl else getLastedDoamin() + pathUrl +// binding.menuWeb.loadUrl(fullUrl) +// } +// +// // --- History Handling --- +// +// private fun getHistory(): List? { +// return try { +// openRealm().query().query("contentsType == $0", contentsType).find().copyFromRealm().reversed() +// } catch (e: Exception) { e.printStackTrace(); null } +// } +// +// fun showHistory(infos: List) { +// val builder = AlertDialog.Builder(requireContext()) +// builder.setTitle("$currentTitle : $currentChapter") +// +// val adapter = ArrayAdapter(requireContext(), android.R.layout.select_dialog_singlechoice) +// infos.forEach { adapter.add(it.title) } +// +// builder.setAdapter(adapter) { dialog, which -> +// val item = infos[which] +// AlertDialog.Builder(requireContext()) +// .setTitle("${item.title}로 이동?") +// .setPositiveButton("이동") { _, _ -> contentsLoad(item.pageUrl); dialog.dismiss() } +// .setNeutralButton("삭제") { _, _ -> +// openRealm().writeBlocking { +// this.query().query("title == '${item.title}'").find().lastOrNull()?.let { delete(it) } +// } +// dialog.dismiss() +// } +// .setNegativeButton("취소", null) +// .show() +// } +// builder.setNegativeButton("닫기", null) +// builder.show() +// } +// +// // --- Port Message Handling --- +// private fun handlePortMessage(msg: bums.lunatic.launcher.home.tokiz.PortMessage) { +// when(msg.type) { +// "getListResult" -> msg.bookInfos?.let { onBookInfos(it.sort()) } +// "BookContents" -> { +// msg.book?.chapterTitle?.let { onFindTitle(it) } +// msg.book?.bookContents?.let { onLoadedContents(it) } +// } +// "NotRegistered", "WebtoonContents" -> binding.pagedLayer.visibility = GONE +// "MSG" -> showToast(msg.msg ?: "") +// "PRIVATES" -> msg.privates?.let { WorkersDb.insertBulkData(it) } +// "SHOWVIEWER" -> binding.progress.visibility = GONE +// } +// } +// +// // --- Viewer Logic (PagedTextView) --- +// +// fun onFindTitle(contents: String) { +// binding.textviewTitle.text = contents +// binding.textviewTitle.setOnClickListener { +// val input = EditText(requireContext()).apply { +// setText(lastedUrl ?: "") +// inputType = InputType.TYPE_CLASS_TEXT +// } +// AlertDialog.Builder(requireContext()) +// .setTitle("URL 이동") +// .setView(input) +// .setPositiveButton("이동") { _, _ -> contentsLoad(input.text.toString().trim()) } +// .setNegativeButton("취소", null) +// .show() +// } +// +// val testRegex = """[^0-9]""".toRegex() +// if (contents.contains("-")) { +// currentTitle = contents.split("-")[0] +// currentChapter = testRegex.replace(contents.split("-")[1], "").toIntOrNull() ?: 0 +// } else { +// currentTitle = contents +// } +// } +// +// fun onLoadedContents(aContents: String) { +// binding.pagedLayer.post { +// if (aContents.length > 10) { +// // 텍스트 정제 +// val contents = aContents.replace("\\\"", "\"").replace("\\n", System.getProperty("line.separator")) +// +// binding.pagedLayer.mPagedTextViewInterface = this +// applyReaderConfig() +// +// activity?.runOnUiThread { +// binding.pagedLayer.text = contents +// binding.pagedLayer.visibility = VISIBLE +// binding.menuWeb.visibility = GONE +// } +// binding.pagedLayer.forceUpdateUI() +// +// // 현재 페이지 정보 저장 +// lastedUrl?.let { url -> +// val path = Uri.parse(url).path ?: return@let +// HistoryManager.getBookPageInfo(contentsType, path) { info -> +// info?.let { +// currentPage = it +// currentChapter = it.chapterNum ?: 0 +// binding.pagedLayer.currentPage = currentChapter +// HistoryManager.save(HistoryItem().putHistory(it, url)) +// } +// } +// // 콘텐츠 저장 (오프라인 등 용도) +// HistoryManager.getBooPageInfoContentsSave(contentsType, path, contents) +// } +// +// if (saveContinuation) { +// binding.menuWeb.postDelayed({ moveToNext(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path) }, 10000) +// } +// } +// } +// } +// +// fun applyReaderConfig() { +// val realm = HistoryManager.openRealm +// realm.query().find().firstOrNull()?.let { config -> +// val it = realm.copyFromRealm(config) +// activity?.runOnUiThread { +// // 폰트, 스타일, 크기 등 설정 적용 +// val typeface = typesfacez[getIndex(typesfacez as PairArray, it.font ?: "")] +// binding.pagedLayer.setTypeface(resources.getFont(typeface.second)) +// val color = colorz[it.style ?: 0] +// binding.pagedLayer.setColorStyle(color.second) +// binding.pagedLayer.setTextSize(it.textSize?.toFloat() ?: 14f) +// // ... 기타 패딩 등 설정 +// binding.pagedLayer.invalidate() +// } +// } +// } +// +// // --- Gesture & Navigation --- +// +// fun sendScrollDown(isUp: Boolean) { +// val message = JSONObject().put("type", "scrollDown").put("isUpDown", if (isUp) 1 else -1) +// mPort?.postMessage(message) +// } +// +// fun sendViewerTouch(area: String) { +// val message = JSONObject().put("type", "ViewerTouch").put("area", area) +// mPort?.postMessage(message) +// } +// +// fun actionNextEvent(fast: Boolean = false) { +// if (binding.pagedLayer.isVisible) { +// binding.pagedLayer.doNext(fast) +// updateLastInfo(binding.pagedLayer) +// } else { +// moveToNext(currentPage?.pathUrl) +// } +// } +// +// fun actionPrevEvent(fast: Boolean = false) { +// if (binding.pagedLayer.isVisible) { +// binding.pagedLayer.doPrev(fast) +// updateLastInfo(binding.pagedLayer) +// } else { +// moveToPrev(currentPage?.pathUrl) +// } +// } +// +// fun moveToNext(path: String?) { +// path?.let { HistoryManager.getNextPage(contentsType, it) { info -> moveTo(info) } } +// } +// +// fun moveToPrev(path: String?) { +// path?.let { HistoryManager.getPrevPage(contentsType, it) { info -> moveTo(info) } } +// } +// +// fun updateLastInfo(pagedTextLayout: PagedTextLayout) { +// // 현재 페이지 인덱스 저장 (책갈피 기능) +// (currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)?.let { +// HistoryManager.setBookPageInfo(contentsType, it, pagedTextLayout.current()) +// } +// // ... Realm 업데이트 로직 +// } +// +// override fun onTouch(touchArea: TouchArea) { +// if(!enableGestures) return +// when(touchArea) { +// TouchArea.Right -> actionNextEvent() +// TouchArea.Left -> actionPrevEvent() +// TouchArea.DoubleRight -> actionNextEvent(true) +// TouchArea.DoubleLeft -> actionPrevEvent(true) +// else -> {} +// } +// } +// override fun onSwipeLeft(count: Int) { if(enableGestures) actionNextEvent(count > 1) } +// override fun onSwipeRight(count: Int) { if(enableGestures) actionPrevEvent(count > 1) } +// override fun onSwipeDown(touchCount: Int) { +// if (touchCount == 2) { +// binding.pagedLayer.visibility = GONE +// if (showWebOnSwipeDown) binding.menuWeb.visibility = VISIBLE +// } +// } +// +// override fun onLongClick() {} +// override fun onSwipeUp(touchCount: Int) {} +// override fun onTimeoverTouch() {} +// +// private fun goToHome() { +// binding.menuWeb.loadUrl(getLastedDoamin()) +// } +// +// fun showToast(msg: String) { +// activity?.runOnUiThread { +// Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show() +// } +// } +// +// override fun onDestroyView() { +// super.onDestroyView() +// _binding = null +// } +// +// // --- Helper for Config --- +// fun loadLastInfo() { +// var targetUrl = getLastedDoamin() +// try { +// applyReaderConfig() +// openRealm().query("contentsType == $0", contentsType).find().lastOrNull()?.copyFromRealm()?.let { lastInfo -> +// HistoryManager.getBookPageInfo(contentsType, lastInfo.pageUrl) { info -> +// info?.let { +// currentPage = it +// if ((currentPage?.pathUrl?.length ?: 0) > 1) { +// HistoryManager.save(HistoryItem().putHistory(currentPage, currentPage?.pathUrl ?: getLastedDoamin())) +// currentPage?.pathUrl?.let { targetPath -> +// targetUrl = if (targetPath.startsWith("http", true)) targetPath else getLastedDoamin() + targetPath +// } +// } +// } +// } +// } +// } catch (e: Exception) { e.printStackTrace() } +// finally { +// if (lastedUrl?.contains(targetUrl) != true) { +// contentsLoad(targetUrl) +// } +// } +// } +// +// override fun onResume() { +// super.onResume() +// loadLastInfo() +// } +//} \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/BaseToki.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/BaseToki.kt index 279c165a..589b520b 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/BaseToki.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/BaseToki.kt @@ -32,11 +32,12 @@ import androidx.core.view.isVisible 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 import bums.lunatic.launcher.home.toast -import bums.lunatic.launcher.home.tokiz.view.BWebview -import bums.lunatic.launcher.home.tokiz.view.JxEvent import bums.lunatic.launcher.home.tokiz.view.PagedTextLayout import bums.lunatic.launcher.home.tokiz.view.PagedTextViewInterface import bums.lunatic.launcher.utils.Blog @@ -66,7 +67,7 @@ import kotlin.random.Random abstract class BaseToki : Fragment(), PagedTextViewInterface { - fun openRealm() : Realm = HistoryManager.openRealm + fun openRealm(): Realm = HistoryManager.openRealm var lastInfo: LastInfo? = null var currentPage: ContentsPageInfo? = null var saveContinuation = false @@ -95,13 +96,14 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { val extPath = "resource://android/assets/extensions/my_extension/" val extId = "messaging@booktoki468.com" var mExtension: WebExtension? = null - abstract val contentsType : String - abstract var lastNumber : Int - abstract val webcontentsName : String - abstract val afterDot : String + abstract val contentsType: String + abstract var lastNumber: Int + abstract val webcontentsName: String + abstract val afterDot: String open fun getLastedDoamin(): String { - return String.format("https://%s%d.%s", webcontentsName , lastNumber, afterDot) + return String.format("https://%s%d.%s", webcontentsName, lastNumber, afterDot) } + val OnTouchListener = object : OnTouchListener { override fun onTouch(v: View?, event: MotionEvent): Boolean { @@ -109,7 +111,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { MotionEvent.ACTION_DOWN -> { handle.removeCallbacks(mActionUp) handle.removeCallbacks(mActionDown) - if(!binding.pagedLayer.isVisible) { + if (!binding.pagedLayer.isVisible) { } } @@ -137,8 +139,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { } - - var mOnGenericMotionListener = object : View.OnGenericMotionListener{ + var mOnGenericMotionListener = object : View.OnGenericMotionListener { override fun onGenericMotion( v: View?, event: MotionEvent? @@ -166,334 +167,18 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { } } + private fun aceptUrl(string: String): Boolean { - Blog.LOGE("string >>> ${string}, domain >>> ${getLastedDoamin()} :: isAcept ${string.contains(getLastedDoamin())}") + Blog.LOGE( + "string >>> ${string}, domain >>> ${getLastedDoamin()} :: isAcept ${ + string.contains( + getLastedDoamin() + ) + }" + ) return string.contains(getLastedDoamin()) } - val mediaDelegate = object : GeckoSession.MediaDelegate { - override fun onRecordingStatusChanged( - session: GeckoSession, - devices: Array - ) { - super.onRecordingStatusChanged(session, devices) - } - } - val mediaSessionDelegate = object : MediaSession.Delegate { - override fun onActivated( - session: GeckoSession, - mediaSession: MediaSession - ) { - super.onActivated(session, mediaSession) - } - override fun onDeactivated( - session: GeckoSession, - mediaSession: MediaSession - ) { - super.onDeactivated(session, mediaSession) - } - - override fun onMetadata( - session: GeckoSession, - mediaSession: MediaSession, - meta: MediaSession.Metadata - ) { - super.onMetadata(session, mediaSession, meta) - } - - override fun onFeatures( - session: GeckoSession, - mediaSession: MediaSession, - features: Long - ) { - super.onFeatures(session, mediaSession, features) - } - - override fun onPlay( - session: GeckoSession, - mediaSession: MediaSession - ) { - super.onPlay(session, mediaSession) - } - - override fun onPause( - session: GeckoSession, - mediaSession: MediaSession - ) { - super.onPause(session, mediaSession) - } - - override fun onStop( - session: GeckoSession, - mediaSession: MediaSession - ) { - super.onStop(session, mediaSession) - } - - override fun onPositionState( - session: GeckoSession, - mediaSession: MediaSession, - state: MediaSession.PositionState - ) { - super.onPositionState(session, mediaSession, state) - } - - override fun onFullscreen( - session: GeckoSession, - mediaSession: MediaSession, - enabled: Boolean, - meta: MediaSession.ElementMetadata? - ) { - super.onFullscreen(session, mediaSession, enabled, meta) - } - } - val contentDelegate = object : GeckoSession.ContentDelegate { - - } - val progressDelegate = object : GeckoSession.ProgressDelegate { - override fun onProgressChange(session: GeckoSession, progress: Int) { - super.onProgressChange(session, progress) - } - override fun onSecurityChange( - session: GeckoSession, - securityInfo: GeckoSession.ProgressDelegate.SecurityInformation - ) { - Blog.LOGE("onSecurityChange $securityInfo from WebExtension") - super.onSecurityChange(session, securityInfo) - } - - override fun onSessionStateChange( - session: GeckoSession, - sessionState: GeckoSession.SessionState - ) { - Blog.LOGE("onSessionStateChange $sessionState from WebExtension") - super.onSessionStateChange(session, sessionState) - when(this@BaseToki) { - is Comics, is Webtoons -> { - onStateChange(sessionState) - } - else -> { - - } - } - } - - override fun onPageStart(session: GeckoSession, url: String) { - super.onPageStart(session, url) - - binding.progress.visibility = VISIBLE - if (aceptUrl(url)) { - - } else { -// session.stop() - } - } - - - - override fun onPageStop(session: GeckoSession, success: Boolean) { - Blog.LOGE("onPageStop $success from WebExtension") - super.onPageStop(session, success) - if (success && mPort != null) { - if (mPort == null) { - // No extension registered yet, let's ignore this message - return - } - if (lastedUrl?.contains("youtube.com") == true) { -// val script = "alert(JSON.stringify({Cookies : document.cookie}))" -// val encodedJs = Uri.encode(script) -// val jsUri = "javascript:$encodedJs" -// session?.loadUri(jsUri) -// session.loadUri() - } else { - binding.menuWeb.postDelayed({ - - Blog.LOGE("onPageStop $success from WebExtension ${mPort!!.name}") - val message: JSONObject = JSONObject() - try { - message.put("type", "getList") - message.put("event", "sadsadds") - message.put("tab", session.settings.screenId) - } catch (ex: JSONException) { - throw RuntimeException(ex) - } - - mPort!!.postMessage(message) - - - when (this@BaseToki) { - is Comics, is Webtoons -> { - lastInfo - } - - else -> { - - } - } - binding.progress.visibility = GONE - }, 10L) - } - } - } - } - val navigationDelegate = object : GeckoSession.NavigationDelegate { - override fun onLoadError( - session: GeckoSession, - uri: String?, - error: WebRequestError - ): GeckoResult? { - error.printStackTrace() - Blog.LOGE("onLoadError >>> ${uri} ::>> ${error.category} , ${error.code}") - if (error.code == WebRequestError.ERROR_NET_RESET) { - session.loadUri("https://naver.com") - } - return super.onLoadError(session, uri, error) - } - - override fun onNewSession( - session: GeckoSession, - uri: String - ): GeckoResult? { - Blog.LOGE("GeckoView", "onNewSession: $session from WebExtension") - - - return super.onNewSession(session, uri) - } - - override fun onLocationChange( - session: GeckoSession, - url: String?, - perms: MutableList, - hasUserGesture: Boolean - ) { - if (url?.startsWith("about") ?: true) { - - } else { - // url이 현재 로드된 주소입니다. - Blog.LOGE("GeckoView", "현재 주소: $url") - Blog.LOGE("GeckoView", "현재 session: $session") - url?.let { url -> - if (url.split("//").size > 1) { - url.replace("//", "/").replace("https:/", "https://").let { - Blog.LOGE("url >> ${url} , it >>> ${it}") - lastedUrl = url - } - } else { - lastedUrl = url - } - binding.menuWeb.checkIfDownloadable(url) - binding.menuWeb.decoViews.filter { it != null && it.id > -1 }.forEach { - if (it != null && it.id > -1) { - if (it.id == R.id.back) { - it.setOnClickListener { session.goBack() } - } else if (it.id == R.id.current_address) { - (it as? TextView)?.let { - it.tag = currentTitle - it.text = url - } - }else if (it.id == R.id.reload) { - it.setOnClickListener { session.reload() } - } - } - } - } - completePageLoad(LastInfo().apply { - this.pageUrl = url?.toUri()?.path ?: getLastedDoamin() ?: "" - this.contentsName = webcontentsName ?: "" - this.contentsType = if (url?.contains("book",true) == true) {"book"} - else if(url?.contains("new",true) == true) {"webtoon"} - else if(url?.contains("mana",true) == true) {"comics"} - else "web" - this.pageIndex = 0 - }) - } - } - - override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) { - super.onCanGoBack(session, canGoBack) - this@BaseToki.canGoBack = canGoBack - if (canGoBack) { - - } - } - - } - val portDelegate: PortDelegate = object : PortDelegate { - - override fun onPortMessage( - message: Any, port: WebExtension.Port - ) { - Blog.LOGE("PortDelegate", "Received message from extension: $message") - if (message is String && message.contains("type")) { - try { - var lPortMessage = - Gson().fromJson(message, PortMessage::class.java) - when(lPortMessage.type) { - "getListResult" -> { - lPortMessage.bookInfos?.let { onBookInfos(it.sort()) } - } - "BookContents"->{ - lPortMessage?.book?.chapterTitle?.let { onFindTitle(it) } - lPortMessage?.book?.bookContents?.let { onLoadedContents(it) } - } - "NotRegistered" -> { - binding.pagedLayer.visibility = GONE - } - "WebtoonContents"-> { - binding.pagedLayer.visibility = GONE - } - "MSG" -> { - lPortMessage.msg?.let { Toast.makeText(requireContext(),it, Toast.LENGTH_SHORT).show() } - } - "SHOWVIEWER" -> { - binding.progress.visibility = GONE - } - "PRIVATES"->{ - lPortMessage.privates?.let { - requireContext().toast("Received Msg privates form ${lPortMessage.currentPage} data => ${it?.size ?: 0}") - WorkersDb.insertBulkData(it) - } - } - else -> { - - } - } - binding.progress.visibility = GONE - } catch (e: Exception) { - e.printStackTrace() - } - - } - } - - - override fun onDisconnect(port: WebExtension.Port) { - // This port is not usable anymore. - if (port === mPort) { - mPort = null - } - } - } - val messageDelegate: MessageDelegate = object : MessageDelegate { - override fun onConnect(port: WebExtension.Port) { - mPort = port - mPort!!.setDelegate(portDelegate) - } - - override fun onMessage( - nativeApp: String, - message: Any, - sender: WebExtension.MessageSender - ): GeckoResult? { - Blog.LOGE( - "messageDelegate", - "onMessage from WebExtension: ${nativeApp} , $message , ${sender.webExtension.id}" - ) - return super.onMessage(nativeApp, message, sender) - } - - - } fun processPageUrl(pageUrl: String): String { if (pageUrl != null && pageUrl.isNotEmpty() && pageUrl.startsWith("http")) { var workingUrl = pageUrl @@ -532,6 +217,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { return workingUrl } } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -540,18 +226,182 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { Blog.LOGD(log = "onCreate ${this::class.java.name} >> savedInstanceState ${savedInstanceState}") binding = BooktokiBinding.inflate(inflater) binding.menuWeb.let { + it.visibility = View.VISIBLE it.lastDomain = getLastedDoamin() it.setOnLongClickListener { onTouch(TouchArea.Center) return@setOnLongClickListener false } it.setOnGenericMotionListener(mOnGenericMotionListener) + it.onPageStopCallback = { success -> + if (success && mPort != null) { + if (mPort != null) { + if (lastedUrl?.contains("youtube.com") == true) { + + } else { + binding.menuWeb.postDelayed({ + + Blog.LOGE("onPageStop $success from WebExtension ${mPort!!.name}") + val message: JSONObject = JSONObject() + try { + message.put("type", "getList") + message.put("event", "sadsadds") +// message.put("tab", session.settings.screenId) + } catch (ex: JSONException) { + throw RuntimeException(ex) + } + + mPort!!.postMessage(message) + + + when (this@BaseToki) { + is Comics, is Webtoons -> { + lastInfo + } + + else -> { + + } + } + binding.progress.visibility = GONE + }, 10L) + } + } + } + } + it.onPageStartCallback = { url -> + binding.progress.visibility = VISIBLE + } + it.onSessionStateChangeCallback = { sessionState -> + when (this@BaseToki) { + is Comics, is Webtoons -> { + onStateChange(sessionState) + } + + else -> { + + } + } + } + + it.onPortMessageCallback = { lPortMessage -> + try { + when (lPortMessage.type) { + "getListResult" -> { + lPortMessage.bookInfos?.let { onBookInfos(it.sort()) } + } + + "BookContents" -> { + lPortMessage?.book?.chapterTitle?.let { onFindTitle(it) } + lPortMessage?.book?.bookContents?.let { onLoadedContents(it) } + } + + "NotRegistered" -> { + binding.pagedLayer.visibility = GONE + } + + "WebtoonContents" -> { + binding.pagedLayer.visibility = GONE + } + + "MSG" -> { + lPortMessage.msg?.let { + Toast.makeText( + requireContext(), + it, + Toast.LENGTH_SHORT + ).show() + } + } + + "SHOWVIEWER" -> { + binding.progress.visibility = GONE + } + + "PRIVATES" -> { + lPortMessage.privates?.let { + requireContext().toast("Received Msg privates form ${lPortMessage.currentPage} data => ${it?.size ?: 0}") + WorkersDb.insertBulkData(it) + } + } + + else -> { + + } + } + binding.progress.visibility = GONE + } catch (e: Exception) { + e.printStackTrace() + } + } + it.onLocationChangeCallback = { url -> + if (url?.startsWith("about") ?: true) { + + } else { + // url이 현재 로드된 주소입니다. + Blog.LOGE("GeckoView", "현재 주소: $url") + Blog.LOGE("GeckoView", "현재 session: ${binding.menuWeb.session}") + url?.let { url -> + if (url.split("//").size > 1) { + url.replace("//", "/").replace("https:/", "https://").let { + Blog.LOGE("url >> ${url} , it >>> ${it}") + lastedUrl = url + } + } else { + lastedUrl = url + } +// binding.menuWeb.checkIfDownloadable(url) +// binding.menuWeb.decoViews.filter { it != null && it.id > -1 }.forEach { +// if (it != null && it.id > -1) { +// if (it.id == R.id.back) { +// it.setOnClickListener { binding.menuWeb.session?.goBack() } +// } else if (it.id == R.id.current_address) { +// (it as? TextView)?.let { +// it.tag = currentTitle +// it.text = url +// } +// }else if (it.id == R.id.reload) { +// it.setOnClickListener { binding.menuWeb.session?.reload() } +// } +// } +// } + } + completePageLoad(LastInfo().apply { + this.pageUrl = url?.toUri()?.path ?: getLastedDoamin() ?: "" + this.contentsName = webcontentsName ?: "" + this.contentsType = if (url?.contains("book", true) == true) { + "book" + } else if (url?.contains("new", true) == true) { + "webtoon" + } else if (url?.contains("mana", true) == true) { + "comics" + } else "web" + this.pageIndex = 0 + }) + } + } + it.jxInteface = { jxEvent -> + when (jxEvent) { + JxEvent.SCROLL_UP -> sendScrollDown(true) + JxEvent.SCROLL_DOWN -> sendScrollDown(false) + JxEvent.SWIPE_LEFT -> if (contentsType == "comics") sendViewerTouch("left") else actionNextEvent() + JxEvent.SWIPE_RIGHT -> if (contentsType == "comics") sendViewerTouch("right") else actionPrevEvent() + else -> {} + } + } + (activity as? NeoRssActivity)?.let { activity -> + it.decoViews.clear() + it.decoViews.add(activity.findViewById(R.id.current_address)) + it.decoViews.add(activity.findViewById(R.id.back)) + it.decoViews.add(activity.findViewById(R.id.reload)) + it.decoViews.add(activity.findViewById(R.id.dl_video)) + } } binding.btnList.setOnClickListener { v -> lastedUrl?.let { Uri.parse(it).path?.let { - HistoryManager.getBookInfos(contentsType,processPageUrl(it), { + HistoryManager.getBookInfos(contentsType, processPageUrl(it), { it?.let { it.pages.sortBy { it.pathUrl } Blog.LOGE("bind ing.btnList it >>> $it") @@ -576,105 +426,25 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { binding.pagedLayer.visibility = GONE goToHome() } - getRuntime()?.apply { - val sessionSettings = GeckoSessionSettings.Builder() - .usePrivateMode(isPrivateMode) - .allowJavascript(true) - .screenId(Random(Int.MAX_VALUE).nextInt()) - .build() - val session: GeckoSession = GeckoSession(sessionSettings) - session.open(this) - session.settings - session.promptDelegate = object : GeckoSession.PromptDelegate { - override fun onAlertPrompt( - session: GeckoSession, - prompt: GeckoSession.PromptDelegate.AlertPrompt - ): GeckoResult? { - -// Blog.LOGE("prompt.message >>> ${prompt.message}") - - if (prompt.message?.contains("COOKIES") == true) { - binding.menuWeb.mGKCookie = Gson().fromJson(prompt.message,BWebview.GKCookie::class.java) - } - prompt.dismiss() - return super.onAlertPrompt(session, prompt) - } - } - binding.menuWeb.setSession(session) - binding.menuWeb.jxInteface = { jxEvent -> - Blog.LOGE("jxEvent $jxEvent") - when(jxEvent) { - JxEvent.SCROLL_UP -> sendScrollDown(true) - JxEvent.SCROLL_DOWN -> sendScrollDown(false) - JxEvent.SWIPE_LEFT -> { - if (contentsType.equals("comics", true)) { - sendViewerTouch("left") - } else { - actionNextEvent() - } - } - JxEvent.SWIPE_RIGHT -> { - if (contentsType.equals("comics", true)) { - sendViewerTouch("right") - } else { - actionPrevEvent() - } - } - else -> {} - } - } - session.contentDelegate = contentDelegate - session.progressDelegate = progressDelegate - session.navigationDelegate = navigationDelegate - this.webExtensionController.setAddonManagerDelegate(addonManagerDelegate) - session.mediaDelegate = mediaDelegate - session.mediaSessionDelegate = mediaSessionDelegate - this.webExtensionController - .ensureBuiltIn(extPath, extId) - .accept( // Register message delegate for background script - { extension: WebExtension? -> - ThreadUtils.runOnUiThread( - Runnable { - if (extension != null) { - session.webExtensionController.setMessageDelegate( - extension, - messageDelegate, - mPortNam - ) - } - }) - }, - { e: Throwable? -> Log.e("MessageDelegate", "Error registering WebExtension", e) }) - } val nullCursor = PointerIcon.getSystemIcon(requireContext(), PointerIcon.TYPE_NULL) binding.root.setPointerIcon(nullCursor) - - - binding.menuWeb - (activity as? NeoRssActivity)?.let { activity -> - binding.menuWeb.decoViews.add(activity.findViewById(R.id.current_address)) - binding.menuWeb.decoViews.add(activity.findViewById(R.id.back)) - binding.menuWeb.decoViews.add(activity.findViewById(R.id.reload)) - binding.menuWeb.decoViews.add(activity.findViewById(R.id.dl_video)) - } return binding.root } - - private fun getHistory() : List? { + private fun getHistory(): List? { try { var realm = openRealm() - return realm.query().query("contentsType == $0", contentsType).find()?.copyFromRealm()?.reversed() - }catch (e: Exception){ + return realm.query().query("contentsType == $0", contentsType).find() + ?.copyFromRealm()?.reversed() + } catch (e: Exception) { e.printStackTrace() } return null } - override fun onStart() { super.onStart() } @@ -686,8 +456,9 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { loadLastInfo() } - fun getLastinfo() : LastInfo? { - lastInfo = openRealm().query("contentsType == $0", contentsType).find().last()?.copyFromRealm() + fun getLastinfo(): LastInfo? { + lastInfo = openRealm().query("contentsType == $0", contentsType).find().last() + ?.copyFromRealm() Blog.LOGE("lastInfo >>> ${Gson().toJson(lastInfo)}") return lastInfo } @@ -710,7 +481,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { ) ) currentPage?.pathUrl?.let { targetPath -> - if (targetPath.startsWith("http",true)) { + if (targetPath.startsWith("http", true)) { targetUrl = targetPath } else { targetUrl = getLastedDoamin() + targetPath @@ -740,7 +511,6 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { } - fun onBookInfos(jsonString: String) { Blog.LOGE("onBookInfos", "jsonString >> ${jsonString}") val realm = openRealm() @@ -750,7 +520,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { var infosj: PageInfosJ? = null infosj = Gson().fromJson(jsonString, PageInfosJ::class.java) - HistoryManager.getBookInfos(contentsType,infosj.bookPageUrl!!) { + HistoryManager.getBookInfos(contentsType, infosj.bookPageUrl!!) { if (it != null) { infos = copyToRealm(it!!, UpdatePolicy.ALL) @@ -780,7 +550,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { e.printStackTrace() } finally { infos?.bookPageUrl?.let { - HistoryManager.getBookInfos(contentsType,it) { + HistoryManager.getBookInfos(contentsType, it) { it?.let { Blog.LOGE(s(), "onBookInfos it >> ${it}") activity?.runOnUiThread { showList(it) } @@ -800,15 +570,17 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { infosj.bookPageUrl?.let { infosj.bookPageUrl = processPageUrl(it) } - infosj.pages?.forEach {item -> item.pathUrl?.let { + infosj.pages?.forEach { item -> item.pathUrl?.let { - item.pathUrl = processPageUrl(it) + item.pathUrl?.let { + item.pathUrl = processPageUrl(it) + } + item.bookPageUrl?.let { + item.bookPageUrl = processPageUrl(it) + } } - item.bookPageUrl?.let { - item.bookPageUrl = processPageUrl(it) - } - }} - HistoryManager.getBookInfos(contentsType,processPageUrl(infosj.bookPageUrl!!)) { + } + HistoryManager.getBookInfos(contentsType, processPageUrl(infosj.bookPageUrl!!)) { if (it != null) { infos = copyToRealm(it!!, UpdatePolicy.ALL) @@ -839,7 +611,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { e.printStackTrace() } finally { infos?.bookPageUrl?.let { - HistoryManager.getBookInfos(contentsType,it) { + HistoryManager.getBookInfos(contentsType, it) { it?.let { Blog.LOGE(s(), "onBookInfos it >> ${it}") activity?.runOnUiThread { showList(it) } @@ -954,7 +726,6 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { } - fun showToast(origin: String) { activity?.runOnUiThread { val toast = Toast(requireContext()) @@ -986,7 +757,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { fun moveToNext(pathUrl: String?) { if (pathUrl != null && pathUrl.length > 6) { - HistoryManager.getNextPage(contentsType,pathUrl) { + HistoryManager.getNextPage(contentsType, pathUrl) { if (it != null && (it.pathUrl?.length ?: 0) > 6) { moveTo(it) } else { @@ -998,7 +769,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { fun moveToPrev(pathUrl: String?) { if (pathUrl != null && pathUrl.length > 6) { - HistoryManager.getPrevPage(contentsType,pathUrl) { + HistoryManager.getPrevPage(contentsType, pathUrl) { if (it != null && (it.pathUrl?.length ?: 0) > 6) { moveTo(it) } else { @@ -1015,21 +786,26 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { contentsPageInfo.chapterTitle?.let { onFindTitle(it) } if (contentsPageInfo.pathUrl?.startsWith("http") == true) { binding.menuWeb.loadUrl(pathUrl) - } else { + } else { binding.menuWeb.loadUrl(getLastedDoamin() + contentsPageInfo.pathUrl!!) } - HistoryManager.save(HistoryItem().putHistory(contentsPageInfo, contentsPageInfo.pathUrl!!)) + HistoryManager.save( + HistoryItem().putHistory( + contentsPageInfo, + contentsPageInfo.pathUrl!! + ) + ) } if (currentPage?.pathUrl.equals(pathUrl)) { applyCurrentBook(currentPage!!) } else { - HistoryManager.getBookPageInfo(contentsType,pathUrl) { + HistoryManager.getBookPageInfo(contentsType, pathUrl) { Blog.LOGE("contentsLoad :::: pathUrl >> ${pathUrl} , book >> ${it}") if (it != null && it.isValidBook()) { currentPage = it applyCurrentBook(it) - } else if(lastInfo != null){ + } else if (lastInfo != null) { binding.pagedLayer.visibility = GONE binding.menuWeb.loadUrl(getLastedDoamin() + lastInfo!!.pageUrl) } else { @@ -1114,7 +890,6 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { // } - fun showAlert(alert: String) { Log.i(TAG, "showAlert >> " + alert) } @@ -1130,14 +905,24 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { it?.let { this@BaseToki.currentPage = it currentChapter = it?.chapterNum ?: 0 - HistoryManager.save(historyItem = HistoryItem().putHistory(it,lastedUrl!! )) + HistoryManager.save( + historyItem = HistoryItem().putHistory( + it, + lastedUrl!! + ) + ) (currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)?.let { - HistoryManager.setBookPageInfo(contentsType,it,binding.pagedLayer.current()) + HistoryManager.setBookPageInfo( + contentsType, + it, + binding.pagedLayer.current() + ) } val realm = openRealm() realm.writeBlocking { - var q = this.query("contentsType == $0", contentsType).find() - if(q.size > 0) { + var q = this.query("contentsType == $0", contentsType) + .find() + if (q.size > 0) { q.last()?.let { it.title = currentTitle it.chapter = currentChapter @@ -1151,10 +936,22 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { it.chapter = currentChapter it.pageIndex = 0 it.contentsType = currentPage!!.contentsType - it._id = if (lastedUrl?.contains("book",true) == true) {"book"} - else if(lastedUrl?.contains("new",true) == true) {"webtoon"} - else if(lastedUrl?.contains("mana",true) == true) {"comics"} - else "web" + it._id = + if (lastedUrl?.contains("book", true) == true) { + "book" + } else if (lastedUrl?.contains( + "new", + true + ) == true + ) { + "webtoon" + } else if (lastedUrl?.contains( + "mana", + true + ) == true + ) { + "comics" + } else "web" copyToRealm(it, UpdatePolicy.ALL) this@BaseToki.lastInfo = it } @@ -1200,7 +997,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { lastedUrl?.let { Uri.parse(it)?.let { uri -> uri.path?.let { - HistoryManager.getBookPageInfo(contentsType,it) { + HistoryManager.getBookPageInfo(contentsType, it) { it?.let { this@BaseToki.currentPage = it currentChapter = it?.chapterNum ?: 0 @@ -1213,10 +1010,18 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { ) } } - HistoryManager.getBooPageInfoContentsSave(contentsType,it, contents) + HistoryManager.getBooPageInfoContentsSave( + contentsType, + it, + contents + ) } if (saveContinuation) { - binding.menuWeb.postDelayed({moveToNext(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)}, 10000) + binding.menuWeb.postDelayed({ + moveToNext( + currentPage?.pathUrl ?: lastedUrl?.toUri()?.path + ) + }, 10000) } } @@ -1303,10 +1108,10 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { Blog.LOGE("lastInfo >>>> ${this@BaseToki.lastInfo}") } } - } else if ((lastInfo._id?.length ?: 0) > 3){ + } else if ((lastInfo._id?.length ?: 0) > 3) { realm.writeBlocking { Blog.LOGE("lastInfo >>>> $lastInfo") - copyToRealm(lastInfo,UpdatePolicy.ALL) + copyToRealm(lastInfo, UpdatePolicy.ALL) } } // Blog.LOGD(log = "Successfully opened realm: ${realm.configuration.name}") @@ -1314,13 +1119,13 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { fun updateLastInfo(pagedTextLayout: PagedTextLayout) { (currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)?.let { - HistoryManager.setBookPageInfo(contentsType,it,binding.pagedLayer.current()) + HistoryManager.setBookPageInfo(contentsType, it, binding.pagedLayer.current()) } val configuration: Configuration = getResources().configuration val realm = openRealm() realm.writeBlocking { var q = this.query("contentsType == $0", contentsType) - if(q.count().find() > 0) { + if (q.count().find() > 0) { q.find().last()?.let { it.displayOrientation = configuration.orientation it.title = currentTitle @@ -1340,12 +1145,15 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { it.chapter = currentChapter it.pageIndex = 0 - it._id = if (lastedUrl?.contains("book",true) == true) {"book"} - else if(lastedUrl?.contains("new",true) == true) {"webtoon"} - else if(lastedUrl?.contains("mana",true) == true) {"comics"} - else "web" + it._id = if (lastedUrl?.contains("book", true) == true) { + "book" + } else if (lastedUrl?.contains("new", true) == true) { + "webtoon" + } else if (lastedUrl?.contains("mana", true) == true) { + "comics" + } else "web" it.contentsType = currentPage?.contentsType ?: it._id - copyToRealm(it,UpdatePolicy.ALL) + copyToRealm(it, UpdatePolicy.ALL) this@BaseToki.lastInfo = it } if (currentTitle.length > 0 && currentChapter > 0) { @@ -1359,13 +1167,14 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { try { message.put("type", "scrollDown") message.put("max", binding.pagedLayer.size()) - message.put("current",binding.pagedLayer.current()) + message.put("current", binding.pagedLayer.current()) } catch (ex: JSONException) { throw RuntimeException(ex) } mPort?.postMessage(message) Blog.LOGD(log = "Successfully opened realm: ${realm.configuration.name}") } + private fun sendViewerTouch(string: String) { val message: JSONObject = JSONObject() try { @@ -1377,6 +1186,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { Blog.LOGE(Gson().toJson(message)) mPort?.postMessage(message) } + fun sendScrollDown(isUp: Boolean) { val message: JSONObject = JSONObject() try { @@ -1390,104 +1200,11 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { } -// override fun onKeyClick(keyCode: Int): Boolean { -// when (keyCode) { -// KeyEvent.KEYCODE_VOLUME_DOWN -> { -// actionNextEvent() -// } -// -// KeyEvent.KEYCODE_VOLUME_UP -> { -// actionPrevEvent() -// } -// -// KeyEvent.KEYCODE_VOLUME_MUTE -> { -// actionNextEvent() -// } -// -// } -// return super.onKeyClick(keyCode) -// } -// -// override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean { -// Blog.LOGE("dispatchTrackballEvent ev?.device?.name >>> ${ev?.device?.name}") -// return super.dispatchTrackballEvent(ev) -// } -// -// @SuppressLint("RestrictedApi") -// override fun dispatchKeyShortcutEvent(ev: KeyEvent): Boolean { -// Blog.LOGE("dispatchKeyShortcutEvent ev?.device?.name >>> ${ev?.device?.name}") -// return super.dispatchKeyShortcutEvent(ev) -// } -// -// -// -// override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { -// Blog.LOGE("dispatchKeyShortcutEvent ev?.device?.name >>> ${ev?.device?.name}") -// return super.dispatchGenericMotionEvent(ev) -// } -// -// override fun dispatchKeyEvent(ev: KeyEvent): Boolean { -// Blog.LOGE("dispatch ev?.device?.name >>> ${ev?.device?.name}") -// if (ev?.device?.name?.contains("SM-031N Mouse") == true) { -// when (ev.action) { -// ACTION_UP -> { -// Blog.LOGE("dispatch dispatchKeyEvent>>> ${ev}") -// when (ev.keyCode) { -// KEYCODE_BUTTON_Y -> { -// actionPrevEvent() -// } -// -// KEYCODE_BUTTON_X -> { -// -// } -// -// KEYCODE_BUTTON_A -> { -// actionNextEvent() -// } -// -// KEYCODE_BUTTON_B -> { -// -// } -// -// KEYCODE_DPAD_DOWN -> { -// -// } -// -// KEYCODE_DPAD_UP -> { -// -// } -// -// KEYCODE_BUTTON_START -> { -// goToHome() -// } -// -// KEYCODE_BUTTON_SELECT -> { -// -// saveContinuation = !saveContinuation -// if (saveContinuation) { -// moveToNext(currentBooinfo?.pathUrl ?: lastedUrl?.toUri()?.path) -// } -// } -// -// else -> {} -// } -// } -// -// else -> {} -// } -// return true -// } -// return super.dispatchKeyEvent(ev) -// } - private fun goToHome() { binding.menuWeb.loadUrl(getLastedDoamin()) } - - - companion object { private const val TAG = "DualScreenStatus" } @@ -1499,16 +1216,17 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { } -class YouTube : BaseToki(){ +class YouTube : BaseToki() { override val contentsType = "youtube" - override var lastNumber : Int = 143 - override val webcontentsName : String = "youtube" + override var lastNumber: Int = 143 + override val webcontentsName: String = "youtube" override val afterDot = "com" override var isPrivateMode: Boolean = true override fun getLastedDoamin(): String { return String.format("https://%s.%s", webcontentsName, afterDot) } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -1552,11 +1270,12 @@ class YouTube : BaseToki(){ override fun onLongClick() { } } + class Comics : BaseToki(), PagedTextViewInterface { override val contentsType = "comics" - override var lastNumber : Int = 468 - override val webcontentsName : String = "manatoki" + override var lastNumber: Int = 468 + override val webcontentsName: String = "manatoki" override val afterDot = "net" override fun onCreateView( inflater: LayoutInflater, @@ -1598,6 +1317,7 @@ class Comics : BaseToki(), PagedTextViewInterface { TouchArea.DoubleLeft -> { actionPrevEvent(true) } + else -> { } @@ -1638,15 +1358,17 @@ class Comics : BaseToki(), PagedTextViewInterface { } } + class Magnet : BaseToki(), PagedTextViewInterface { // "https://btsearch.love/ override val contentsType = "btsearch" - override var lastNumber : Int = 143 - override val webcontentsName : String = "btsearch" + override var lastNumber: Int = 143 + override val webcontentsName: String = "btsearch" override val afterDot = "love" override fun getLastedDoamin(): String { return String.format("https://%s.%s", webcontentsName, afterDot) } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -1687,6 +1409,7 @@ class Magnet : BaseToki(), PagedTextViewInterface { TouchArea.DoubleLeft -> { actionPrevEvent(true) } + else -> { } @@ -1727,11 +1450,12 @@ class Magnet : BaseToki(), PagedTextViewInterface { } } + class Novels : BaseToki(), PagedTextViewInterface { override val contentsType = "book" - override var lastNumber : Int = 468 - override val webcontentsName : String = "booktoki" + override var lastNumber: Int = 468 + override val webcontentsName: String = "booktoki" override val afterDot = "com" override fun onCreateView( inflater: LayoutInflater, @@ -1773,6 +1497,7 @@ class Novels : BaseToki(), PagedTextViewInterface { TouchArea.DoubleLeft -> { actionPrevEvent(true) } + else -> { } @@ -1814,15 +1539,17 @@ class Novels : BaseToki(), PagedTextViewInterface { } } + class Perplexity : BaseToki(), PagedTextViewInterface { // "https://btsearch.love/ override val contentsType = "perplexity" - override var lastNumber : Int = 143 - override val webcontentsName : String = "www.perplexity" + override var lastNumber: Int = 143 + override val webcontentsName: String = "www.perplexity" override val afterDot = "ai" override fun getLastedDoamin(): String { return String.format("https://%s.%s", webcontentsName, afterDot) } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -1863,6 +1590,7 @@ class Perplexity : BaseToki(), PagedTextViewInterface { TouchArea.DoubleLeft -> { actionPrevEvent(true) } + else -> { } @@ -1903,11 +1631,12 @@ class Perplexity : BaseToki(), PagedTextViewInterface { } } + class Webtoons : BaseToki(), PagedTextViewInterface { override val contentsType = "webtoon" - override var lastNumber : Int = 468 - override val webcontentsName : String = "newtoki" + override var lastNumber: Int = 468 + override val webcontentsName: String = "newtoki" override val afterDot = "com" override fun onCreateView( inflater: LayoutInflater, @@ -1951,6 +1680,7 @@ class Webtoons : BaseToki(), PagedTextViewInterface { TouchArea.DoubleLeft -> { actionPrevEvent(true) } + else -> { } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt new file mode 100644 index 00000000..aa3cedec --- /dev/null +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt @@ -0,0 +1,1251 @@ +package bums.lunatic.launcher.home.tokiz + +import android.content.DialogInterface +import android.content.Intent +import android.content.res.Configuration +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.text.InputType +import android.text.SpannableStringBuilder +import android.text.style.RelativeSizeSpan +import android.util.Log +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.PointerIcon +import android.view.View +import android.view.View.GONE +import android.view.View.OnTouchListener +import android.view.View.VISIBLE +import android.view.View.inflate +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.ImageButton +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.core.net.toUri +import androidx.core.view.isVisible +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 +import bums.lunatic.launcher.home.toast +import bums.lunatic.launcher.home.tokiz.view.PagedTextLayout +import bums.lunatic.launcher.home.tokiz.view.PagedTextViewInterface +import bums.lunatic.launcher.utils.Blog +import bums.lunatic.launcher.workers.WorkersDb +import com.google.gson.Gson +import io.realm.kotlin.Realm +import io.realm.kotlin.UpdatePolicy +import io.realm.kotlin.ext.copyFromRealm +import io.realm.kotlin.ext.query +import org.json.JSONException +import org.json.JSONObject +import org.mozilla.gecko.util.ThreadUtils +import org.mozilla.geckoview.GeckoResult +import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.GeckoSessionSettings +import org.mozilla.geckoview.MediaSession +import org.mozilla.geckoview.WebExtension +import org.mozilla.geckoview.WebExtension.MessageDelegate +import org.mozilla.geckoview.WebExtension.PortDelegate +import org.mozilla.geckoview.WebExtensionController.AddonManagerDelegate +import org.mozilla.geckoview.WebRequestError +import java.lang.System.currentTimeMillis +import java.net.URL +import java.text.SimpleDateFormat +import java.util.Date +import kotlin.random.Random + +// 기존 BaseToki 및 하위 클래스들을 통합한 단일 클래스 +class TokiFragment : Fragment(), PagedTextViewInterface { + + // --- Configuration Properties (Arguments에서 로드) --- + private lateinit var contentsType: String + private var lastNumber: Int = 0 + private lateinit var webcontentsName: String + private lateinit var afterDot: String + private var useNumberInUrl: Boolean = true + private var enableGestures: Boolean = true + var isPrivateMode = false + + // --- Existing Logic --- + fun openRealm(): Realm = HistoryManager.openRealm + var lastInfo: LastInfo? = null + var currentPage: ContentsPageInfo? = null + var saveContinuation = false + + val handle = object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + if (msg.what == 0) { + (msg.obj as? ReaderConfig)?.let { + } + } + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + Blog.LOGD(log = "onConfigurationChanged ${this::class.java.name} >> newConfig ${newConfig}") + } + + var lastedUrl: String? = null + var canGoBack: Boolean? = null + protected lateinit var binding: BooktokiBinding + + var mPort: WebExtension.Port? = null + val mPortNam = "browser" + val extPath = "resource://android/assets/extensions/my_extension/" + val extId = "messaging@booktoki468.com" + var mExtension: WebExtension? = null + + // 통합된 URL 생성 로직 + fun getLastedDoamin(): String { + return if (useNumberInUrl) { + String.format("https://%s%d.%s", webcontentsName, lastNumber, afterDot) + } else { + String.format("https://%s.%s", webcontentsName, afterDot) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + contentsType = it.getString(ARG_TYPE, "web") + lastNumber = it.getInt(ARG_LAST_NUM, 0) + webcontentsName = it.getString(ARG_NAME, "") + afterDot = it.getString(ARG_DOT, "com") + useNumberInUrl = it.getBoolean(ARG_USE_NUM_URL, true) + enableGestures = it.getBoolean(ARG_ENABLE_GESTURE, true) + isPrivateMode = it.getBoolean(ARG_PRIVATE, false) + } + } + + // --- Companion Object (Factory Methods for Creation) --- + companion object { + private const val TAG = "TokiFragment" + private const val ARG_TYPE = "arg_type" + private const val ARG_LAST_NUM = "arg_last_num" + private const val ARG_NAME = "arg_name" + private const val ARG_DOT = "arg_dot" + private const val ARG_USE_NUM_URL = "arg_use_num_url" + private const val ARG_ENABLE_GESTURE = "arg_enable_gesture" + private const val ARG_PRIVATE = "arg_private" + + fun newInstanceComics(): TokiFragment = TokiFragment().apply { + arguments = Bundle().apply { + putString(ARG_TYPE, "comics") + putInt(ARG_LAST_NUM, 468) + putString(ARG_NAME, "manatoki") + putString(ARG_DOT, "net") + putBoolean(ARG_USE_NUM_URL, true) + putBoolean(ARG_ENABLE_GESTURE, true) + } + } + + fun newInstanceWebtoons(): TokiFragment = TokiFragment().apply { + arguments = Bundle().apply { + putString(ARG_TYPE, "webtoon") + putInt(ARG_LAST_NUM, 468) + putString(ARG_NAME, "newtoki") + putString(ARG_DOT, "com") + putBoolean(ARG_USE_NUM_URL, true) + putBoolean(ARG_ENABLE_GESTURE, true) + } + } + + fun newInstanceNovels(): TokiFragment = TokiFragment().apply { + arguments = Bundle().apply { + putString(ARG_TYPE, "book") + putInt(ARG_LAST_NUM, 468) + putString(ARG_NAME, "booktoki") + putString(ARG_DOT, "com") + putBoolean(ARG_USE_NUM_URL, true) + putBoolean(ARG_ENABLE_GESTURE, true) + } + } + + fun newInstanceYouTube(): TokiFragment = TokiFragment().apply { + arguments = Bundle().apply { + putString(ARG_TYPE, "youtube") + putInt(ARG_LAST_NUM, 143) + putString(ARG_NAME, "youtube") + putString(ARG_DOT, "com") + putBoolean(ARG_USE_NUM_URL, false) // YouTube doesn't use number in URL + putBoolean(ARG_ENABLE_GESTURE, false) // YouTube disables gestures + putBoolean(ARG_PRIVATE, true) + } + } + + fun newInstanceMagnet(): TokiFragment = TokiFragment().apply { + arguments = Bundle().apply { + putString(ARG_TYPE, "btsearch") + putInt(ARG_LAST_NUM, 143) + putString(ARG_NAME, "btsearch") + putString(ARG_DOT, "love") + putBoolean(ARG_USE_NUM_URL, false) + putBoolean(ARG_ENABLE_GESTURE, true) + } + } + + fun newInstancePerplexity(): TokiFragment = TokiFragment().apply { + arguments = Bundle().apply { + putString(ARG_TYPE, "perplexity") + putInt(ARG_LAST_NUM, 143) + putString(ARG_NAME, "www.perplexity") + putString(ARG_DOT, "ai") + putBoolean(ARG_USE_NUM_URL, false) + putBoolean(ARG_ENABLE_GESTURE, true) + } + } + } + + // --- Unified Gesture Implementation --- + + override fun onTouch(touchArea: TouchArea) { + if (!enableGestures) return + + Blog.LOGD(log = "onTouch ${touchArea}") + when (touchArea) { + TouchArea.Center -> { } + TouchArea.Right -> actionNextEvent() + TouchArea.Left -> actionPrevEvent() + TouchArea.DoubleRight -> actionNextEvent(true) + TouchArea.DoubleLeft -> actionPrevEvent(true) + else -> { } + } + } + + override fun onSwipeLeft(count: Int) { + if (!enableGestures) return + Blog.LOGD(log = "onSwipeLeft ${count}") + + // Comics는 단순 이동, 나머지는 count > 1 체크 등의 미세한 차이가 있었으나 + // 대부분의 구현체에서 count > 1 로직을 공유하므로 통합함. + // Comics의 경우 sendViewerTouch("left") 로직이 JxEvent에 있었으므로 여기서는 페이지 이동 로직으로 통일. + actionNextEvent(count > 1) + } + + override fun onSwipeRight(count: Int) { + if (!enableGestures) return + Blog.LOGD(log = "onSwipeRight ${count}") + actionPrevEvent(count > 1) + } + + override fun onSwipeDown(touchCount: Int) { + if (!enableGestures) return + + if (touchCount == 2) { + if (binding.pagedLayer.isVisible) { + binding.pagedLayer.visibility = GONE + } + // Novels의 경우 VISIBLE 처리가 있었으나, GONE 처리 후 필요 시 메뉴를 보여주는 흐름으로 통합 + if(contentsType == "book") { + binding.menuWeb.visibility = VISIBLE + } + } + } + + override fun onSwipeUp(touchCount: Int) { + // Implementation typically empty or same across fragments + } + + override fun onLongClick() { + if (!enableGestures) return + Blog.LOGD(log = "onLongClick") + } + + override fun onTimeoverTouch() { + // Implementation typically empty + } + + fun back() { + if (contentsType == "youtube") { + binding.menuWeb.session?.goBack() + } else { + // binding.menuWeb.session?.goBack() + // 기존 BaseToki 주석 처리됨 + } + } + + // --- Original Logic (Unchanged below, just pasted for completeness of the class) --- + + val OnTouchListener = object : OnTouchListener { + + override fun onTouch(v: View?, event: MotionEvent): Boolean { + when (event.getAction()) { + MotionEvent.ACTION_DOWN -> { + handle.removeCallbacks(mActionUp) + handle.removeCallbacks(mActionDown) + if(!binding.pagedLayer.isVisible) { + + } + } + + MotionEvent.ACTION_UP -> { + handle.removeCallbacks(mActionUp) + handle.removeCallbacks(mActionDown) + } + } + return false + } + + var mActionDown: Runnable = object : Runnable { + override fun run() { + sendScrollDown(false) + handle.postDelayed(this, 150) + } + } + var mActionUp: Runnable = object : Runnable { + override fun run() { + sendScrollDown(true) + handle.postDelayed(this, 150) + } + } + } + + var mOnGenericMotionListener = object : View.OnGenericMotionListener{ + override fun onGenericMotion( + v: View?, + event: MotionEvent? + ): Boolean { + Blog.LOGE("event >>>> $event") + return true + } + } + + val addonManagerDelegate = object : AddonManagerDelegate { + override fun onReady(extension: WebExtension) { + Blog.LOGE("onReady ${extension.id} from WebExtension") + mExtension = extension + + } + + override fun onEnabling(extension: WebExtension) { + Blog.LOGE("onEnabling ${extension.id} from WebExtension") + } + + override fun onEnabled(extension: WebExtension) { + Blog.LOGE("onEnabled ${extension.id} from WebExtension") +// extension?.setMessageDelegate(messageDelegate, mPortNam) + mExtension = extension + + } + } + private fun aceptUrl(string: String): Boolean { + Blog.LOGE("string >>> ${string}, domain >>> ${getLastedDoamin()} :: isAcept ${string.contains(getLastedDoamin())}") + return string.contains(getLastedDoamin()) + } + + fun processPageUrl(pageUrl: String): String { + if (pageUrl != null && pageUrl.isNotEmpty() && pageUrl.startsWith("http")) { + var workingUrl = pageUrl + val paths = workingUrl.split("/").toMutableList() + if (paths.isNotEmpty()) { + val last = paths.last() + if (last.isNotEmpty() && last.all { it in '0'..'9' }) { + Log.d("TAG", "only nums $last") + } else { + Log.d("TAG", "not only nums $last") + paths.removeAt(paths.size - 1) + } + } + workingUrl = paths.joinToString("/") + // URL이 정상적으로 파싱되는지 확인(try-catch 권장) + val path = try { + URL(workingUrl).path + } catch (e: Exception) { + Log.d("TAG", "Invalid URL: $workingUrl") + return pageUrl + } + return path + } else { + var workingUrl = pageUrl + val paths = workingUrl.split("/").toMutableList() + if (paths.isNotEmpty()) { + val last = paths.last() + if (last.isNotEmpty() && last.all { it in '0'..'9' }) { + Log.d("TAG", "only nums $last") + } else { + Log.d("TAG", "not only nums $last") + paths.removeAt(paths.size - 1) + } + } + workingUrl = paths.joinToString("/") + return workingUrl + } + } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + Blog.LOGD(log = "onCreate ${this::class.java.name} >> savedInstanceState ${savedInstanceState}") + binding = BooktokiBinding.inflate(inflater) + binding.menuWeb.let { + it.visibility = View.VISIBLE + it.lastDomain = getLastedDoamin() + it.setOnLongClickListener { + onTouch(TouchArea.Center) + return@setOnLongClickListener false + } + it.setOnGenericMotionListener(mOnGenericMotionListener) + it.onPageStopCallback = { success-> + if (success && mPort != null) { + if (mPort != null) { + if (lastedUrl?.contains("youtube.com") == true) { + + } else { + binding.menuWeb.postDelayed({ + + Blog.LOGE("onPageStop $success from WebExtension ${mPort!!.name}") + val message: JSONObject = JSONObject() + try { + message.put("type", "getList") + message.put("event", "sadsadds") +// message.put("tab", session.settings.screenId) + } catch (ex: JSONException) { + throw RuntimeException(ex) + } + + mPort!!.postMessage(message) + + + // 타입별 분기 처리 (기존 when 절 대체) + if (contentsType == "comics" || contentsType == "webtoon") { + lastInfo + } + binding.progress.visibility = GONE + }, 10L) + } + } + } + } + it.onPageStartCallback = { url -> + binding.progress.visibility = VISIBLE + } + it.onSessionStateChangeCallback = {sessionState -> + if (contentsType == "comics" || contentsType == "webtoon") { + onStateChange(sessionState) + } + } + + it.onPortMessageCallback = { lPortMessage -> + try { + when(lPortMessage.type) { + "getListResult" -> { + lPortMessage.bookInfos?.let { onBookInfos(it.sort()) } + } + "BookContents"->{ + lPortMessage?.book?.chapterTitle?.let { onFindTitle(it) } + lPortMessage?.book?.bookContents?.let { onLoadedContents(it) } + } + "NotRegistered" -> { + binding.pagedLayer.visibility = GONE + } + "WebtoonContents"-> { + binding.pagedLayer.visibility = GONE + } + "MSG" -> { + lPortMessage.msg?.let { Toast.makeText(requireContext(),it, Toast.LENGTH_SHORT).show() } + } + "SHOWVIEWER" -> { + binding.progress.visibility = GONE + } + "PRIVATES"->{ + lPortMessage.privates?.let { + requireContext().toast("Received Msg privates form ${lPortMessage.currentPage} data => ${it?.size ?: 0}") + WorkersDb.insertBulkData(it) + } + } + else -> { + + } + } + binding.progress.visibility = GONE + } catch (e: Exception) { + e.printStackTrace() + } + } + it.onLocationChangeCallback = { url -> + if (url?.startsWith("about") ?: true) { + + } else { + // url이 현재 로드된 주소입니다. + Blog.LOGE("GeckoView", "현재 주소: $url") + Blog.LOGE("GeckoView", "현재 session: ${binding.menuWeb.session}") + url?.let { url -> + if (url.split("//").size > 1) { + url.replace("//", "/").replace("https:/", "https://").let { + Blog.LOGE("url >> ${url} , it >>> ${it}") + lastedUrl = url + } + } else { + lastedUrl = url + } + } + completePageLoad(LastInfo().apply { + this.pageUrl = url?.toUri()?.path ?: getLastedDoamin() ?: "" + this.contentsName = webcontentsName ?: "" + this.contentsType = if (url?.contains("book",true) == true) {"book"} + else if(url?.contains("new",true) == true) {"webtoon"} + else if(url?.contains("mana",true) == true) {"comics"} + else "web" + this.pageIndex = 0 + }) + } + } + it.jxInteface = { jxEvent -> + when (jxEvent) { + JxEvent.SCROLL_UP -> sendScrollDown(true) + JxEvent.SCROLL_DOWN -> sendScrollDown(false) + JxEvent.SWIPE_LEFT -> if (contentsType == "comics") sendViewerTouch("left") else actionNextEvent() + JxEvent.SWIPE_RIGHT -> if (contentsType == "comics") sendViewerTouch("right") else actionPrevEvent() + else -> {} + } + } + (activity as? NeoRssActivity)?.let { activity -> + it.decoViews.clear() + it.decoViews.add(activity.findViewById(R.id.current_address)) + it.decoViews.add(activity.findViewById(R.id.back)) + it.decoViews.add(activity.findViewById(R.id.reload)) + it.decoViews.add(activity.findViewById(R.id.dl_video)) + } + } + + binding.btnList.setOnClickListener { v -> + lastedUrl?.let { + Uri.parse(it).path?.let { + HistoryManager.getBookInfos(contentsType,processPageUrl(it), { + it?.let { + it.pages.sortBy { it.pathUrl } + Blog.LOGE("bind ing.btnList it >>> $it") + showList(it) + } + }) + } + } + } + + + binding.btnSetting.setOnClickListener { v -> + activity?.startActivity(Intent(requireContext(), Settings::class.java)) + } + binding.btnHistory.setOnClickListener { v -> + getHistory()?.let { + showHistory(it) + } + } + + binding.btnHome.setOnClickListener { v -> + binding.pagedLayer.visibility = GONE + goToHome() + } + + val nullCursor = PointerIcon.getSystemIcon(requireContext(), PointerIcon.TYPE_NULL) + binding.root.setPointerIcon(nullCursor) + return binding.root + } + + + + private fun getHistory() : List? { + try { + var realm = openRealm() + return realm.query().query("contentsType == $0", contentsType).find()?.copyFromRealm()?.reversed() + }catch (e: Exception){ + e.printStackTrace() + } + return null + } + + + + override fun onStart() { + super.onStart() + } + + override fun onResume() { + super.onResume() + val nullCursor = PointerIcon.getSystemIcon(requireContext(), PointerIcon.TYPE_NULL) + binding.root.setPointerIcon(nullCursor) + loadLastInfo() + } + + fun getLastinfo() : LastInfo? { + lastInfo = openRealm().query("contentsType == $0", contentsType).find().last()?.copyFromRealm() + Blog.LOGE("lastInfo >>> ${Gson().toJson(lastInfo)}") + return lastInfo + } + + fun loadLastInfo() { + var targetUrl = getLastedDoamin() + try { + applyReaderConfig() + getLastinfo()?.let { lastInfo -> + Blog.LOGE("lastInfo >>> ${lastInfo} path ::> ${lastInfo.pageUrl}") + HistoryManager.getBookPageInfo(contentsType, lastInfo.pageUrl) { + it?.let { + currentPage = it + Blog.LOGE("currentBooinfo :: ${currentPage} ${currentPage?.pathUrl}") + if ((currentPage?.pathUrl?.length ?: 0) > 1) { + HistoryManager.save( + historyItem = HistoryItem().putHistory( + currentPage, + currentPage?.pathUrl ?: getLastedDoamin() + ) + ) + currentPage?.pathUrl?.let { targetPath -> + if (targetPath.startsWith("http",true)) { + targetUrl = targetPath + } else { + targetUrl = getLastedDoamin() + targetPath + } + } + } + } + } + } ?: Blog.LOGE("lastInfo is Null") + } catch (e1: Exception) { + + } finally { + if ((lastedUrl?.contains(targetUrl) == true)) { + + } else { + contentsLoad(targetUrl) + } + } + } + + + fun onBookInfos(aInfos: ContentsCollection) { + Blog.LOGE("onBookInfos(aInfos: ${aInfos})") + activity?.runOnUiThread { + showList(aInfos) + } + } + + + + fun onBookInfos(jsonString: String) { + Blog.LOGE("onBookInfos", "jsonString >> ${jsonString}") + val realm = openRealm() + var infos: ContentsCollection? = null + realm.writeBlocking { + try { + var infosj: PageInfosJ? = null + infosj = Gson().fromJson(jsonString, PageInfosJ::class.java) + + HistoryManager.getBookInfos(contentsType,infosj.bookPageUrl!!) { + if (it != null) { + + infos = copyToRealm(it!!, UpdatePolicy.ALL) + for (item in infosj.pages) { + if (infos!!.hasItem(item.getRealm()) == false) { + infos!!.pages.add(item.getRealm()) + } + } + if (infos != null) { + infos = this.copyFromRealm(infos!!) + } + } else { + infos = infosj?.getR() + if (infos != null) { + infos = copyToRealm(infos!!, UpdatePolicy.ALL) + for (item in infosj.pages) { + infos?.pages?.add(item.getRealm()) + } + } + if (infos != null) { + infos = this.copyFromRealm(infos!!) + } + } + } + + } catch (e: Exception) { + e.printStackTrace() + } finally { + infos?.bookPageUrl?.let { + HistoryManager.getBookInfos(contentsType,it) { + it?.let { + Blog.LOGE(s(), "onBookInfos it >> ${it}") + activity?.runOnUiThread { showList(it) } + } + } + } + } + } + } + + fun onBookInfos(infosj: PageInfosJ) { + Blog.LOGE("onBookInfos", "jsonString >> ${infosj}") + val realm = openRealm() + var infos: ContentsCollection? = null + realm.writeBlocking { + try { + infosj.bookPageUrl?.let { + infosj.bookPageUrl = processPageUrl(it) + } + infosj.pages?.forEach {item -> item.pathUrl?.let { + item.pathUrl?.let { + item.pathUrl = processPageUrl(it) + } + item.bookPageUrl?.let { + item.bookPageUrl = processPageUrl(it) + } + }} + HistoryManager.getBookInfos(contentsType,processPageUrl(infosj.bookPageUrl!!)) { + if (it != null) { + + infos = copyToRealm(it!!, UpdatePolicy.ALL) + + for (item in infosj.pages) { + if (infos!!.hasItem(item.getRealm()) == false) { + infos!!.pages.add(item.getRealm()) + } + } + if (infos != null) { + infos = this.copyFromRealm(infos!!) + } + } else { + infos = infosj?.getR() + if (infos != null) { + infos = copyToRealm(infos!!, UpdatePolicy.ALL) + for (item in infosj.pages) { + infos?.pages?.add(item.getRealm()) + } + } + if (infos != null) { + infos = this.copyFromRealm(infos!!) + } + } + } + + } catch (e: Exception) { + e.printStackTrace() + } finally { + infos?.bookPageUrl?.let { + HistoryManager.getBookInfos(contentsType,it) { + it?.let { + Blog.LOGE(s(), "onBookInfos it >> ${it}") + activity?.runOnUiThread { showList(it) } + } + } + } + } + } + } + + private fun s() = "onBookInfos" + + + fun showHistory(infos: List) { + + val builderSingle: AlertDialog.Builder = AlertDialog.Builder(requireContext()) + builderSingle.setTitle("${currentTitle} : ${currentChapter} -> Select One ") + val arrayAdapter = + ArrayAdapter(requireContext(), android.R.layout.select_dialog_singlechoice) + for (item in infos) { + arrayAdapter.addAll(item.title) + } + + builderSingle.setNegativeButton( + "cancel", + DialogInterface.OnClickListener { dialog, which -> dialog.dismiss() }) + + builderSingle.setAdapter( + arrayAdapter, + DialogInterface.OnClickListener { dialog, which -> + val strName = arrayAdapter.getItem(which) + val item = infos.get(which) + val builderInner: AlertDialog.Builder = AlertDialog.Builder(requireContext()) + builderInner.setMessage(strName) + builderInner.setTitle("${infos.get(which).title}로 이동 고고!?") + builderInner.setPositiveButton( + "Ok", + DialogInterface.OnClickListener { dialog, which -> + contentsLoad(item.pageUrl) + dialog.dismiss() + }) + builderInner.setNeutralButton( + "삭제", + DialogInterface.OnClickListener { dialog, which -> + var realm = openRealm() + realm?.writeBlocking { + this.query().query("title == '${item.title}'").find() + ?.last()?.let { + this.delete(it) + } + } + dialog.dismiss() + }) + builderInner.setNegativeButton( + "취소", + DialogInterface.OnClickListener { dialog, which -> + dialog.dismiss() + }) + builderInner.show() + }) + var ddddd = builderSingle.create() + ddddd.setOnShowListener { d -> + (d as? AlertDialog)?.let { + it.listView?.setSelection(currentChapter) + } + } + + ddddd.show() + } + + fun showList(infos: ContentsCollection) { + Blog.LOGE("showList infos >>>>${infos}") + if (infos != null && infos.pages.size ?: 0 > 0) { + var items: ArrayList = arrayListOf() + for (item in infos.pages) { + items.add(item) + } + + items.sortBy { it.chapterID } + + DefaultList.showDefaultList( + requireContext(), + "현제는 ${currentTitle} - ${currentChapter} -> 다른화를 골라", + items, + currentChapter, + { position -> + return@showDefaultList items?.get(position)?.chapterTitle ?: "" + }, + { position -> + items?.get(position)?.let { moveTo(it) } + }, { state -> + if (state < 0) { + saveContinuation = true + moveToNext(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path) + } + }) + } + } + + val String.cleanTextContent: String + get() { + // strips off all non-ASCII characters + var text = this + text = text.replace("[^\\x00-\\x7F]".toRegex(), "") + + // erases all the ASCII control characters + text = text.replace("[\\p{Cntrl}&&[^\r\n\t]]".toRegex(), "") + + // removes non-printable characters from Unicode + text = text.replace("\\p{C}".toRegex(), "") + return text.trim() + } + + + + fun showToast(origin: String) { + activity?.runOnUiThread { + val toast = Toast(requireContext()) + toast.duration = Toast.LENGTH_SHORT + val biggerText = SpannableStringBuilder(origin) + biggerText.setSpan(RelativeSizeSpan(1.6f), 0, origin.length, 0) + val view: View = inflate(requireContext(), R.layout.simple_toast, null) + view.findViewById(R.id.text).text = biggerText + toast.view = view + toast.show() +// Toast.makeText( +// baseContext, +// biggerText, +// Toast.LENGTH_SHORT +// ).show() + } + } + + var delayed = 3500L + Math.abs(Random.nextLong().rem(9999L)) + var finishedUrl: String? = null + + private fun moveTo(item: ContentsPageInfo?) { + Blog.LOGE("item >>> ${item}") + item?.pathUrl?.let { newPath -> + contentsLoad(newPath) + } + } + + + fun moveToNext(pathUrl: String?) { + if (pathUrl != null && pathUrl.length > 6) { + HistoryManager.getNextPage(contentsType,pathUrl) { + if (it != null && (it.pathUrl?.length ?: 0) > 6) { + moveTo(it) + } else { + showToast("다음 편이 없다요.") + } + } + } + } + + fun moveToPrev(pathUrl: String?) { + if (pathUrl != null && pathUrl.length > 6) { + HistoryManager.getPrevPage(contentsType,pathUrl) { + if (it != null && (it.pathUrl?.length ?: 0) > 6) { + moveTo(it) + } else { + showToast("이전 편이 없다요.") + } + } + } + } + + fun contentsLoad(pathUrl: String) { + fun applyCurrentBook(contentsPageInfo: ContentsPageInfo) { + applyReaderConfig() + contentsPageInfo.contents?.let { onLoadedContents(it) } + contentsPageInfo.chapterTitle?.let { onFindTitle(it) } + if (contentsPageInfo.pathUrl?.startsWith("http") == true) { + binding.menuWeb.loadUrl(pathUrl) + } else { + binding.menuWeb.loadUrl(getLastedDoamin() + contentsPageInfo.pathUrl!!) + } + HistoryManager.save(HistoryItem().putHistory(contentsPageInfo, contentsPageInfo.pathUrl!!)) + } + + if (currentPage?.pathUrl.equals(pathUrl)) { + applyCurrentBook(currentPage!!) + } else { + HistoryManager.getBookPageInfo(contentsType,pathUrl) { + Blog.LOGE("contentsLoad :::: pathUrl >> ${pathUrl} , book >> ${it}") + if (it != null && it.isValidBook()) { + currentPage = it + applyCurrentBook(it) + } else if(lastInfo != null){ + binding.pagedLayer.visibility = GONE + binding.menuWeb.loadUrl(getLastedDoamin() + lastInfo!!.pageUrl) + } else { + binding.menuWeb.loadUrl(getLastedDoamin()) + } + } + } + } + + + fun actionNextEvent(fast: Boolean = false) { + if (binding.pagedLayer.isVisible && binding.pagedLayer.size() > 0 && (binding.pagedLayer.current() < binding.pagedLayer!!.size() - 1)) { + binding.pagedLayer.doNext(fast) + updateLastInfo(binding.pagedLayer!!) + } else { + moveToNext(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path) + } + } + + fun applyReaderConfig() { + var realm = HistoryManager.openRealm + realm.query().find().let { + if (it.isNotEmpty()) { + realm.copyFromRealm(it.first()).let { + activity?.runOnUiThread { + var typeface = + typesfacez.get(getIndex(typesfacez as PairArray, it.font ?: "")) + binding.pagedLayer.setTypeface(resources.getFont(typeface.second)) + val color = colorz.get(it.style ?: 0) + binding.pagedLayer.setColorStyle(color.second) + binding.pagedLayer.setTextSize(it.textSize?.toFloat() ?: 14f) + binding.pagedLayer.setLineSpacing(it.lineSpace?.toFloat() ?: 1f) + binding.pagedLayer.setLetterSpacing(it.letterSpace?.toFloat() ?: 1f) + binding.pagedLayer.setPadding( + it.padding ?: 1, + it.padding ?: 1, + it.padding ?: 1, + it.padding ?: 1 + ) + binding.pagedLayer.invalidate() + } + } + } + } + } + + fun actionPrevEvent(fast: Boolean = false) { + if (binding.pagedLayer.isVisible && binding.pagedLayer.size() > 0 && binding.pagedLayer.current() > 0) { + binding.pagedLayer.doPrev(fast) + updateLastInfo(binding.pagedLayer) + } else { + moveToPrev(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path) + } + } + + fun showAlert(alert: String) { + Log.i(TAG, "showAlert >> " + alert) + } + + + fun onStateChange(sessionState: GeckoSession.SessionState) { + + if (sessionState.last().uri.length > 10) { + lastedUrl?.let { + Uri.parse(it)?.let { uri -> + uri.path?.let { + HistoryManager.getBookPageInfo(contentsType, it) { + it?.let { + this@TokiFragment.currentPage = it + currentChapter = it?.chapterNum ?: 0 + HistoryManager.save(historyItem = HistoryItem().putHistory(it,lastedUrl!! )) + (currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)?.let { + HistoryManager.setBookPageInfo(contentsType,it,binding.pagedLayer.current()) + } + val realm = openRealm() + realm.writeBlocking { + var q = this.query("contentsType == $0", contentsType).find() + if(q.size > 0) { + q.last()?.let { + it.title = currentTitle + it.chapter = currentChapter + it.pageIndex = 0 + it.contentsType = currentPage!!.contentsType + this@TokiFragment.lastInfo = copyFromRealm(it) + } + } else { + LastInfo()?.let { + it.title = currentTitle + it.chapter = currentChapter + it.pageIndex = 0 + it.contentsType = currentPage!!.contentsType + it._id = if (lastedUrl?.contains("book",true) == true) {"book"} + else if(lastedUrl?.contains("new",true) == true) {"webtoon"} + else if(lastedUrl?.contains("mana",true) == true) {"comics"} + else "web" + copyToRealm(it, UpdatePolicy.ALL) + this@TokiFragment.lastInfo = it + } + } + } + + } + } + } + if (saveContinuation) { + binding.menuWeb.postDelayed({ + moveToNext( + currentPage?.pathUrl ?: lastedUrl?.toUri()?.path + ) + }, 10000) + } + } + } + } + } + + fun onLoadedContents(aContents: String) { + Blog.LOGE("onLoadedContents ") + binding.pagedLayer.let { view -> + view.post { + if (aContents.length > 10) { + var contents = aContents.replace("\\\"", "\"") + contents = (contents.replace("\\n", System.getProperty("line.separator"))) + view.mPagedTextViewInterface = this@TokiFragment + if (lastInfo != null && lastedUrl?.endsWith(lastInfo!!.pageUrl) == true) { + binding.progress.visibility = VISIBLE + binding.pagedLayer.postDelayed({ + binding.progress.visibility = GONE + }, 1000) + } + applyReaderConfig() + activity?.runOnUiThread { + view.text = contents + view.visibility = VISIBLE + binding.menuWeb.visibility = GONE + } + view.forceUpdateUI() + lastedUrl?.let { + Uri.parse(it)?.let { uri -> + uri.path?.let { + HistoryManager.getBookPageInfo(contentsType,it) { + it?.let { + this@TokiFragment.currentPage = it + currentChapter = it?.chapterNum ?: 0 + view.currentPage = it?.chapterNum ?: 0 + HistoryManager.save( + historyItem = HistoryItem().putHistory( + it, + lastedUrl!! + ) + ) + } + } + HistoryManager.getBooPageInfoContentsSave(contentsType,it, contents) + } + if (saveContinuation) { + binding.menuWeb.postDelayed({moveToNext(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)}, 10000) + + } + } + } + } + + } + } +// Log.i(TAG, "onLoadedContents >> " + aContents) + } + + var currentTitle: String = "" + var currentChapter: Int = 0 + + fun onFindTitle(contents: String) { + binding.textviewTitle.text = contents + binding.textviewTitle.setOnClickListener { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle("Title") + val input = EditText(requireContext()) + input.setText(lastedUrl ?: "") + input.inputType = InputType.TYPE_CLASS_TEXT + builder.setView(input) + builder.setPositiveButton( + "OK" + ) { dialog, which -> + var m_Text = input.text.toString() + contentsLoad(m_Text.trim()) + } + builder.setNegativeButton( + "Cancel" + ) { dialog, which -> dialog.cancel() } + builder.show() + } + var testRegex = """[^0-9]""".toRegex() + Blog.LOGI(TAG, "onFindTitle >> " + contents + " ::: ${testRegex.replace(contents, "")}") + if (contents.contains("-")) { + currentTitle = contents.split("-")[0] + try { + currentChapter = testRegex.replace(contents.split("-")[1], "").toInt() + } catch (e: Exception) { + currentChapter = 0 + } + } else if (testRegex.replace(contents, "").length > 0) { + currentChapter = testRegex.replace(contents, "").toInt() + currentTitle = contents.split(testRegex.replace(contents, ""))[0] + } else { + val dateFormat = "yyyyMMdd-HH" + val date = Date(currentTimeMillis()) + val simpleDateFormat = SimpleDateFormat(dateFormat) + currentTitle = simpleDateFormat.format(date) + } + + } + + fun onStartLoad() { + binding.progress.visibility = VISIBLE + } + + fun completePageLoad(lastInfo: LastInfo) { + saveLastInfo(lastInfo) + } + + fun saveLastInfo(lastInfo: LastInfo) { + val realm = openRealm() + if ((realm.query("contentsType == $0", contentsType)?.count()?.find() ?: 0) > 0) { + realm.writeBlocking { + this.query("contentsType == $0", contentsType)?.find()?.last()?.let { + it.pageUrl = lastInfo.pageUrl + it.title = currentTitle + it.chapter = currentChapter + it.pageIndex = lastInfo.pageIndex + it.contentsName = lastInfo.contentsName + it.displayOrientation = lastInfo.displayOrientation + it.contentsType = lastInfo.contentsType + this@TokiFragment.lastInfo = copyFromRealm(it) + Blog.LOGE("lastInfo >>>> ${this@TokiFragment.lastInfo}") + } + } + } else if ((lastInfo._id?.length ?: 0) > 3){ + realm.writeBlocking { + Blog.LOGE("lastInfo >>>> $lastInfo") + copyToRealm(lastInfo,UpdatePolicy.ALL) + } + } + } + + fun updateLastInfo(pagedTextLayout: PagedTextLayout) { + (currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)?.let { + HistoryManager.setBookPageInfo(contentsType,it,binding.pagedLayer.current()) + } + val configuration: Configuration = getResources().configuration + val realm = openRealm() + realm.writeBlocking { + var q = this.query("contentsType == $0", contentsType) + if(q.count().find() > 0) { + q.find().last()?.let { + it.displayOrientation = configuration.orientation + it.title = currentTitle + it.chapter = currentChapter + it.pageIndex = pagedTextLayout.current() + this@TokiFragment.lastInfo = copyFromRealm(it) + } + if (currentTitle.length > 0 && currentChapter > 0) { + this@TokiFragment.lastInfo?.makeHistoryItem()?.let { + copyToRealm(it, UpdatePolicy.ALL) + } + } + } else { + + LastInfo().let { + it.title = currentTitle + it.chapter = currentChapter + it.pageIndex = 0 + + it._id = if (lastedUrl?.contains("book",true) == true) {"book"} + else if(lastedUrl?.contains("new",true) == true) {"webtoon"} + else if(lastedUrl?.contains("mana",true) == true) {"comics"} + else "web" + it.contentsType = currentPage?.contentsType ?: it._id + copyToRealm(it,UpdatePolicy.ALL) + this@TokiFragment.lastInfo = it + } + if (currentTitle.length > 0 && currentChapter > 0) { + this@TokiFragment.lastInfo?.makeHistoryItem()?.let { + copyToRealm(it, UpdatePolicy.ALL) + } + } + } + } + val message: JSONObject = JSONObject() + try { + message.put("type", "scrollDown") + message.put("max", binding.pagedLayer.size()) + message.put("current",binding.pagedLayer.current()) + } catch (ex: JSONException) { + throw RuntimeException(ex) + } + mPort?.postMessage(message) + Blog.LOGD(log = "Successfully opened realm: ${realm.configuration.name}") + } + private fun sendViewerTouch(string: String) { + val message: JSONObject = JSONObject() + try { + message.put("type", "ViewerTouch") + message.put("area", string) + } catch (ex: JSONException) { + throw RuntimeException(ex) + } + Blog.LOGE(Gson().toJson(message)) + mPort?.postMessage(message) + } + fun sendScrollDown(isUp: Boolean) { + val message: JSONObject = JSONObject() + try { + message.put("type", "scrollDown") + message.put("isUpDown", if (isUp) +1 else -1) + } catch (ex: JSONException) { + throw RuntimeException(ex) + } + Blog.LOGE(Gson().toJson(message)) + mPort?.postMessage(message) + } + + private fun goToHome() { + binding.menuWeb.loadUrl(getLastedDoamin()) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/view/BWebview.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/view/BWebview.kt index 0d8b2bac..3b25fa4f 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/view/BWebview.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/view/BWebview.kt @@ -1,336 +1,336 @@ -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() - @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) - } - } -} \ No newline at end of file +//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() +// @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) +// } +// } +//} \ No newline at end of file diff --git a/app/src/main/res/layout/booktoki.xml b/app/src/main/res/layout/booktoki.xml index 6c1b5b4c..e60077b0 100644 --- a/app/src/main/res/layout/booktoki.xml +++ b/app/src/main/res/layout/booktoki.xml @@ -8,7 +8,7 @@ android:layout_height="match_parent" > -