diff --git a/app/src/main/assets/extensions/my_extension/messaging.js b/app/src/main/assets/extensions/my_extension/messaging.js index a8b5996d..41dc2b02 100644 --- a/app/src/main/assets/extensions/my_extension/messaging.js +++ b/app/src/main/assets/extensions/my_extension/messaging.js @@ -385,10 +385,20 @@ const domainRules = [ { test: url => url.includes("torrentzota"), handler: handleToreentZota}, { test: url => url.includes("acrofan.com") && document.querySelectorAll('[id^="wide"]').length > 0, handler: handleAcrofan }, { test: url => url.includes("yna.co.kr") && document.querySelectorAll('[class^="wrapper"]').length > 0, handler: handleYna }, + { test: url => url.includes("yt1d.com/"), handler: ytDown }, { test: url => url.includes("clien") && document.querySelectorAll('[class^="content_view"]').length > 0, handler: handleClien }, { test: url => url.includes("toki") && (document.querySelectorAll('[id^="id_mbv"]').length > 0 || document.querySelectorAll('[class^="basic-banner"]').length > 0), handler: handleToki }, ]; +function ytDown() { + navigator.clipboard.readText().then(function(text) { + document.querySelector("#txt-url").value = text; + }).catch(function(err) { + alert('클립보드 읽기에 실패했습니다: ' + err); + }); + +} + function handleCommon() { // 공통 광고 제거 if (document.querySelector(".top_google_ad_space")) document.querySelector(".top_google_ad_space").remove(); @@ -752,6 +762,10 @@ function handleDcinside() { document.querySelectorAll( '[id^="view_btn_area"], [class^="trend-rank"], [class^="view-btm-con"], [class^="md-tit-box"], [class^="gall-detail-lst"], [class^="outside-search-box"], [class^="footer ftlong"], [class^="adv-group"], li[style^="cursor:default;"], [id^="div_adnmore_area"]' ).forEach(e => e.remove()); + document.querySelectorAll('div[class^="imgwrap"]').forEach(function (e) { + try {e.style.backgroundColor = 'red';} catch (e) {} + }) + mainContentsEl = document.querySelector('div[class="container"]'); } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt index 8ed9daf7..37d5e65a 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt @@ -45,6 +45,7 @@ import android.view.KeyEvent.KEYCODE_BUTTON_Y import android.view.KeyEvent.KEYCODE_DPAD_DOWN import android.view.KeyEvent.KEYCODE_DPAD_UP import android.view.MotionEvent +import android.view.PointerIcon import android.view.View import android.view.WindowInsets import android.view.WindowManager @@ -77,6 +78,7 @@ import bums.lunatic.launcher.helpers.Constants.Companion.widgetHostId import bums.lunatic.launcher.helpers.HeadsetActionButtonReceiver import bums.lunatic.launcher.helpers.PrefHelper.putString import bums.lunatic.launcher.helpers.PrefLong +import bums.lunatic.launcher.home.GeckoWeb import bums.lunatic.launcher.home.RssHome import bums.lunatic.launcher.home.RssViewBuilder import bums.lunatic.launcher.model.RssData @@ -562,7 +564,8 @@ open class LauncherActivity : CommonActivity() { if (intent?.action == Intent.ACTION_WEB_SEARCH) { openWithIntent(intent) } - + val nullCursor = PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL) + binding.root.setPointerIcon(nullCursor) binding.share.setOnClickListener { if (binding.currentAddress.text.length > 5) { val sendIntent: Intent = Intent().apply { 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 08579389..79c8af98 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt @@ -1,7 +1,10 @@ package bums.lunatic.launcher.home import CustomVideoNodeRenderer +import android.app.Dialog import android.app.DownloadManager +import android.content.ClipData +import android.content.ClipboardManager import android.content.Context import android.content.DialogInterface import android.content.Intent @@ -9,6 +12,8 @@ import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.net.Uri import android.os.Environment +import android.os.Handler +import android.os.Looper import android.util.AttributeSet import android.util.Base64 import android.util.Log @@ -23,6 +28,7 @@ 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.PointerIcon import android.view.View import android.widget.CheckBox import android.widget.EditText @@ -37,6 +43,7 @@ import bums.lunatic.launcher.R import bums.lunatic.launcher.tokiz.data.model.PortMessage import bums.lunatic.launcher.tokiz.view.BWebview import bums.lunatic.launcher.utils.Blog +import bums.lunatic.launcher.utils.CommonUtils import bums.lunatic.launcher.workers.WorkersDb import com.google.gson.Gson import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter @@ -46,6 +53,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kr.lunaticbum.utils.service.ServiceUtil.getSystemService +import okhttp3.OkHttpClient +import okhttp3.Request import org.json.JSONException import org.json.JSONObject import org.jsoup.Jsoup @@ -54,13 +64,13 @@ 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.GeckoSession.PermissionDelegate 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 org.mozilla.geckoview.WebResponse import java.io.File import java.io.FileOutputStream import java.io.InputStream @@ -94,6 +104,7 @@ class GeckoWeb : BWebview { val session: GeckoSession = GeckoSession() session.open(it) this.setSession(session) + session.contentDelegate = contentDelegate session.progressDelegate = progressDelegate session.navigationDelegate = navigationDelegate @@ -437,7 +448,7 @@ class GeckoWeb : BWebview { } } } - + var dialog : Dialog? = null fun getFilterF() = String(java.util.Base64.getMimeDecoder().decode("aHR0cHM6Ly9pamF2dG9ycmVudC5jb20=".toByteArray())) var currentTitle = "" val contentDelegate = object : GeckoSession.ContentDelegate { @@ -463,6 +474,62 @@ class GeckoWeb : BWebview { super.onFirstContentfulPaint(session) } + fun replaceDcUrl(origin: String): String { + var result = origin + for (i in 0..19) { + result = result.replace(String.format("dcimg%d.", i), "dcimg2.") + } + return result + } + + 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() + } + + 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("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) + } override fun onContextMenu( session: GeckoSession, @@ -470,12 +537,37 @@ class GeckoWeb : BWebview { screenY: Int, element: GeckoSession.ContentDelegate.ContextElement ) { - if (element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE) { - Uri.parse(element.srcUri)?.let { - showImageDownloadDialog(context,it) + + if (element.baseUri?.contains("youtube") == true) { + copyToClipboard(lastedUrl) + loadUrl("https://ko.savefrom.net/227lt/#url=${lastedUrl}") +// copyToClipboard(lastedUrl) +// Dialog(context)?.let { dialog -> +// val popupWebView = GeckoWeb(context).apply { +// loadUrl(lastedUrl!!.replace("https://","https://ss")) +// this.dialog = dialog +// } +// dialog.setCanceledOnTouchOutside(true) +// dialog.setContentView(popupWebView) +// dialog.show() +// } + } else { + Blog.LOGE("onContextMenu:: x = ${x}, y = ${y} , element = ${Gson().toJson(element)}") + if (element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE) { + element.srcUri?.let { + (if (it.contains("dcimg")){ replaceDcUrl(it) } else { element.srcUri })?.let { + + CommonUtils.downloadFileWithOkHttp(context, Uri.parse(element.baseUri), it) + } + } + }else if (element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_VIDEO) { + element.srcUri?.let { + (if (it.contains("dcimg")){ replaceDcUrl(it) } else { element.srcUri })?.let { + CommonUtils.downloadFileWithOkHttp(context, Uri.parse(element.baseUri), it) + } + } } } - super.onContextMenu(session, screenX, screenY, element) } } @@ -557,8 +649,18 @@ class GeckoWeb : BWebview { Blog.LOGE("GeckoView", "onNewSession: $session from WebExtension") Uri.parse(uri)?.let { - if(it.host?.let { it1 -> lastedUrl?.contains(it1, true) } == true) { + 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) + ) { loadUrl(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}") @@ -607,7 +709,8 @@ class GeckoWeb : BWebview { // 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 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 2be3fbbb..37a3925d 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt @@ -259,7 +259,7 @@ internal class RssHome : Fragment() { } RssDataType.REDDIT -> { - openReddit(rss.originPage()) + openGecko(rss) } RssDataType.DOTAX -> { @@ -267,7 +267,7 @@ internal class RssHome : Fragment() { } RssDataType.YOUTUBE -> { - openYouTube(rss.originPage()) + openGecko(rss) } RssDataType.CLIEN -> { @@ -415,7 +415,8 @@ internal class RssHome : Fragment() { } } else if (rssData?.category()?.equals(RssDataType.PRIVATE) == true){ rssData?.let { - binding.geckoWeb.privateMode = true + currentRss = it + synchronized(lasted) { if (lasted.isNotEmpty()) { lasted.removeAll { target -> target.originPage.equals(it.originPage) } @@ -423,6 +424,11 @@ internal class RssHome : Fragment() { } appendReadCount(it, 1, false) Blog.LOGE("removeFirst >>> ${Gson().toJson(it)}") + binding.layoutRssSummary.title.setOnLongClickListener { + currentRss?.originPage?.let { binding.geckoWeb.loadUrl(it)} + binding.layoutRssSummary.root.visibility = View.GONE + true + } binding.layoutRssSummary.title.tag = it binding.layoutRssSummary.root.visibility = View.VISIBLE binding.layoutRssSummary.scrollView.scrollTo(0,0) @@ -607,12 +613,12 @@ internal class RssHome : Fragment() { } } val nullCursor = PointerIcon.getSystemIcon(requireContext(), PointerIcon.TYPE_NULL) + binding.root.setPointerIcon(nullCursor) binding.search.setOnClickListener { searchKeyword() } binding.search.setOnLongClickListener{ ask() true } - binding.root.setPointerIcon(nullCursor) binding.geckoWeb.decoViews.add(binding.hide) binding.geckoWeb.decoViews.add(binding.vote) binding.geckoWeb.decoViews.add(binding.progressBar) diff --git a/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt b/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt index c3a5566f..e639c70b 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt @@ -1,9 +1,17 @@ package bums.lunatic.launcher.utils import android.content.Context +import android.net.Uri import android.os.Build - - +import java.io.FileOutputStream +import android.webkit.MimeTypeMap +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.File object CommonUtils { fun dpToPx(context: Context, dp: Float): Int { @@ -18,4 +26,145 @@ object CommonUtils { fun hasLollipop(): Boolean { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP } + + fun getFileTypeBySignature(file: File): String? { + val buffer = ByteArray(12) // 동영상 포맷 확인용으로 충분히 크게 버퍼 확장 + file.inputStream().use { it.read(buffer, 0, buffer.size) } + + // GIF + if (buffer.sliceArray(0..5).contentEquals("GIF87a".toByteArray()) || + buffer.sliceArray(0..5).contentEquals("GIF89a".toByteArray())) { + return "gif" + } + // JPEG + if (buffer[0] == 0xFF.toByte() && buffer[1] == 0xD8.toByte() && buffer[2] == 0xFF.toByte()) { + return "jpg" + } + // PNG + if (buffer.sliceArray(0..7).contentEquals(byteArrayOf(0x89.toByte(),'P'.toByte(),'N'.toByte(),'G'.toByte(),0x0D,0x0A,0x1A,0x0A))) { + return "png" + } + + // MP4 (ftyp... 브랜드 체크, 보통 4~8바이트 확인) + // "ftyp"는 4바이트 offset 4 ~ 7에 위치 + if (buffer.size >= 12 && buffer.sliceArray(4..7).contentEquals("ftyp".toByteArray())) { + return "mp4" + } + + // AVI (큰 헤더 "RIFF....AVI ") + if (buffer.size >= 12 && + buffer.sliceArray(0..3).contentEquals("RIFF".toByteArray()) && + buffer.sliceArray(8..11).contentEquals("AVI ".toByteArray())) { + return "avi" + } + + // MKV (Matroska) - EBML 헤더: 0x1A 0x45 0xDF 0xA3 + if (buffer.size >= 4 && + buffer[0] == 0x1A.toByte() && buffer[1] == 0x45.toByte() && + buffer[2] == 0xDF.toByte() && buffer[3] == 0xA3.toByte()) { + return "mkv" + } + + return null + } + + + fun downloadFileWithOkHttp(context: Context,refferer : Uri, fileUrl: String) { + android.app.AlertDialog.Builder(context) + .setTitle("파일 다운로드") + .setMessage("해당 파일을 다운로드하시겠습니까?") + .setPositiveButton("확인") { _, _ -> + CoroutineScope(Dispatchers.IO).launch { + val client = OkHttpClient() + val request = Request.Builder().url(fileUrl).addHeader("Referer", refferer.toString()) + .addHeader("User-Agent", "Mozilla/5.0").build() + + val dir = File("/storage/emulated/0/bums_ob/BUM'S PACED /scraped/md") + if (!dir.exists()) { + dir.mkdirs() + } else { + dir.listFiles().forEach { Blog.LOGE("child -> ${it.absolutePath}") } + } + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + withContext(Dispatchers.Main) { + android.widget.Toast.makeText( + context, + "다운로드 실패!\n${fileUrl}\n${response.message()}", + android.widget.Toast.LENGTH_SHORT + ).show() + } + } else { + + + // Content-Type에서 확장자 추출 + val contentType = response.header("Content-Type") + Blog.LOGE("downloadFileWithOkHttp contentType $contentType") + var extension = contentType?.let { + MimeTypeMap.getSingleton().getExtensionFromMimeType(it) + } + + // 확장자 없으면 URL에서 추출하거나 기본값 "dat" 사용 + if (extension.isNullOrBlank()) { + extension = MimeTypeMap.getFileExtensionFromUrl(fileUrl) + if (extension.isNullOrBlank()) { + extension = "dat" + } + } + Blog.LOGE("downloadFileWithOkHttp extension $extension") +// if (extension.equals("bin")){ +//// extension = "jpg" +// } + val fileName = + "${ + refferer.host?.replace( + ",", + "_" + ) + }_${System.currentTimeMillis()}.$extension" + val file = File(dir, fileName) + + response.body()?.byteStream()?.use { input -> + FileOutputStream(file).use { output -> + input.copyTo(output) + } + } + + + val resultFile = if (extension in listOf("bin", "dat")) { + val realExt = getFileTypeBySignature(file) + when { + realExt.isNullOrBlank() -> file + realExt == extension -> file + else -> { + val newFile = File(dir, "${ + refferer.host?.replace( + ",", + "_" + ) + }_${System.currentTimeMillis()}.$extension") + if (file.renameTo(newFile)) newFile else file + } + } + } else { + file + } + + Blog.LOGE("downloadFileWithOkHttp File saved: ${resultFile.absolutePath}") + + withContext(Dispatchers.Main) { + android.widget.Toast.makeText( + context, + "다운로드 완료!\n${resultFile.absolutePath}", + android.widget.Toast.LENGTH_SHORT + ).show() + } + } + } + } + } + .setNegativeButton("취소", null) + .show() + + } } \ No newline at end of file