This commit is contained in:
lunaticbum 2026-03-24 15:53:29 +09:00
parent 112012ce0c
commit 68c16339ea
8 changed files with 298 additions and 60 deletions

View File

@ -81,6 +81,19 @@ port.onMessage.addListener(response => {
} }
} }
} break;
case "PAUSE_YT": // 5초 뒤로
{
if (document.location.href.search("youtube") > -1) {
let btn =
document.querySelector('button[aria-label="동영상 일시중지"]') ;
if (btn) {
btn.click();
return;
}
}
} break; } break;
case "SEEK_PREV": // 5초 뒤로 case "SEEK_PREV": // 5초 뒤로
{ {
@ -594,6 +607,36 @@ var mainContentsEl = null;
let lastState = null; // 이전 상태 저장 (-1, 0, 1) let lastState = null; // 이전 상태 저장 (-1, 0, 1)
let throttleTimer = null; let throttleTimer = null;
function sendCookiesToNative() {
try {
const cookies = document.cookie;
// 쿠키가 존재할 때만 전송
if (cookies && cookies.length > 0) {
const netscapeCookies = '# Netscape HTTP Cookie File\\n' +
document.cookie.split('; ')
.map(c => {
const eqIdx = c.indexOf('=');
const name = c.substring(0, eqIdx).trim();
const value = decodeURIComponent(c.substring(eqIdx + 1));
return `.youtube.com\\tTRUE\\t/\\tFALSE\\t0\\t${name}\\t${value}`;
})
.filter(c => c.includes('youtube') || c.includes('google'))
.join('\\n');
sendMessage({
type: "COOKIES_REPORT",
value: netscapeCookies,
url: location.href
});
console.log("Cookies sent to native.");
}
} catch (e) {
// 예외 무시
}
}
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const currentUrl = location.href; const currentUrl = location.href;
@ -650,6 +693,13 @@ document.addEventListener('DOMContentLoaded', function () {
throttleTimer = null; throttleTimer = null;
}, 150); // 0.15초 간격으로 체크 (사용성에 따라 조절 가능) }, 150); // 0.15초 간격으로 체크 (사용성에 따라 조절 가능)
}; };
if (document.readyState === 'complete') {
sendCookiesToNative();
} else {
// 2. 아직 로딩 중이라면 window.onload(모든 리소스 로드 완료) 시점에 실행
window.addEventListener('load', sendCookiesToNative);
}
}) })
const keywords = ["youtube", "mojeek"]; const keywords = ["youtube", "mojeek"];

View File

@ -6,6 +6,7 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.app.WallpaperManager
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@ -47,14 +48,66 @@ import java.io.File
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import android.bluetooth.BluetoothClass import android.bluetooth.BluetoothClass
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.media.AudioManager import android.media.AudioManager
import android.os.SystemClock import android.os.SystemClock
import android.os.VibrationEffect import android.os.VibrationEffect
import android.os.Vibrator import android.os.Vibrator
import android.view.KeyEvent import android.view.KeyEvent
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
import androidx.core.content.ContentProviderCompat.requireContext
import androidx.work.workDataOf
import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieString
import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieUrlString
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
class WallpaperAutoChangeWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val folderPath = inputData.getString("FOLDER_PATH") ?: return Result.failure()
val folder = File(folderPath)
// 1. 이미지 파일 목록 가져오기
val images = folder.listFiles { file ->
file.isFile && (file.extension.equals("jpg", true) || file.extension.equals("png", true) || file.extension.equals("jpeg", true))
}
if (images.isNullOrEmpty()) return Result.failure()
// 2. 랜덤 이미지 선택 및 비트맵 로드
val randomImage = images.random()
val options = BitmapFactory.Options().apply { inJustDecodeBounds = false }
val originalBitmap = BitmapFactory.decodeFile(randomImage.absolutePath, options) ?: return Result.failure()
return try {
val wm = WallpaperManager.getInstance(context)
val targetWidth = wm.desiredMinimumWidth
val targetHeight = wm.desiredMinimumHeight
// 3. 비율 유지하며 Center Crop 처리
val finalBitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(finalBitmap)
val scale = Math.max(targetWidth.toFloat() / originalBitmap.width, targetHeight.toFloat() / originalBitmap.height)
val scaledWidth = scale * originalBitmap.width
val scaledHeight = scale * originalBitmap.height
val left = (targetWidth - scaledWidth) / 2f
val top = (targetHeight - scaledHeight) / 2f
canvas.drawBitmap(originalBitmap, null, RectF(left, top, left + scaledWidth, top + scaledHeight), Paint(Paint.FILTER_BITMAP_FLAG))
// 4. 적용
wm.setBitmap(finalBitmap, null, true, WallpaperManager.FLAG_SYSTEM)
Result.success()
} catch (e: Exception) {
e.printStackTrace()
Result.retry()
}
}
}
class AggregatedSystemWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { class AggregatedSystemWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
// 통합 시스템 작업 실행 // 통합 시스템 작업 실행
@ -95,7 +148,7 @@ class ForeGroundService : Service() {
val ACTION_SIT_DOWN = "ACTION_SIT_DOWN" val ACTION_SIT_DOWN = "ACTION_SIT_DOWN"
val ACTION_COPY_COMPLETE = "ACTION_COPY_COMPLETE" val ACTION_COPY_COMPLETE = "ACTION_COPY_COMPLETE"
val targetUrls = arrayListOf<String>() val targetUrls = arrayListOf<Pair<String, Boolean>>()
} }
enum class BLUETOOTH_STATE(val statestr: String) { enum class BLUETOOTH_STATE(val statestr: String) {
@ -106,7 +159,7 @@ class ForeGroundService : Service() {
var blueToothAdapter:BluetoothAdapter? = null var blueToothAdapter:BluetoothAdapter? = null
private var mWorkManager: WorkManager? = null private var mWorkManager: WorkManager? = null
private val serviceScope = CoroutineScope(Dispatchers.Default)
val NOTIF_ID = 830721 val NOTIF_ID = 830721
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -114,9 +167,31 @@ class ForeGroundService : Service() {
val filter = IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED) val filter = IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED)
registerReceiver(bluetoothreceiver, filter) registerReceiver(bluetoothreceiver, filter)
refreshFeeds() refreshFeeds()
startWallpaperTimer()
startForeGround(vibrator = true) startForeGround(vibrator = true)
} }
private fun startWallpaperTimer() {
serviceScope.launch {
while (true) {
// 실행하고자 하는 폴더 경로 (본인 경로로 수정)
val myFolderPath = File(File(getExternalFilesDir(null), "completed_torrents"),"이미지").absolutePath
val workRequest = OneTimeWorkRequestBuilder<WallpaperAutoChangeWorker>()
.setInputData(workDataOf("FOLDER_PATH" to myFolderPath))
.build()
workmanager()?.enqueueUniqueWork(
"SingleWallpaperChange",
ExistingWorkPolicy.REPLACE,
workRequest
)
delay(TimeUnit.MINUTES.toMillis(10)) // 10분 대기
}
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Blog.LOGE("intent?.action >> ${intent?.action}") Blog.LOGE("intent?.action >> ${intent?.action}")
when(intent?.action) { when(intent?.action) {
@ -128,7 +203,7 @@ class ForeGroundService : Service() {
ACTION_VIDEO_DOWNLOAD -> { ACTION_VIDEO_DOWNLOAD -> {
intent?.getStringExtra(EXTRA_TARGET_URL)?.let { intent?.getStringExtra(EXTRA_TARGET_URL)?.let {
Uri.parse(it)?.let { Uri.parse(it)?.let {
addToTargetYtubeUrl(it.toString()) addToTargetYtubeUrl(it.toString(), intent.getBooleanExtra("forMusic", false))
} }
} }
} }
@ -159,51 +234,76 @@ class ForeGroundService : Service() {
return START_STICKY return START_STICKY
} }
fun addToTargetYtubeUrl(url : String) { fun addToTargetYtubeUrl(url : String? = null, forMusic: Boolean = false) {
targetUrls.add(url) url?.let { url ->
if((targetUrls?.size ?: 0) > 0) { if (url.length > 0) {
downloadVideo(targetUrls?.firstOrNull()) targetUrls.add(url to forMusic)
} }
} }
targetUrls.removeFirstOrNull()?.let {
downloadVideo(it.first,it.second)
}
}
var currentProcessId : String? = null var currentProcessId : String? = null
set(value) { set(value) {
field = value field = value
if (value == null) { if (value == null) {
startForeGround(max= 0, progress = 0, vibrator = false) // startForeGround(max= 0, progress = 0, vibrator = false)
} }
} }
fun downloadVideo(url: String?) { fun downloadVideo(url: String?, forMusic: Boolean = false) {
url?.let { url?.let {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
// val youtubeDLDir = File(
// Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
// "youtubedl-android"
// )
val youtubeDLDir = File(getExternalFilesDir(null), "completed_torrents") val youtubeDLDir = File(getExternalFilesDir(null), "completed_torrents")
val command = YoutubeDLRequest(url) val command = YoutubeDLRequest(url).apply {
command.addOption("-o", youtubeDLDir.getAbsolutePath() + "/%(title)s.%(ext)s"); addOption("-q") // 로그 최소화
addOption("--no-warnings") // 경고 숨김
val cookieFile = File(this@ForeGroundService.cacheDir, "cookies.txt")
addOption("--cookies", "${cookieFile.absolutePath}")
// 출력 경로
addOption("-o", "${youtubeDLDir.absolutePath}/%(title)s.%(ext)s")
if (forMusic) {
// 음악 전용 옵션
addOption("-x") // 오디오 추출
addOption("--audio-format", "mp3")
addOption("--extractor-args", "youtube:player_client=web_music,android")
addOption("-f", "bestaudio[ext=m4a]/bestaudio/best")
} else {
// 일반 영상용
addOption("-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best")
}
// 안정성 향상 옵션 (모두에 적용)
addOption("--no-mtime") // 파일 수정시간 안 건드림
addOption("--restrict-filenames") // 특수문자 제한
}
currentProcessId = UUID.randomUUID().toString() currentProcessId = UUID.randomUUID().toString()
YoutubeDL.getInstance() YoutubeDL.getInstance()
.execute(command, currentProcessId) { progress, est, str -> .execute(command, currentProcessId) { progress, est, str ->
startForeGround(100, progress.toInt(),str, false) startForeGround(100, progress.toInt(), str, false)
if (progress >= 100) { if (progress >= 100) {
targetUrls.remove(url)
currentProcessId = null currentProcessId = null
if((targetUrls?.size ?: 0) > 0) { if ((targetUrls?.size ?: 0) > 0) {
downloadVideo(targetUrls?.firstOrNull()) addToTargetYtubeUrl()
} }
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Blog.LOGE("Download Error", e)
currentProcessId = null currentProcessId = null
if((targetUrls?.size ?: 0) > 0) { if ((targetUrls?.size ?: 0) > 0) {
downloadVideo(targetUrls?.firstOrNull()) addToTargetYtubeUrl()
} }
} }
} }

View File

@ -737,11 +737,11 @@ class CompletedFilesFragment : Fragment() {
private fun organizeRootFiles() { private fun organizeRootFiles() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
// 1. 루트 폴더의 파일 목록 가져오기 // 1. 루트 폴더의 파일 목록 가져오기
val filesInRoot = rootDir.listFiles()?.filter { it.isFile } ?: emptyList() val filesInRoot = if (selectedFiles.isEmpty()) rootDir.listFiles()?.filter { it.isFile } else selectedFiles
var movedCount = 0 var movedCount = 0
// 2. 널브러진 파일들을 확장자별 폴더로 이동 // 2. 널브러진 파일들을 확장자별 폴더로 이동
filesInRoot.forEach { file -> filesInRoot?.forEach { file ->
val ext = file.extension.lowercase() val ext = file.extension.lowercase()
val folderName = when { val folderName = when {
extImages.contains(ext) -> "이미지" extImages.contains(ext) -> "이미지"

View File

@ -83,6 +83,7 @@ open class GeckoWeb @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null context: Context, attrs: AttributeSet? = null
) : GeckoView(context, attrs) { ) : GeckoView(context, attrs) {
// --- 1. Properties & Initialization --- // --- 1. Properties & Initialization ---
// 1. 세션 상태를 저장할 SharedPreferences 키 // 1. 세션 상태를 저장할 SharedPreferences 키
@ -193,6 +194,8 @@ open class GeckoWeb @JvmOverloads constructor(
companion object { companion object {
var currentRetryCount = 0 var currentRetryCount = 0
var currentCookieString = ""
var currentCookieUrlString = ""
} }
@ -201,7 +204,7 @@ open class GeckoWeb @JvmOverloads constructor(
override fun setVisibility(visibility: Int) { override fun setVisibility(visibility: Int) {
super.setVisibility(visibility) super.setVisibility(visibility)
decoViews.filter { it.id > -1 && it.id != R.id.btn_dl_video }.forEach { it.visibility = visibility } decoViews.filter { it.id > -1 }.forEach { it.visibility = visibility }
} }
open fun loadUrl(url: String, param: String? = null) { open fun loadUrl(url: String, param: String? = null) {
@ -226,58 +229,110 @@ open class GeckoWeb @JvmOverloads constructor(
currentRetryCount = 0 currentRetryCount = 0
} }
fun checkIfDownloadable(url: String) { var lastCheckUrlS = hashSetOf<String>()
// UI 초기화
post { fun cleanYoutubeUrl(url: String): String {
return try {
val uri = Uri.parse(url)
val videoId = uri.getQueryParameter("v")
if (videoId != null) {
// v=만 남기고 나머지 파라미터 제거
"https://www.youtube.com/watch?v=$videoId"
} else {
// Shorts나 다른 형식
url
}
} catch (e: Exception) {
url // 실패시 원본 반환
}
}
fun checkIfDownloadable(firstUrl: String, forMusic: Boolean = false) {
if (firstUrl.startsWith("about")) {
return
}
val cleanUrl = cleanYoutubeUrl(firstUrl) // ← 추가!
decoViews.firstOrNull { it.id == R.id.btn_dl_video }?.let { decoViews.firstOrNull { it.id == R.id.btn_dl_video }?.let {
it.setOnClickListener {} it.setOnClickListener {}
it.visibility = GONE it.visibility = GONE
} }
}
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
val request = YoutubeDLRequest(url) val request = YoutubeDLRequest(cleanUrl).apply {
// mGKCookie?.COOKIES?.let { cookieStr -> // 1. yt-dlp 업데이트 강제 (가장 중요!)
// val cookieFile = File(context.filesDir, "cookies.txt") addOption("-q") // 로그 최소화
// val cookies = cookieStr.split(";").mapNotNull { addOption("--no-warnings") // 경고 숨김
// val p = it.trim().split("=", limit = 2)
// if (p.size == 2) p[0] to p[1] else null val cookieFile = File(context.cacheDir, "cookies.txt")
// }.toMap() addOption("--cookies", "${cookieFile.absolutePath}")
// val expires = (System.currentTimeMillis() / 1000) + 3600 * 24 * 7
// if (forMusic) {
// val content = buildString { addOption("-x") // 오디오 추출
// appendLine("# Netscape HTTP Cookie File") addOption("--audio-format", "mp3")
// cookies.forEach { (k, v) -> // YouTube Music 전용 extractor arg 추가
// appendLine(".${url.toUri().host}\tTRUE\t/\tTRUE\t$expires\t$k\t$v") addOption("--extractor-args", "youtube:player_client=web_music,android")
// } // 음악 스트림 우선 선택
// } addOption("-f", "bestaudio[ext=m4a]/bestaudio/best")
// cookieFile.writeText(content) } else {
// request.addOption("--cookies", cookieFile.absolutePath) // 일반 영상용 기본 포맷
// } addOption("-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best")
}
// 디버깅용 (테스트 후 제거)
// addOption("--verbose")
}
val videoInfo = YoutubeDL.getInstance().getInfo(request) val videoInfo = YoutubeDL.getInstance().getInfo(request)
if (videoInfo != null && !videoInfo.title.isNullOrEmpty()) { if (videoInfo != null && !videoInfo.title.isNullOrEmpty()) {
post { CoroutineScope(Dispatchers.Main).launch {
decoViews.firstOrNull { it.id == R.id.btn_dl_video }?.let { view -> decoViews.firstOrNull { it.id == R.id.btn_dl_video }?.let { view ->
view.setOnClickListener { videoDownload(url) } view.setOnClickListener { videoDownload(cleanUrl, forMusic) }
view.visibility = VISIBLE view.visibility = VISIBLE
} }
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
// Log.e("GeckoWeb", "Download Check Error", e) Blog.LOGE("Download Check Error", e)
// val msg = e.message ?: ""
// if (msg.contains("parse video information") && !lastCheckUrlS.contains(cleanUrl)) {
// CoroutineScope(Dispatchers.Main).launch {
// CoroutineScope(Dispatchers.Main).launch {
// decoViews.firstOrNull { it.id == R.id.btn_dl_video }?.let { view ->
// view.setOnClickListener {
// AlertDialog.Builder(context)
// .setTitle("음악 전용으로 재시도?")
// .setMessage("parse 에러 발생. forMusic=true로 재시도합니다.")
// .setPositiveButton("오키") { _, _ ->
// checkIfDownloadable(cleanUrl, true)
// }
// .setNegativeButton("취소", null)
// .show()
// lastCheckUrlS.add(cleanUrl)
// }
// }
// }
//
// }
// }
} }
} }
} }
fun videoDownload(videoUrl: String) { fun videoDownload(videoUrl: String,forMusic : Boolean = false) {
val intent = Intent(context, ForeGroundService::class.java).apply { val intent = Intent(context, ForeGroundService::class.java).apply {
action = ACTION_VIDEO_DOWNLOAD action = ACTION_VIDEO_DOWNLOAD
putExtra(EXTRA_TARGET_URL, videoUrl) putExtra(EXTRA_TARGET_URL, videoUrl)
putExtra("forMusic",forMusic)
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent)
else context.startService(intent) else context.startService(intent)
pauseYT()
} }
// --- 3. Delegates (선언 위치를 위로 올려 초기화 보장) --- // --- 3. Delegates (선언 위치를 위로 올려 초기화 보장) ---
@ -322,6 +377,8 @@ open class GeckoWeb @JvmOverloads constructor(
// [Navigation Delegate] // [Navigation Delegate]
private val navigationDelegate = object : GeckoSession.NavigationDelegate { private val navigationDelegate = object : GeckoSession.NavigationDelegate {
override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? { override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? {
Uri.parse(uri)?.let { Uri.parse(uri)?.let {
if (it.host?.let { h -> lastedUrl?.contains(h, true) } == true || if (it.host?.let { h -> lastedUrl?.contains(h, true) } == true ||
@ -378,6 +435,7 @@ open class GeckoWeb @JvmOverloads constructor(
} }
override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) { override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
Blog.LOGE("onCanGoBack $canGoBack ${session}")
this@GeckoWeb.canGoBack = canGoBack this@GeckoWeb.canGoBack = canGoBack
} }
} }
@ -411,17 +469,21 @@ open class GeckoWeb @JvmOverloads constructor(
saveCurrentSessionState() saveCurrentSessionState()
} }
onPageStartCallback?.invoke(url) onPageStartCallback?.invoke(url)
// Blog.LOGE("onPageStart $url ${session}")
checkIfDownloadable(url)
} }
override fun onPageStop(session: GeckoSession, success: Boolean) { override fun onPageStop(session: GeckoSession, success: Boolean) {
onPageStopCallback?.invoke(success) onPageStopCallback?.invoke(success)
if (success) { if (success) {
saveCurrentSessionState() saveCurrentSessionState()
} }
Blog.LOGE("onPageStop $success ${session}")
} }
override fun onSessionStateChange(session: GeckoSession, sessionState: GeckoSession.SessionState) { override fun onSessionStateChange(session: GeckoSession, sessionState: GeckoSession.SessionState) {
lastSessionState = sessionState lastSessionState = sessionState
onSessionStateChangeCallback?.invoke(sessionState) onSessionStateChangeCallback?.invoke(sessionState)
Blog.LOGE("onSessionStateChange $sessionState ${session}")
} }
} }
private var lastSessionState: GeckoSession.SessionState? = null private var lastSessionState: GeckoSession.SessionState? = null
@ -557,9 +619,28 @@ open class GeckoWeb @JvmOverloads constructor(
return false // 실제 구현 시에는 이전 MTRANS_Y 값과 비교 로직 추가 return false // 실제 구현 시에는 이전 MTRANS_Y 값과 비교 로직 추가
} }
private suspend fun saveYoutubeCookiesToFile(): String? {
val cookiesStr = currentCookieString
return try {
val cookieFile = File(context.cacheDir, "cookies.txt")
cookieFile.writeText(cookiesStr)
cookieFile.absolutePath
} catch (e: Exception) {
Blog.LOGE("Cookie file save failed", e)
null
}
}
private fun handlePortMessage(msg: PortMessage) { private fun handlePortMessage(msg: PortMessage) {
when (msg.type) { when (msg.type) {
"COOKIES_REPORT"-> {
Blog.LOGE("${msg.value} -> ${msg.url}")
currentCookieString = msg.value ?: ""
currentCookieUrlString = msg.url ?: ""
CoroutineScope(Dispatchers.IO).launch {
saveYoutubeCookiesToFile()
}
}
"SCROLL_STATE" -> { "SCROLL_STATE" -> {
Blog.LOGE("${msg.type} : ${msg.value}") Blog.LOGE("${msg.type} : ${msg.value}")
scrollState = msg.value?.toInt() ?: 0 scrollState = msg.value?.toInt() ?: 0
@ -790,4 +871,9 @@ open class GeckoWeb @JvmOverloads constructor(
fun play_pause() { fun play_pause() {
sendJsonMsg("PLAY_PAUSE") sendJsonMsg("PLAY_PAUSE")
} }
fun pauseYT() {
sendJsonMsg("PAUSE_YT")
}
} }

View File

@ -426,6 +426,7 @@ open class NeoRssActivity : CommonActivity() {
FFmpeg.getInstance().init(this) FFmpeg.getInstance().init(this)
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
Blog.LOGE("YoutubeDL.getInstance().updateYoutubeDL()")
YoutubeDL.getInstance().updateYoutubeDL(this@NeoRssActivity) YoutubeDL.getInstance().updateYoutubeDL(this@NeoRssActivity)
} catch (e: YoutubeDLException) { } catch (e: YoutubeDLException) {
Blog.LOGE("failed to initialize youtubedl-android", e) Blog.LOGE("failed to initialize youtubedl-android", e)

View File

@ -104,6 +104,7 @@ class PortMessage {
var imgSrc: String? = null var imgSrc: String? = null
var base64Data: String? = null var base64Data: String? = null
var value : String? = null var value : String? = null
var url : String? = null
} }
class BookContents { class BookContents {
var chapterTitle : String? = null var chapterTitle : String? = null

View File

@ -90,7 +90,7 @@
android:ellipsize="middle" android:ellipsize="middle"
android:singleLine="true" /> android:singleLine="true" />
<TextView android:id="@+id/btn_dl_video" android:text="download" style="@style/MaterialIconButtonStyle" android:visibility="gone" /> <TextView android:id="@+id/btn_dl_video" android:text="download" style="@style/MaterialIconButtonStyle" />
<TextView android:id="@+id/btn_share" android:text="share" style="@style/MaterialIconButtonStyle" /> <TextView android:id="@+id/btn_share" android:text="share" style="@style/MaterialIconButtonStyle" />
</LinearLayout> </LinearLayout>
</merge> </merge>

View File

@ -6,7 +6,7 @@
android:padding="0dp" android:padding="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@android:color/transparent" android:background="#66000000"
android:orientation="vertical" android:orientation="vertical"
> >
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView