diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e7e98741..ced53b4f 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -120,7 +120,9 @@ dependencies {
// implementation("org.opencv:opencv-android:4.11.0")
// build.gradle에 추가
// implementation ("com.github.aeonSolutions:FloatingActionButtonMenuDrag:1.1")
-
+ implementation("io.github.junkfood02.youtubedl-android:library:0.17.4")
+ implementation("io.github.junkfood02.youtubedl-android:ffmpeg:0.17.4")
+ implementation("io.github.junkfood02.youtubedl-android:aria2c:0.17.4")
implementation ("androidx.media:media:1.7.0")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 445e4c3f..c1636ba8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -57,17 +57,17 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -212,52 +213,52 @@
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/extensions/my_extension/messaging.js b/app/src/main/assets/extensions/my_extension/messaging.js
index 41dc2b02..fb7a2565 100644
--- a/app/src/main/assets/extensions/my_extension/messaging.js
+++ b/app/src/main/assets/extensions/my_extension/messaging.js
@@ -253,9 +253,16 @@ document.addEventListener('DOMContentLoaded', function () {
if (port) {
sendMessage({type: "MSG", msg: "connect prot"});
time1 = setTimeout(autoScrollAndSave(false), 3500)
+
}
})
+document.addEventListener('touchstart', function(e) {
+ console.log('터치 시작');
+});
+document.addEventListener('touchend', function(e) {
+ autoScrollAndSave()
+});
function scrollToLazyImg(fastMode) {
(function(autoScrollAndSave){
@@ -346,12 +353,16 @@ function isNewerThanOneDay(dateStr) {
function autoScrollAndSave(senContents) {
// 도메인에 맞는 handler 실행
const matchedRule = domainRules.find(rule => rule.test(location.href));
- if (matchedRule) {
- matchedRule.handler();
- }
+ try {
+ if (matchedRule) {
+ matchedRule.handler();
+ }
+ }catch (e) { }
+
+ try {
// 공통 광고 요소 제거는 항상 실행
handleCommon();
- window.scrollTo({ top: 2, behavior: 'smooth' });
+}catch (e) { }
if (mainContentsEl == null) {
mainContentsEl = document.body.outerHTML
}
@@ -545,7 +556,11 @@ function handleCommon() {
gotoNext()
}
- window.scrollTo({ top: 2, behavior: 'smooth' });
+ if (window.scrollY < 5) {
+ console.log("window.scrollY >>> " + window.scrollY)
+ window.scrollTo({ top: 5, behavior: 'smooth' });
+ }
+
}
function handleToreentZota() {
if (location.href.search("torrentzota") > -1 && document.querySelectorAll('a')) {
@@ -759,13 +774,26 @@ function handleDoctorsnews() {
}
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) {}
- })
+ try {
+ 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());
+ }catch (e) {
+
+ }
+ try {
+ document.querySelectorAll('div[class^="imgwrap"]').forEach(function (e) {
+ try {e.style.backgroundColor = 'red';} catch (e) {}
+ })
+ } catch (e) {
+
+ }
+ try {
+ document.querySelectorAll('div[class^="imgwrap"]')[0].click()
+ }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 37d5e65a..003161e6 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt
@@ -22,13 +22,11 @@ package bums.lunatic.launcher
import android.annotation.SuppressLint
import android.app.SearchManager
-import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.graphics.Color
-import android.graphics.Rect
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -49,89 +47,50 @@ import android.view.PointerIcon
import android.view.View
import android.view.WindowInsets
import android.view.WindowManager
-import android.widget.Button
-import android.widget.HorizontalScrollView
-import android.widget.LinearLayout
import androidx.activity.OnBackPressedCallback
import androidx.annotation.RequiresApi
-import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
-import androidx.work.ExistingPeriodicWorkPolicy
-import androidx.work.OneTimeWorkRequest
-import androidx.work.PeriodicWorkRequestBuilder
-import androidx.work.WorkManager
import bums.lunatic.launcher.LauncherActivity.Companion.lActivity
import bums.lunatic.launcher.apps.AppDrawer
-import bums.lunatic.launcher.tokiz.Novels
import bums.lunatic.launcher.common.CommonActivity
import bums.lunatic.launcher.databinding.LauncherActivityBinding
-import bums.lunatic.launcher.feeds.WidgetHost
-import bums.lunatic.launcher.helpers.BluetoothManager
import bums.lunatic.launcher.helpers.Constants.Companion.KEY_STATUS_BAR
import bums.lunatic.launcher.helpers.Constants.Companion.PREFS_SETTINGS
-import bums.lunatic.launcher.helpers.Constants.Companion.widgetHostId
+import bums.lunatic.launcher.helpers.ForeGroundService
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
import bums.lunatic.launcher.model.RssDataType
+import bums.lunatic.launcher.receiver.NLService
+import bums.lunatic.launcher.settings.SettingsActivity
import bums.lunatic.launcher.tokiz.Comics
import bums.lunatic.launcher.tokiz.Magnet
+import bums.lunatic.launcher.tokiz.Novels
import bums.lunatic.launcher.tokiz.Perplexity
import bums.lunatic.launcher.tokiz.Twitter
import bums.lunatic.launcher.tokiz.Webtoons
import bums.lunatic.launcher.tokiz.Zota
import bums.lunatic.launcher.utils.Blog
-import bums.lunatic.launcher.utils.FeedParseManager
-import bums.lunatic.launcher.utils.getJ
-import bums.lunatic.launcher.workers.AppInfoGetter
-import bums.lunatic.launcher.workers.ArcaGetter
-import bums.lunatic.launcher.workers.CalendarGetter
-import bums.lunatic.launcher.workers.ClienGetter
-import bums.lunatic.launcher.workers.ContactInfoGetter
-import bums.lunatic.launcher.workers.DCGetter
-import bums.lunatic.launcher.workers.DotaxGetter
-import bums.lunatic.launcher.workers.DotaxGetter.Companion.COMIC2_WORK_TAG
-import bums.lunatic.launcher.workers.FmKoreaGetter
-import bums.lunatic.launcher.workers.FmKoreaGetter.Companion.FM_WORK_TAG
-import bums.lunatic.launcher.workers.LocationGetter
-import bums.lunatic.launcher.workers.NewsFeedsGetter
-import bums.lunatic.launcher.workers.NewsFeedsGetter.Companion.FEDDS_WORK_TAG
-import bums.lunatic.launcher.workers.RecentCallGetter
-import bums.lunatic.launcher.workers.RecentSmsGetter
-import bums.lunatic.launcher.workers.RecentSmsGetter.Companion.SMS_WORK_TAG
-import bums.lunatic.launcher.workers.RedditGetter
-import bums.lunatic.launcher.workers.RedditGetter.Companion.REDDIT_WORK_TAG
-import bums.lunatic.launcher.workers.RuliWebGetter
-import bums.lunatic.launcher.workers.TheQooGetter
import bums.lunatic.launcher.workers.WorkersDb
-import bums.lunatic.launcher.workers.YoutubeGetter
-import bums.lunatic.launcher.workers.YoutubeGetter.Companion.YT_WORK_TAG
import com.google.android.material.color.DynamicColors
+import com.yausername.ffmpeg.FFmpeg
+import com.yausername.youtubedl_android.YoutubeDL
+import com.yausername.youtubedl_android.YoutubeDLException
import io.realm.kotlin.ext.query
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
import kr.lunaticbum.utils.ui.DisplayUtil
import org.json.JSONObject
import org.mozilla.geckoview.ExperimentDelegate
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings
-import java.util.Base64
import java.util.Calendar
import java.util.Date
-import java.util.concurrent.Executors
-import java.util.concurrent.TimeUnit
-import kotlin.jvm.java
open class LauncherActivity : CommonActivity() {
@@ -209,7 +168,8 @@ open class LauncherActivity : CommonActivity() {
Blog.LOGE("onConfigurationChanged newConfig?.screenWidthDp >> ${newConfig?.screenWidthDp}")
Blog.LOGE("onConfigurationChanged newConfig?.screenHeightDp >> ${newConfig?.screenHeightDp}")
isOpendFold = (newConfig.screenWidthDp * 1.1f) > newConfig.screenHeightDp
-
+ val nullCursor = PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL)
+ binding.root.setPointerIcon(nullCursor)
}
// override fun onKeyClick(keyCode: Int): Boolean {
// when (keyCode) {
@@ -533,15 +493,24 @@ open class LauncherActivity : CommonActivity() {
@SuppressLint("NewApi", "MissingPermission")
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
+ super.onCreate(savedInstanceState)
+ try {
+ YoutubeDL.getInstance().init(this)
+ FFmpeg.getInstance().init(this);
+ } catch (e: YoutubeDLException) {
+ Blog.LOGE("failed to initialize youtubedl-android", e)
+ }
+ val intent = Intent(this, ForeGroundService::class.java)
+ this.startForegroundService(intent)
+
+ val nlService = Intent(this, NLService::class.java)
+ this.startService(nlService)
+
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
lActivity = this
-
DynamicColors.applyToActivityIfAvailable(this)
-
-
settingsPrefs = getSharedPreferences(PREFS_SETTINGS, 0)
-// AppCompatDelegate.setDefaultNightMode(settingsPrefs.getInt(KEY_APPLICATION_THEME, MODE_NIGHT_FOLLOW_SYSTEM))
- super.onCreate(savedInstanceState)
+
binding = LauncherActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
@@ -553,8 +522,8 @@ open class LauncherActivity : CommonActivity() {
updateLocationService()
- val intent = Intent(this, BluetoothManager::class.java)
- ContextCompat.startForegroundService(this, intent)
+
+
showContents(binding.feeds.id)
binding.floatingActionMenu.setOnMenuButtonClickListener { v->
Blog.LOGE("v >> ${v}")
@@ -622,6 +591,10 @@ open class LauncherActivity : CommonActivity() {
.replace(R.id.fragment_container, Magnet())
.commit()
}
+ R.id.setting ->{
+ startActivity(Intent(this, SettingsActivity::class.java))
+ }
+
else -> {}
}
binding.floatingActionMenu.close(false)
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/helpers/BluetoothManager.kt b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt
similarity index 75%
rename from app/src/main/kotlin/bums/lunatic/launcher/helpers/BluetoothManager.kt
rename to app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt
index a555d592..87e2a2fb 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/helpers/BluetoothManager.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt
@@ -2,9 +2,9 @@ package bums.lunatic.launcher.helpers
import android.Manifest
import android.annotation.SuppressLint
-import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
+import android.app.PendingIntent
import android.app.Service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
@@ -17,12 +17,13 @@ import android.os.Build
import android.os.IBinder
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
+import androidx.core.content.ContextCompat
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
+import bums.lunatic.launcher.LauncherActivity
import bums.lunatic.launcher.LauncherActivity.Companion.lActivity
import bums.lunatic.launcher.R
-import bums.lunatic.launcher.home.GeckoWeb
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.workers.ArcaGetter
import bums.lunatic.launcher.workers.ClienGetter
@@ -51,8 +52,11 @@ import okhttp3.ResponseBody
import java.util.concurrent.TimeUnit
-class BluetoothManager : Service() {
-
+class ForeGroundService : Service() {
+ companion object {
+ val ACTION_SENDMSG = "ACTION_SEND_TO_LOVE"
+ val EXTRA_MSGKEY = "SEND_MSG"
+ }
enum class BLUETOOTH_STATE(val statestr: String) {
ENABLED("enabledBlutooth"),
DISABLED("disableBlutooth"),
@@ -67,44 +71,71 @@ class BluetoothManager : Service() {
super.onCreate()
Blog.LOGE("onCreate")
mWorkManager = WorkManager.getInstance(this)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- startForeground(NOTIF_ID, createNotification(this))
- }
val filter = IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED)
registerReceiver(bluetoothreceiver, filter)
refreshFeeds()
-// GeckoWeb(applicationContext).apply {
-// loadUrl("https://arca.live/b/live")
-// }
}
- override fun onBind(intent: Intent?): IBinder? {
- Blog.LOGE("intent >>> ${intent}")
- return null
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Blog.LOGE("onStartCommand >>> ${intent}")
+ if (ACTION_SENDMSG.equals(intent?.action)) {
+ intent?.getStringExtra(EXTRA_MSGKEY)?.let {
+ sendToI(it)
+ }
+ }
+ startForeGround()
+ return START_STICKY
}
- private val CHANNEL_ID = "ble_service_channel"
- private val CHANNEL_NAME = "BLE 서비스"
- fun createNotification(context: Context): Notification {
+ fun startForeGround() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
- CHANNEL_NAME,
- NotificationManager.IMPORTANCE_HIGH // 중요도 낮게 (필요시 변경)
+ "BLE 서비스 채널",
+ NotificationManager.IMPORTANCE_HIGH
)
- val notificationManager =
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.createNotificationChannel(channel)
+ val manager = getSystemService(NotificationManager::class.java)
+ manager.createNotificationChannel(channel)
}
- return NotificationCompat.Builder(context, CHANNEL_ID)
+ val intent = Intent(this, LauncherActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ }
+
+ val pendingIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ startForeground(NOTIF_ID, NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("BLE 서비스")
.setContentText("실행중입니다.")
.setPriority(NotificationCompat.PRIORITY_MAX)
.setSmallIcon(R.drawable.ic_b)
+ .setContentIntent(pendingIntent)
+ .addAction(android.R.drawable.ic_btn_speak_now,"퇴근", makeSendMsgAction(0,"돼지 퇴근했다요~!"))
+ .addAction(android.R.drawable.ic_btn_speak_now,"버스 탐", makeSendMsgAction(1,"돼지 버스 탔다요~!"))
+ .addAction(android.R.drawable.ic_btn_speak_now,"버스 내림", makeSendMsgAction(2,"돼지 버스 내린다요~!"))
.setOngoing(true) // 사용자가 알림을 스와이프로 지울 수 없게 만듦
- .build()
+ .build())
}
+ fun makeSendMsgAction(code : Int, msg : String) : PendingIntent {
+ val actionIntent = Intent(this, ForeGroundService::class.java).apply {
+ action = ACTION_SENDMSG
+ putExtra(EXTRA_MSGKEY, msg) // 전달할 데이터
+ }
+ return PendingIntent.getService(this, code, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE )
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ Blog.LOGE("onBind intent >>> ${intent}")
+ return null
+ }
+ private val CHANNEL_ID = "ble_service_channel"
+ private val CHANNEL_NAME = "BLE 서비스"
+
//페어링된 디바이스 정보 가져오기
fun getPairedDevices() {
@@ -192,6 +223,14 @@ class BluetoothManager : Service() {
PeriodicWorkRequestBuilder(PrefLong.locationTimePeriod.get(), TimeUnit.MINUTES)
.addTag(LocationGetter.TAG)
.build())
+ mWorkManager?.cancelAllWorkByTag(ServiceWatchdogWorker.TAG)
+ mWorkManager?.enqueueUniquePeriodicWork(
+ ServiceWatchdogWorker.TAG,
+ ExistingPeriodicWorkPolicy.REPLACE,
+ PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES)
+ .addTag(ServiceWatchdogWorker.TAG)
+ .build())
+
}
@@ -202,6 +241,21 @@ class BluetoothManager : Service() {
return mWorkManager
}
+ fun sendToI(msg: String) {
+ if (PrefString.telegramSendTarget.get().length > 5) {
+ CoroutineScope(Dispatchers.IO).launch {
+ OkHttpClient.Builder()
+ .connectionPool(ConnectionPool(5, 60, TimeUnit.SECONDS))
+ .build().newCall(Request.Builder().url("https://api.telegram.org/bot7934509464:AAE_xUbICxMdywLGnxo7BkeIqA1nVza4P9w/sendMessage?chat_id=${PrefString.telegramSendTarget.get()}&text=${msg}")
+ .addHeader("Content-Type", "application/json").get().build()).execute()?.let { response ->
+ if (response.isSuccessful()) {
+ val body: ResponseBody? = response.body()
+ if (body != null) { }
+ } else Blog.LOGE("sendToI telegram Error Occurred")
+ }
+ }
+ }
+ }
fun sendToI(boolean: Boolean) {
if (PrefString.telegramSendTarget.get().length > 5) {
CoroutineScope(Dispatchers.IO).launch {
@@ -215,7 +269,7 @@ class BluetoothManager : Service() {
val response: Response = OkHttpClient.Builder()
.connectionPool(ConnectionPool(5, 60, TimeUnit.SECONDS))
.build().newCall(Request.Builder().url(url)
- .addHeader("Content-Type", "application/json").get().build()).execute()
+ .addHeader("Content-Type", "application/json").get().build()).execute()
if (response.isSuccessful()) {
// 응답 받아서 처리
val body: ResponseBody? = response.body()
@@ -251,6 +305,13 @@ class BluetoothManager : Service() {
return BLUETOOTH_STATE.NOT_SUPPORT.statestr
}
+ override fun stopService(name: Intent?): Boolean {
+ Blog.LOGE("stopService ${name}")
+ val intent = Intent(this, ForeGroundService::class.java)
+ ContextCompat.startForegroundService(this, intent)
+ return super.stopService(name)
+ }
+
//add Receive action
private fun addFilterAction(): IntentFilter {
val stateFilter = IntentFilter()
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/helpers/ServiceWatchdogWorker.kt b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ServiceWatchdogWorker.kt
new file mode 100644
index 00000000..64e17ff8
--- /dev/null
+++ b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ServiceWatchdogWorker.kt
@@ -0,0 +1,36 @@
+package bums.lunatic.launcher.helpers
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+
+class ServiceWatchdogWorker(
+ private val context: Context,
+ workerParams: WorkerParameters
+) : Worker(context, workerParams) {
+
+ companion object{
+ val TAG = "ServiceWatchdogWorker"
+ }
+
+ override fun doWork(): Result {
+ val isServiceRunning = isServiceRunning(ForeGroundService::class.java)
+ if (!isServiceRunning) {
+ val intent = Intent(context, ForeGroundService::class.java)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(intent)
+ } else {
+ context.startService(intent)
+ }
+ }
+ return Result.success()
+ }
+ fun isServiceRunning(serviceClass: Class<*>): Boolean {
+ val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ return manager.getRunningServices(Int.MAX_VALUE)
+ .any { it.service.className == serviceClass.name }
+ }
+}
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 5d5f6c99..58564a38 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt
@@ -40,6 +40,7 @@ import androidx.core.net.toUri
import androidx.core.view.isVisible
import bums.lunatic.launcher.LauncherActivity.Companion.getRuntime
import bums.lunatic.launcher.R
+import bums.lunatic.launcher.model.others.Button
import bums.lunatic.launcher.tokiz.data.model.PortMessage
import bums.lunatic.launcher.tokiz.view.BWebview
import bums.lunatic.launcher.utils.Blog
@@ -47,6 +48,9 @@ import bums.lunatic.launcher.utils.CommonUtils
import bums.lunatic.launcher.workers.WorkersDb
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 com.yausername.youtubedl_android.YoutubeDLResponse
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -54,6 +58,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kr.lunaticbum.utils.service.ServiceUtil.getSystemService
+import kr.lunaticbum.utils.service.ServiceUtil.layoutInflater
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONException
@@ -61,6 +66,7 @@ import org.json.JSONObject
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.mozilla.gecko.util.ThreadUtils
+import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
import org.mozilla.geckoview.ExperimentDelegate
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoSession
@@ -76,6 +82,7 @@ import java.io.FileOutputStream
import java.io.InputStream
import java.text.SimpleDateFormat
import java.util.Date
+import java.util.UUID
class GeckoWeb : BWebview {
constructor(context: Context?) : super(context) {
@@ -84,7 +91,7 @@ class GeckoWeb : BWebview {
var decoViews = arrayListOf()
override fun setVisibility(visibility: Int) {
super.setVisibility(visibility)
- decoViews.filter { it != null && it.id > -1 }.forEach { it.visibility = visibility }
+ decoViews.filter { it != null && it.id > -1 && it.id != R.id.dl_video }.forEach { it.visibility = visibility }
}
interface OnSave {
fun saved()
@@ -448,6 +455,153 @@ class GeckoWeb : BWebview {
}
}
}
+
+ fun checkIfDownloadable(url: String) {
+ CoroutineScope(Dispatchers.Main).launch {
+ runOnUiThread {
+ decoViews.filter { it.id == R.id.dl_video }.firstOrNull()?.let {
+ it.setOnClickListener {}
+ it.visibility = View.GONE
+ }}}
+ Blog.LOGE("checkIfDownloadable ${url}")
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ val videoInfo = YoutubeDL.getInstance().getInfo(url)
+ // 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){View.VISIBLE} else{View.GONE}
+ }
+ }
+ }
+
+ } catch (e: Exception) {
+ Blog.LOGE("checkIfDownloadable ${url} ${e}")
+ CoroutineScope(Dispatchers.Main).launch {
+ runOnUiThread {
+ decoViews.filter { it.id == R.id.dl_video }.firstOrNull()?.let {
+ it.setOnClickListener {}
+ it.visibility = View.GONE
+ }}}
+ }
+ }
+ }
+ 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()
+ }
+
+
+ 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()
+ }
+
+
+
+ lateinit var progressDialog: AlertDialog
+ fun showProgressDialog() {
+ val dialogView = layoutInflater.inflate(R.layout.progress_dialog, null)
+ val progressBar = dialogView.findViewById(R.id.progressBar)
+ val textProgress = dialogView.findViewById(R.id.textProgress)
+ val btn = dialogView.findViewById(R.id.dl_cancel)
+ progressDialog = AlertDialog.Builder(context)
+ .setTitle("다운로드 중...")
+ .setView(dialogView)
+ .setCancelable(false)
+ .create()
+ progressDialog.show()
+
+ // UI 업데이트 함수 예 (나중에 실행)
+ fun updateProgress(progress: Int, est : Long, str : String) {
+ runOnUiThread {
+ progressBar.progress = progress
+ textProgress.text = "$progress%"
+ }
+ }
+ }
+ fun dismissProgressDialog() {
+ progressDialog.dismiss()
+ }
+
+ suspend fun downloadVideo(processId : String,url: String, updateProgress: (Float, Long, String) -> Unit) = withContext(Dispatchers.IO) {
+ val youtubeDLDir = File(
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
+ "youtubedl-android"
+ )
+ val command = YoutubeDLRequest(url)
+ command.addOption("-o", youtubeDLDir.getAbsolutePath() + "/%(title)s.%(ext)s");
+ var process = YoutubeDL.getInstance().execute(command,processId) { progress, est , str ->
+ updateProgress(progress, est, str)
+ }
+ return@withContext process
+ }
+
+ fun videoDlownLoad(videoUrl : String) {
+ CoroutineScope(Dispatchers.Main).launch {
+ try {
+ showProgressDialog()
+ var res: YoutubeDLResponse? = null
+ val processId = UUID.randomUUID().toString()
+ res = downloadVideo(processId, videoUrl) { progress , time , str->
+ runOnUiThread {
+ val pb =
+ progressDialog.findViewById(R.id.progressBar)
+ val tv =
+ progressDialog.findViewById(R.id.textProgress)
+ pb?.progress = progress.toInt()
+ val btn = progressDialog.findViewById(R.id.dl_cancel)
+ tv?.text = "$progress%\n$str"
+ btn?.setOnClickListener {
+ progressDialog?.dismiss()
+ YoutubeDL.getInstance().destroyProcessById(processId);
+ }
+ }
+ }
+ dismissProgressDialog()
+ Toast.makeText(context, "다운로드 완료", Toast.LENGTH_SHORT)
+ .show()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ progressDialog?.dismiss()
+ Toast.makeText(context, "오류: ${e.message}", Toast.LENGTH_LONG)
+ .show()
+ }
+ }
+ }
+
var dialog : Dialog? = null
fun getFilterF() = String(java.util.Base64.getMimeDecoder().decode("aHR0cHM6Ly9pamF2dG9ycmVudC5jb20=".toByteArray()))
var currentTitle = ""
@@ -474,21 +628,7 @@ 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} ")
@@ -504,6 +644,7 @@ class GeckoWeb : BWebview {
val client = OkHttpClient()
val request = Request.Builder()
.url(url)
+ .addHeader("Referer", lastedUrl)
.addHeader("User-Agent", "Mozilla/5.0")
// 필요시 Referer, 쿠키 등 헤더 추가
.build()
@@ -531,6 +672,7 @@ class GeckoWeb : BWebview {
super.onExternalResponse(session, response)
}
+
override fun onContextMenu(
session: GeckoSession,
screenX: Int,
@@ -539,18 +681,11 @@ class GeckoWeb : BWebview {
) {
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()
-// }
+ lastedUrl?.let { videoUrl ->
+ lastedUrl?.let {
+ videoDlownLoad(it)
+ }
+ }
} else {
Blog.LOGE("onContextMenu:: x = ${x}, y = ${y} , element = ${Gson().toJson(element)}")
if (element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE) {
@@ -656,8 +791,8 @@ class GeckoWeb : BWebview {
)
} == true ||
((it.host?.contains("x.com") ?: false) == true) ||
- ((it.host?.contains("www.instagram.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)
@@ -723,6 +858,7 @@ class GeckoWeb : BWebview {
} else {
lastedUrl = url
}
+ checkIfDownloadable(url)
}
@@ -735,6 +871,8 @@ class GeckoWeb : BWebview {
it.tag = currentTitle
it.text = url
}
+ }else if (it.id == R.id.reload) {
+ it.setOnClickListener { session.reload() }
}
}
}
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 37a3925d..0f118deb 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt
@@ -625,6 +625,8 @@ internal class RssHome : Fragment() {
(activity as? LauncherActivity)?.let { activity ->
binding.geckoWeb.decoViews.add(activity.findViewById(R.id.current_address))
binding.geckoWeb.decoViews.add(activity.findViewById(R.id.back))
+ binding.geckoWeb.decoViews.add(activity.findViewById(R.id.reload))
+ binding.geckoWeb.decoViews.add(activity.findViewById(R.id.dl_video))
}
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/receiver/NLService.kt b/app/src/main/kotlin/bums/lunatic/launcher/receiver/NLService.kt
index 07212a82..49f0ffdb 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/receiver/NLService.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/receiver/NLService.kt
@@ -1,5 +1,6 @@
package bums.lunatic.launcher.receiver
+import android.app.Notification
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
@@ -13,9 +14,11 @@ import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
+import bums.lunatic.launcher.helpers.ForeGroundService
import bums.lunatic.launcher.model.CurrentPlayItem
import bums.lunatic.launcher.model.NotificationItem
import bums.lunatic.launcher.utils.BitmapConverter
+import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.workers.WorkersDb
import io.realm.kotlin.UpdatePolicy
import io.realm.kotlin.ext.query
@@ -29,7 +32,6 @@ class NLService : NotificationListenerService() {
super.onCreate()
nlservicereciver = NLServiceReceiver()
val filter = IntentFilter()
-// filter.addAction("com.kpbird.nlsexample.NOTIFICATION_LISTENER_SERVICE_EXAMPLE")
registerReceiver(nlservicereciver, filter)
}
@@ -38,130 +40,54 @@ class NLService : NotificationListenerService() {
unregisterReceiver(nlservicereciver)
}
- val skips = arrayListOf("com.wssyncmldm")
+
@RequiresApi(Build.VERSION_CODES.S)
override fun onNotificationPosted(sbn: StatusBarNotification) {
-// BLog.LOGE("NLService********** onNotificationPosted")
-// BLog.LOGE("NLServiceID :" + sbn.id + "\t${sbn.notification.tickerText}\t" + sbn.packageName)
-// sbn.notification.extras.keySet().forEach {
-// BLog.LOGE("NLService********** keySet >> ${it} ${sbn.notification.extras.get(it)}")
-// }
- try {
- if (sbn.id != 0 && (sbn.packageName.contains(".") || sbn.packageName.contains("android")) && sbn.packageName.length > 0) {
- NotificationItem().apply {
- notiId = sbn.id
- pkgName = sbn.packageName
- title = sbn.notification?.extras?.getString("android.title") ?: ""
- subtext = sbn.notification?.extras?.getString("android.subText") ?: ""
- selfDisplayName = sbn.notification?.extras?.getString("android.selfDisplayName") ?: ""
- tikerMsg = sbn.notification?.tickerText?.toString() ?: ""
- postTime = sbn.postTime
- var uniq = title ?: subtext ?: selfDisplayName ?: tikerMsg ?: ""
- uniq_id = "${sbn.id}_${sbn.packageName}_${if (uniq.length > 3) uniq.substring(0,3) else uniq}"
-// BLog.LOGE("NLService********** enqueue TelegramBotGetter ${true == "bumssavor".equals(title)}")
-// BLog.LOGE("NLService********** enqueue TelegramBotGetter ${(true == "org.telegram.messenger".equals(pkgName))}")
-// BLog.LOGE("NLService********** enqueue TelegramBotGetter ${sbn.notification?.extras?.getString("android.text")?.startsWith("/") == true}")
- }.apply {
- if (skips.contains(pkgName)) {
+ Blog.LOGE("onNotificationPosted ${sbn}")
+ val notification = sbn.notification
+ val extras = notification.extras
+ when (sbn.packageName){
+ "com.kakao.talk" -> {
- } else {
-// WorkersDb.insertNoti(this)
-// BLog.LOGE("NLService********** onNotificationPosted ${Gson().toJson(this)}")
- }
- }
- }
- } catch (e : Exception) {
- e.printStackTrace()
- }
-
-
- try {
- if (sbn.packageName.contains("youtube")) {
- val m = getSystemService()!!
- val component = ComponentName(this, NLService::class.java)
- val sessions = m.getActiveSessions(component)
- sessions.forEach { session ->
- WorkersDb.getRealm().writeBlocking {
-// Blog.LOGE("session.playbackState >>> ${session.playbackState}")
- if (session.playbackState != null) {
- if (session.playbackState?.isActive == true && session.playbackState?.state?.equals(
- STATE_PLAYING
- ) == true
- ) {
- session.playbackState?.state
- val result = query().find()
- var current: CurrentPlayItem? = null
- if (result.size > 0) {
- current = result.first()
- } else {
- current = CurrentPlayItem()
- copyToRealm(current, UpdatePolicy.ALL)
- }
- if (session?.metadata?.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART) == true) {
- current.albumArt = BitmapConverter.BitmapToString(
- session.metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
- )
- } else {
- current.albumArt = ""
- }
- current.title =
- session?.metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)
- current.artists =
- session?.metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
- } else {
- delete(query().find())
- }
- }
- }
}
- }}catch (e : Exception) {
- e.printStackTrace()
}
-// val i = Intent("com.kpbird.nlsexample.NOTIFICATION_LISTENER_EXAMPLE")
-// i.putExtra("notification_event", "onNotificationPosted :" + sbn.packageName + "\n")
-// sendBroadcast(i)
+ val title = extras.getString(Notification.EXTRA_TITLE) ?: ""
+ val text = extras.getCharSequence(Notification.EXTRA_TEXT)?.toString() ?: ""
+ val bigText = extras.getCharSequence(Notification.EXTRA_BIG_TEXT)?.toString() ?: ""
+ val extraInfo = extras.getCharSequence(Notification.EXTRA_INFO_TEXT)?.toString() ?: ""
+ val subText = extras.getCharSequence(Notification.EXTRA_SUB_TEXT)?.toString() ?: ""
+ val conversationTitle = extras.getCharSequence(Notification.EXTRA_CONVERSATION_TITLE)?.toString() ?: ""
+ val summaryText = extras.getCharSequence(Notification.EXTRA_SUMMARY_TEXT)?.toString() ?: ""
+ val verificationText = extras.getCharSequence(Notification.EXTRA_VERIFICATION_TEXT)?.toString() ?: ""
+
+ Blog.LOGE("title >> ${title} text >> ${text} bigText >> ${bigText} extraInfo >> ${extraInfo} subText >> ${subText} conversationTitle >> ${conversationTitle} summaryText >> ${summaryText} verificationText >> ${verificationText}")
+
}
override fun onNotificationRemoved(sbn: StatusBarNotification) {
-//// BLog.LOGE("NLService********** onNOtificationRemoved")
-//// BLog.LOGE("NLService ID :" + sbn.id + "\t" + sbn.notification.tickerText + "\t" + sbn.packageName)
-// var uniq_id = "${sbn.id}_${sbn.packageName}"
-// try {
-// WorkersDb.getRealm()?.apply {
-// this.writeBlocking {
-//// delete(query().query("pkgName == $0", sbn.packageName).find())
-// }
-// }
-// }catch (e : Exception){e.printStackTrace()}
-//// val i = Intent("com.kpbird.nlsexample.NOTIFICATION_LISTENER_EXAMPLE")
-//// i.putExtra("notification_event", "onNotificationRemoved :" + sbn.packageName + "\n")
-//// sendBroadcast(i)
+ Blog.LOGE("onNotificationPosted ${sbn}")
+ if (sbn.packageName == "bums.lunatic.launcher" && sbn.id == 830721) {
+ // 포그라운드 알림이 사라짐 감지
+ Blog.LOGE("NotificationListener", "포그라운드 알림 제거 감지")
+
+ // 서비스 재시작 시도
+ val intent = Intent(this, ForeGroundService::class.java)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForegroundService(intent)
+ } else {
+ startService(intent)
+ }
+ }
}
internal inner class NLServiceReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
-// BLog.LOGE("NLService intent >>> ${intent.action}")
if (intent.getStringExtra("command") == "clearall") {
this@NLService.cancelAllNotifications()
} else if (intent.getStringExtra("command") == "list") {
-// val i1 = Intent("com.kpbird.nlsexample.NOTIFICATION_LISTENER_EXAMPLE")
-// i1.putExtra("notification_event", "=====================")
-// sendBroadcast(i1)
- var i = 1
- for (sbn in this@NLService.activeNotifications) {
-// BLog.LOGE("NLService sbn >>> ${sbn.packageName} , ${Gson().toJson(sbn.notification.extras.keySet())}")
-// val i2 = Intent("com.kpbird.nlsexample.NOTIFICATION_LISTENER_EXAMPLE")
-// i2.putExtra("notification_event", i.toString() + " " + sbn.packageName + "\n")
-// sendBroadcast(i2)
-// i++
- }
-// val i3 = Intent("com.kpbird.nlsexample.NOTIFICATION_LISTENER_EXAMPLE")
-// i3.putExtra("notification_event", "===== Notification List ====")
-// sendBroadcast(i3)
+
}
}
}
-
-
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/view/ZoomImageView.kt b/app/src/main/kotlin/bums/lunatic/launcher/view/ZoomImageView.kt
new file mode 100644
index 00000000..db223da1
--- /dev/null
+++ b/app/src/main/kotlin/bums/lunatic/launcher/view/ZoomImageView.kt
@@ -0,0 +1,57 @@
+package bums.lunatic.launcher.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.ScaleGestureDetector
+import androidx.appcompat.widget.AppCompatImageView
+class ZoomImageView(context: Context, attrs: AttributeSet) : AppCompatImageView(context, attrs),
+ ScaleGestureDetector.OnScaleGestureListener, GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
+
+ private var scaleFactor = 1.0f
+ private val scaleGestureDetector = ScaleGestureDetector(context, this)
+ private val gestureDetector = GestureDetector(context, this)
+
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ scaleGestureDetector.onTouchEvent(event)
+ gestureDetector.onTouchEvent(event)
+ return true
+ }
+
+ // ScaleGestureDetector callbacks
+ override fun onScale(detector: ScaleGestureDetector): Boolean {
+ scaleFactor *= detector.scaleFactor
+ scaleFactor = scaleFactor.coerceIn(1.0f, 4.0f)
+ scaleX = scaleFactor
+ scaleY = scaleFactor
+ return true
+ }
+
+ override fun onScaleBegin(detector: ScaleGestureDetector) = true
+ override fun onScaleEnd(detector: ScaleGestureDetector) {}
+
+ // GestureDetector callbacks (클릭, 터치 등)
+ override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
+ performClick() // 클릭 이벤트 발생
+ return true
+ }
+
+ override fun performClick(): Boolean {
+ super.performClick()
+ // 추가로 클릭 시 동작할 코드 넣기
+ if (hasOnClickListeners()) {
+ callOnClick()
+ }
+ return true
+ }
+
+ override fun onDown(e: MotionEvent) = true
+ override fun onShowPress(e: MotionEvent) {}
+ override fun onSingleTapUp(e: MotionEvent) = true
+ override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float) = false
+ override fun onLongPress(e: MotionEvent) {}
+ override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float) = false
+ override fun onDoubleTap(e: MotionEvent) = true
+ override fun onDoubleTapEvent(e: MotionEvent) = true
+}
diff --git a/app/src/main/res/drawable/dl_vid.png b/app/src/main/res/drawable/dl_vid.png
new file mode 100644
index 00000000..c6148858
Binary files /dev/null and b/app/src/main/res/drawable/dl_vid.png differ
diff --git a/app/src/main/res/layout/launcher_activity.xml b/app/src/main/res/layout/launcher_activity.xml
index d6646ed5..01f48345 100644
--- a/app/src/main/res/layout/launcher_activity.xml
+++ b/app/src/main/res/layout/launcher_activity.xml
@@ -36,12 +36,27 @@
android:layout_width="40dp"
tools:ignore="ContentDescription"
android:layout_height="40dp" />
+
+
+
diff --git a/app/src/main/res/layout/layout_rss_summary.xml b/app/src/main/res/layout/layout_rss_summary.xml
index 2a62acb2..827c3e41 100644
--- a/app/src/main/res/layout/layout_rss_summary.xml
+++ b/app/src/main/res/layout/layout_rss_summary.xml
@@ -49,7 +49,7 @@
android:textColor="@color/white"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
-
-
+
+
+
+
+
+
+