This commit is contained in:
lunaticbum 2025-08-18 18:24:19 +09:00
parent 44ac92ca17
commit b34750099e
5 changed files with 290 additions and 15 deletions

View File

@ -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"]');
}

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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()
}
}