....
This commit is contained in:
parent
53ba35a3de
commit
4acc01b742
@ -901,5 +901,5 @@ function autoScrollAndHandleDotax() {
|
||||
//
|
||||
// // 시작
|
||||
// scrollAndSend();
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,16 +76,21 @@ 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.YouTube
|
||||
import bums.lunatic.launcher.tokiz.Zota
|
||||
import bums.lunatic.launcher.utils.Blog
|
||||
import bums.lunatic.launcher.utils.KakaoPublicTransfer
|
||||
import bums.lunatic.launcher.workers.WorkersDb
|
||||
import com.google.android.gms.common.util.DataUtils
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.gson.Gson
|
||||
import com.yausername.ffmpeg.FFmpeg
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import com.yausername.youtubedl_android.YoutubeDLException
|
||||
import io.realm.kotlin.ext.query
|
||||
import kr.lunaticbum.utils.ui.DisplayUtil
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.helper.DataUtil
|
||||
import org.mozilla.geckoview.ExperimentDelegate
|
||||
import org.mozilla.geckoview.GeckoResult
|
||||
import org.mozilla.geckoview.GeckoRuntime
|
||||
@ -465,6 +470,7 @@ open class LauncherActivity : CommonActivity() {
|
||||
} catch (e: YoutubeDLException) {
|
||||
Blog.LOGE("failed to initialize youtubedl-android", e)
|
||||
}
|
||||
|
||||
val intent = Intent(this, ForeGroundService::class.java)
|
||||
this.startForegroundService(intent)
|
||||
|
||||
@ -536,6 +542,11 @@ open class LauncherActivity : CommonActivity() {
|
||||
.replace(R.id.fragment_container, Comics())
|
||||
.commit()
|
||||
}
|
||||
R.id.youtube ->{
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, YouTube())
|
||||
.commit()
|
||||
}
|
||||
R.id.perplexity ->{
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, Perplexity())
|
||||
@ -679,6 +690,9 @@ open class LauncherActivity : CommonActivity() {
|
||||
currentFragment.doNextPage()
|
||||
}
|
||||
}
|
||||
is YouTube -> {
|
||||
currentFragment.back()
|
||||
}
|
||||
is Novels -> {
|
||||
currentFragment.actionNextEvent(false)
|
||||
}
|
||||
|
||||
@ -13,7 +13,9 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
@ -41,6 +43,8 @@ import bums.lunatic.launcher.workers.RuliWebGetter
|
||||
import bums.lunatic.launcher.workers.TheQooGetter
|
||||
import bums.lunatic.launcher.workers.YoutubeGetter
|
||||
import bums.lunatic.launcher.workers.YoutubeGetter.Companion.YT_WORK_TAG
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import com.yausername.youtubedl_android.YoutubeDLRequest
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -49,6 +53,8 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
@ -56,7 +62,12 @@ class ForeGroundService : Service() {
|
||||
companion object {
|
||||
val ACTION_SENDMSG = "ACTION_SEND_TO_LOVE"
|
||||
val EXTRA_MSGKEY = "SEND_MSG"
|
||||
|
||||
val ACTION_VIDEO_DOWNLOAD = "ACTION_YTURL_DOWNLOAD"
|
||||
val EXTRA_TARGET_URL = "ACTION_SEND_TO_LOVE"
|
||||
val targetUrls = arrayListOf<String>()
|
||||
}
|
||||
|
||||
enum class BLUETOOTH_STATE(val statestr: String) {
|
||||
ENABLED("enabledBlutooth"),
|
||||
DISABLED("disableBlutooth"),
|
||||
@ -78,20 +89,80 @@ class ForeGroundService : Service() {
|
||||
|
||||
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)
|
||||
when(intent?.action) {
|
||||
ACTION_SENDMSG -> {
|
||||
intent?.getStringExtra(EXTRA_MSGKEY)?.let {
|
||||
sendToI(it)
|
||||
}
|
||||
}
|
||||
ACTION_VIDEO_DOWNLOAD -> {
|
||||
intent?.getStringExtra(EXTRA_TARGET_URL)?.let {
|
||||
Uri.parse(it)?.let {
|
||||
addToTargetYtubeUrl(it.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startForeGround()
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
fun startForeGround() {
|
||||
fun addToTargetYtubeUrl(url : String) {
|
||||
targetUrls.add(url)
|
||||
if((targetUrls?.size ?: 0) > 0) {
|
||||
downloadVideo(targetUrls?.firstOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
var currentProcessId : String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (value == null) {
|
||||
startForeGround(0,0)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadVideo(url: String?) {
|
||||
url?.let {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
|
||||
|
||||
try {
|
||||
val youtubeDLDir = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"youtubedl-android"
|
||||
)
|
||||
val command = YoutubeDLRequest(url)
|
||||
command.addOption("-o", youtubeDLDir.getAbsolutePath() + "/%(title)s.%(ext)s");
|
||||
currentProcessId = UUID.randomUUID().toString()
|
||||
YoutubeDL.getInstance()
|
||||
.execute(command, currentProcessId) { progress, est, str ->
|
||||
startForeGround(100, progress.toInt(),str)
|
||||
if (progress >= 100) {
|
||||
targetUrls.remove(url)
|
||||
currentProcessId = null
|
||||
if((targetUrls?.size ?: 0) > 0) {
|
||||
downloadVideo(targetUrls?.firstOrNull())
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
currentProcessId = null
|
||||
if((targetUrls?.size ?: 0) > 0) {
|
||||
downloadVideo(targetUrls?.firstOrNull())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startForeGround(max : Int = 0 , progress : Int = 0, str : String = "실행중입니다.") {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"BLE 서비스 채널",
|
||||
"BUM'S 서비스",
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
@ -110,7 +181,7 @@ class ForeGroundService : Service() {
|
||||
|
||||
startForeground(NOTIF_ID, NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("BLE 서비스")
|
||||
.setContentText("실행중입니다.")
|
||||
.setContentText(str)
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setSmallIcon(R.drawable.ic_b)
|
||||
.setContentIntent(pendingIntent)
|
||||
@ -118,6 +189,7 @@ class ForeGroundService : Service() {
|
||||
.addAction(android.R.drawable.ic_btn_speak_now,"버스 탐", makeSendMsgAction(1,"돼지 버스 탔다요~!"))
|
||||
.addAction(android.R.drawable.ic_btn_speak_now,"버스 내림", makeSendMsgAction(2,"돼지 버스 내린다요~!"))
|
||||
.setOngoing(true) // 사용자가 알림을 스와이프로 지울 수 없게 만듦
|
||||
.setProgress(max, progress, false)
|
||||
.build())
|
||||
}
|
||||
|
||||
@ -241,6 +313,8 @@ class ForeGroundService : Service() {
|
||||
return mWorkManager
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun sendToI(msg: String) {
|
||||
if (PrefString.telegramSendTarget.get().length > 5) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
@ -256,6 +330,8 @@ class ForeGroundService : Service() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun sendToI(boolean: Boolean) {
|
||||
if (PrefString.telegramSendTarget.get().length > 5) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
@ -280,6 +356,8 @@ class ForeGroundService : Service() {
|
||||
} else Blog.LOGE("sendToI telegram Error Occurred")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@SuppressLint("MissingPermission")
|
||||
fun isConnected(device: BluetoothDevice): Boolean {
|
||||
|
||||
@ -11,6 +11,7 @@ import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@ -41,6 +42,11 @@ import androidx.core.view.isVisible
|
||||
import androidx.work.Worker
|
||||
import bums.lunatic.launcher.LauncherActivity.Companion.getRuntime
|
||||
import bums.lunatic.launcher.R
|
||||
import bums.lunatic.launcher.helpers.ForeGroundService
|
||||
import bums.lunatic.launcher.helpers.ForeGroundService.Companion.ACTION_SENDMSG
|
||||
import bums.lunatic.launcher.helpers.ForeGroundService.Companion.ACTION_VIDEO_DOWNLOAD
|
||||
import bums.lunatic.launcher.helpers.ForeGroundService.Companion.EXTRA_MSGKEY
|
||||
import bums.lunatic.launcher.helpers.ForeGroundService.Companion.EXTRA_TARGET_URL
|
||||
import bums.lunatic.launcher.model.Dotax
|
||||
import bums.lunatic.launcher.model.DotaxArticles
|
||||
import bums.lunatic.launcher.model.getRssData
|
||||
@ -93,7 +99,6 @@ class GeckoWeb : BWebview {
|
||||
constructor(context: Context?) : super(context) {
|
||||
buildWeb()
|
||||
}
|
||||
var decoViews = arrayListOf<View>()
|
||||
override fun setVisibility(visibility: Int) {
|
||||
super.setVisibility(visibility)
|
||||
decoViews.filter { it != null && it.id > -1 && it.id != R.id.dl_video }.forEach { it.visibility = visibility }
|
||||
@ -124,38 +129,7 @@ class GeckoWeb : BWebview {
|
||||
session.mediaDelegate = mediaDelegate
|
||||
session.promptDelegate = promptDelegate
|
||||
session.mediaSessionDelegate = mediaSessionDelegate
|
||||
// session.permissionDelegate = (object : PermissionDelegate {
|
||||
// override fun onContentPermissionRequest(
|
||||
// session: GeckoSession,
|
||||
// perm: PermissionDelegate.ContentPermission
|
||||
// ): GeckoResult<Int?>? {
|
||||
//
|
||||
// return super.onContentPermissionRequest(session, perm)
|
||||
// }
|
||||
//
|
||||
// override fun onAndroidPermissionsRequest(
|
||||
// session: GeckoSession,
|
||||
// permissions: Array<out String?>?,
|
||||
// callback: PermissionDelegate.Callback
|
||||
// ) {
|
||||
// super.onAndroidPermissionsRequest(session, permissions, callback)
|
||||
// }
|
||||
//
|
||||
// override fun onMediaPermissionRequest(
|
||||
// session: GeckoSession,
|
||||
// uri: String,
|
||||
// video: Array<out PermissionDelegate.MediaSource?>?,
|
||||
// audio: Array<out PermissionDelegate.MediaSource?>?,
|
||||
// callback: PermissionDelegate.MediaCallback
|
||||
// ) {
|
||||
//
|
||||
// // 첫 번째 비디오·오디오 소스를 허용
|
||||
//
|
||||
// callback.grant(video?.firstOrNull(), audio?.firstOrNull())
|
||||
// }
|
||||
//
|
||||
//
|
||||
// });
|
||||
|
||||
it.webExtensionController
|
||||
.ensureBuiltIn(extPath, extId)
|
||||
.accept( // Register message delegate for background script
|
||||
@ -420,109 +394,6 @@ class GeckoWeb : BWebview {
|
||||
mOnSave?.saved()
|
||||
}
|
||||
|
||||
fun downloadImage(context: Context, url: Uri, isGif : Boolean = false) {
|
||||
Blog.LOGE("url.lastPathSegment ${url.lastPathSegment}")
|
||||
val fileName = url.host + "_${SimpleDateFormat("yyyyMMddHHmmsss").format(Date())}.${if(isGif){"gif"}else{"jpg"}}"
|
||||
val request = DownloadManager.Request(url)
|
||||
request.setTitle(fileName)
|
||||
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
|
||||
request.setDescription("이미지 다운로드 중...")
|
||||
request.setAllowedOverMetered(true) // 선택 사항 - 데이터 요금제 네트워크에서도 허용
|
||||
request.setVisibleInDownloadsUi(true)
|
||||
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
|
||||
// 네트워크타입, 알림설정 등 옵션 추가 가능
|
||||
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
|
||||
val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
Toast.makeText(context, "다운로드 시작: $fileName", Toast.LENGTH_SHORT).show()
|
||||
val downloadId = dm.enqueue(request)
|
||||
monitorDownloadStatus(dm, downloadId, context)
|
||||
}
|
||||
fun monitorDownloadStatus(dm: DownloadManager, downloadId: Long, context: Context) {
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
// 에러 처리 로직 (선택)
|
||||
exception.printStackTrace()
|
||||
}
|
||||
|
||||
// 백그라운드에서 5초 간격으로 상태 체크
|
||||
CoroutineScope(Dispatchers.IO + handler).launch {
|
||||
while (true) {
|
||||
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||
val cursor = dm.query(query)
|
||||
var downloadFinished = false
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||
when (status) {
|
||||
DownloadManager.STATUS_SUCCESSFUL -> {
|
||||
// 다운로드 성공 처리
|
||||
withContext(Dispatchers.Main) {
|
||||
// UI 갱신 등 메인 스레드 작업 (필요 시)
|
||||
}
|
||||
downloadFinished = true
|
||||
}
|
||||
DownloadManager.STATUS_FAILED -> {
|
||||
val reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON))
|
||||
// 여기에 reason값에 따라 적절한 처리 또는 로깅 수행
|
||||
Log.e("DownloadManager", "Download failed with reason code: $reason")
|
||||
// 다운로드 실패 처리
|
||||
withContext(Dispatchers.Main) {
|
||||
// UI 갱신 등 메인 스레드 작업 (필요 시)
|
||||
}
|
||||
downloadFinished = true
|
||||
}
|
||||
else -> {
|
||||
Blog.LOGE("DownloadManager.STATUS >> ${status}")
|
||||
}
|
||||
// 진행 중, 대기 중 등 기타 상태는 계속 확인
|
||||
}
|
||||
}
|
||||
cursor?.close()
|
||||
|
||||
if (downloadFinished) break
|
||||
|
||||
delay(5000L) // 5초 대기 후 다시 반복
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -562,76 +433,6 @@ class GeckoWeb : BWebview {
|
||||
|
||||
|
||||
|
||||
lateinit var progressDialog: AlertDialog
|
||||
fun showProgressDialog() {
|
||||
val dialogView = layoutInflater.inflate(R.layout.progress_dialog, null)
|
||||
val progressBar = dialogView.findViewById<ProgressBar>(R.id.progressBar)
|
||||
val textProgress = dialogView.findViewById<TextView>(R.id.textProgress)
|
||||
val btn = dialogView.findViewById<android.widget.Button>(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<ProgressBar>(R.id.progressBar)
|
||||
val tv =
|
||||
progressDialog.findViewById<TextView>(R.id.textProgress)
|
||||
pb?.progress = progress.toInt()
|
||||
val btn = progressDialog.findViewById<android.widget.Button>(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()))
|
||||
@ -890,8 +691,6 @@ class GeckoWeb : BWebview {
|
||||
lastedUrl = url
|
||||
}
|
||||
checkIfDownloadable(url)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -281,6 +281,7 @@ internal class RssHome : Fragment() {
|
||||
}
|
||||
}
|
||||
fun searchKeyword() {
|
||||
binding.geckoWeb.visibility = View.GONE
|
||||
// val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext())
|
||||
// builder.setTitle("Keyword")
|
||||
// val viewInflated: View = LayoutInflater.from(requireContext())
|
||||
@ -327,6 +328,7 @@ internal class RssHome : Fragment() {
|
||||
|
||||
|
||||
fun ask() {
|
||||
binding.geckoWeb.visibility = View.GONE
|
||||
val bottomSheet = WebBottomSheet()
|
||||
bottomSheet.listener = object : WebBottomSheet.OnGoToWebListener{
|
||||
override fun enterSearch() {
|
||||
|
||||
@ -54,7 +54,7 @@ class SearchBottomSheet : BottomSheetDialogFragment() {
|
||||
val categoryContainer = view.findViewById<LinearLayout>(R.id.categoryContainer)
|
||||
addVote = view.findViewById<CheckBox>(R.id.add_vote) as CheckBox
|
||||
addRead = view.findViewById<CheckBox>(R.id.add_read) as CheckBox
|
||||
addRead.setOnCheckedChangeListener {v,b->triggerSearchWithDebounce(inputKeyword.text.toString())}
|
||||
addVote.setOnCheckedChangeListener {v,b->triggerSearchWithDebounce(inputKeyword.text.toString())}
|
||||
addRead.setOnCheckedChangeListener {v,b->triggerSearchWithDebounce(inputKeyword.text.toString())}
|
||||
// 카테고리 목록
|
||||
val categories = RssDataType.getAll()
|
||||
@ -66,6 +66,7 @@ class SearchBottomSheet : BottomSheetDialogFragment() {
|
||||
text = category.name
|
||||
tag = category
|
||||
isAllCaps = false
|
||||
this.isSelected = true
|
||||
setBackgroundResource(android.R.drawable.btn_default)
|
||||
setTextColor(Color.WHITE)
|
||||
setBackgroundResource(R.color.tabs_black)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package bums.lunatic.launcher.receiver
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
@ -30,6 +31,7 @@ import bums.lunatic.launcher.model.LocationLog
|
||||
import bums.lunatic.launcher.model.NotificationItem
|
||||
import bums.lunatic.launcher.utils.BitmapConverter
|
||||
import bums.lunatic.launcher.utils.Blog
|
||||
import bums.lunatic.launcher.utils.KakaoPublicTransfer
|
||||
import bums.lunatic.launcher.workers.LocationUpdateService.Companion.inRangeLocation
|
||||
import bums.lunatic.launcher.workers.WorkersDb
|
||||
import com.google.android.gms.location.LocationServices
|
||||
@ -70,6 +72,7 @@ class NLService : NotificationListenerService() {
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||
Blog.LOGE("onNotificationPosted ${sbn}")
|
||||
@ -95,27 +98,53 @@ class NLService : NotificationListenerService() {
|
||||
Blog.LOGE("title >> ${title} text >> ${text} bigText >> ${bigText} extraInfo >> ${extraInfo} subText >> ${subText} conversationTitle >> ${conversationTitle} summaryText >> ${summaryText} verificationText >> ${verificationText}")
|
||||
mHourlyLogWriter?.writeLog("${sbn.packageName}\n${stringBuffer.toString()}")
|
||||
when (sbn.packageName){
|
||||
"com.kakao.talk" -> {
|
||||
|
||||
}
|
||||
"kakaopay.app" -> {
|
||||
if (stringBuffer.contains("모바일") && stringBuffer.contains("교통카드")) {
|
||||
var usePublicTransportation = PrefBoolean.usePublicTransportation.get(false)
|
||||
PrefBoolean.usePublicTransportation.set(!usePublicTransportation)
|
||||
val actionIntent = Intent(this, ForeGroundService::class.java).apply {
|
||||
action = ACTION_SENDMSG
|
||||
putExtra(EXTRA_MSGKEY, "돼지가 대중교통에${if (!usePublicTransportation){" 탑승 "} else {"서 하차"}} 했다요~!") // 전달할 데이터
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(actionIntent)
|
||||
} else {
|
||||
startService(actionIntent)
|
||||
}
|
||||
"com.kakao.taxi" -> {
|
||||
var defaultMsg : StringBuffer? = StringBuffer("돼지 택시 ")
|
||||
if (stringBuffer.contains("택시") && stringBuffer.contains("탑승") && stringBuffer.contains("완료")) {
|
||||
defaultMsg?.append("탔다요~!")
|
||||
}else if(stringBuffer.contains("택시") && stringBuffer.contains("자동결제") && stringBuffer.contains("물건")) {
|
||||
defaultMsg?.append("거의 다 왔다요~!")
|
||||
}else if(stringBuffer.contains("택시") && stringBuffer.contains("도착") && stringBuffer.contains("선택")) {
|
||||
defaultMsg?.append("내린다요~!")
|
||||
} else {
|
||||
defaultMsg = null
|
||||
}
|
||||
defaultMsg?.let {
|
||||
makeMsgByTransferInfomation(it)
|
||||
}
|
||||
}
|
||||
"com.kakao.talk" -> {
|
||||
if (stringBuffer.contains("카카오페이") && stringBuffer.contains("모바일") && stringBuffer.contains("교통카드") && stringBuffer.contains("사용 내역")) {
|
||||
var usePublicTransportation = PrefBoolean.usePublicTransportation.get(false)
|
||||
PrefBoolean.usePublicTransportation.set(!usePublicTransportation)
|
||||
var defaultMsg = StringBuffer("돼지가 대중교통에${if (!usePublicTransportation){" 탑승 "} else {"서 하차"}} 했다요~!")
|
||||
KakaoPublicTransfer(stringBuffer.toString()).let {
|
||||
defaultMsg.append("\n${it.transportType}(${it.transportName})")
|
||||
defaultMsg.append("\n${it.dateTime}")
|
||||
}
|
||||
makeMsgByTransferInfomation(defaultMsg)
|
||||
}
|
||||
}
|
||||
"kakaopay.app" -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
fun makeMsgByTransferInfomation(stringBuffer : StringBuffer) {
|
||||
val actionIntent = Intent(this, ForeGroundService::class.java).apply {
|
||||
action = ACTION_SENDMSG
|
||||
putExtra(EXTRA_MSGKEY, stringBuffer.toString()) // 전달할 데이터
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(actionIntent)
|
||||
} else {
|
||||
startService(actionIntent)
|
||||
}
|
||||
pushLocation(this)
|
||||
}
|
||||
|
||||
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
fun pushLocation(context: Context) {
|
||||
try {
|
||||
|
||||
@ -28,12 +28,14 @@ import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import bums.lunatic.launcher.LauncherActivity
|
||||
import bums.lunatic.launcher.LauncherActivity.Companion.getRuntime
|
||||
import bums.lunatic.launcher.R
|
||||
import bums.lunatic.launcher.tokiz.common.PairArray
|
||||
@ -404,6 +406,21 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface {
|
||||
} else {
|
||||
lastedUrl = url
|
||||
}
|
||||
binding.menuWeb.checkIfDownloadable(url)
|
||||
binding.menuWeb.decoViews.filter { it != null && it.id > -1 }.forEach {
|
||||
if (it != null && it.id > -1) {
|
||||
if (it.id == R.id.back) {
|
||||
it.setOnClickListener { session.goBack() }
|
||||
} else if (it.id == R.id.current_address) {
|
||||
(it as? TextView)?.let {
|
||||
it.tag = currentTitle
|
||||
it.text = url
|
||||
}
|
||||
}else if (it.id == R.id.reload) {
|
||||
it.setOnClickListener { session.reload() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
completePageLoad(LastInfo().apply {
|
||||
this.pageUrl = url?.toUri()?.path ?: getLastedDoamin() ?: ""
|
||||
@ -635,6 +652,15 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface {
|
||||
}
|
||||
val nullCursor = PointerIcon.getSystemIcon(context!!, PointerIcon.TYPE_NULL)
|
||||
binding.root.setPointerIcon(nullCursor)
|
||||
|
||||
|
||||
binding.menuWeb
|
||||
(activity as? LauncherActivity)?.let { activity ->
|
||||
binding.menuWeb.decoViews.add(activity.findViewById<TextView>(R.id.current_address))
|
||||
binding.menuWeb.decoViews.add(activity.findViewById<ImageButton>(R.id.back))
|
||||
binding.menuWeb.decoViews.add(activity.findViewById<ImageButton>(R.id.reload))
|
||||
binding.menuWeb.decoViews.add(activity.findViewById<ImageButton>(R.id.dl_video))
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@ -1500,6 +1526,8 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface {
|
||||
}
|
||||
|
||||
|
||||
|
||||
open fun back() {
|
||||
// binding.menuWeb.session?.goBack()
|
||||
}
|
||||
|
||||
}
|
||||
60
app/src/main/kotlin/bums/lunatic/launcher/tokiz/YouTube.kt
Normal file
60
app/src/main/kotlin/bums/lunatic/launcher/tokiz/YouTube.kt
Normal file
@ -0,0 +1,60 @@
|
||||
package bums.lunatic.launcher.tokiz
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import bums.lunatic.launcher.tokiz.common.TouchArea
|
||||
import bums.lunatic.launcher.tokiz.view.PagedTextViewInterface
|
||||
|
||||
class YouTube : BaseToki(){
|
||||
override val contentsType = "youtube"
|
||||
override var lastNumber : Int = 143
|
||||
override val webcontentsName : String = "youtube"
|
||||
override val afterDot = "com"
|
||||
override fun getLastedDoamin(): String {
|
||||
return String.format("https://%s.%s", webcontentsName, afterDot)
|
||||
}
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
loadLastInfo()
|
||||
}
|
||||
|
||||
override fun back() {
|
||||
binding.menuWeb.session?.goBack()
|
||||
}
|
||||
|
||||
override fun onTouch(touchArea: TouchArea) {
|
||||
}
|
||||
|
||||
override fun onTimeoverTouch() {
|
||||
}
|
||||
|
||||
override fun onSwipeLeft(touchCount: Int) {
|
||||
}
|
||||
|
||||
override fun onSwipeRight(touchCount: Int) {
|
||||
}
|
||||
|
||||
override fun onSwipeDown(touchCount: Int) {
|
||||
}
|
||||
|
||||
override fun onSwipeUp(touchCount: Int) {
|
||||
}
|
||||
|
||||
override fun onLongClick() {
|
||||
}
|
||||
}
|
||||
@ -2,15 +2,25 @@ package bums.lunatic.launcher.tokiz.view
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.PointerIcon
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import bums.lunatic.launcher.R
|
||||
import bums.lunatic.launcher.helpers.ForeGroundService
|
||||
import bums.lunatic.launcher.helpers.ForeGroundService.Companion.ACTION_VIDEO_DOWNLOAD
|
||||
import bums.lunatic.launcher.helpers.ForeGroundService.Companion.EXTRA_TARGET_URL
|
||||
import bums.lunatic.launcher.tokiz.common.TouchArea
|
||||
import bums.lunatic.launcher.utils.Blog
|
||||
import bums.lunatic.launcher.utils.SimpleFingerGestures
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
|
||||
import org.mozilla.geckoview.GeckoView
|
||||
import java.util.Base64
|
||||
|
||||
@ -23,6 +33,7 @@ enum class JxEvent {
|
||||
}
|
||||
typealias JxInteface = (JxEvent)->Unit
|
||||
open class BWebview : GeckoView {
|
||||
var decoViews = arrayListOf<View>()
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
constructor(context: Context?) : super(context) {
|
||||
this.setOnTouchListener { v, event ->
|
||||
@ -57,8 +68,53 @@ open class BWebview : GeckoView {
|
||||
this.setPointerIcon(nullCursor)
|
||||
}
|
||||
}
|
||||
fun videoDlownLoad(videoUrl : String) {
|
||||
val actionIntent = Intent(context, ForeGroundService::class.java).apply {
|
||||
action = ACTION_VIDEO_DOWNLOAD
|
||||
putExtra(EXTRA_TARGET_URL, videoUrl) // 전달할 데이터
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(actionIntent)
|
||||
} else {
|
||||
context.startService(actionIntent)
|
||||
}
|
||||
}
|
||||
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
|
||||
}}}
|
||||
}
|
||||
}
|
||||
}
|
||||
val mSimpleFingerGestures = SimpleFingerGestures(omfgl = object : SimpleFingerGestures.OnFingerGestureListener{
|
||||
|
||||
override fun onSwipeUp(
|
||||
|
||||
@ -8,6 +8,27 @@ import android.provider.ContactsContract.PhoneLookup
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
|
||||
data class KakaoPayTransitHistory(
|
||||
val dateTime: String,
|
||||
val transportType: String,
|
||||
val transportName: String,
|
||||
val balance: Int
|
||||
)
|
||||
|
||||
fun KakaoPublicTransfer(raw : String) : KakaoPayTransitHistory {
|
||||
val dateTimeRegex = Regex("""사용일시\s*:\s*([^\n]+)""")
|
||||
val transportTypeRegex = Regex("""이용수단\s*:\s*([^\n]+)""")
|
||||
val transportNameRegex = Regex("""이용수단명\s*:\s*([^\n]+)""")
|
||||
val balanceRegex = Regex("""잔액\s*:\s*([\d,]+)""")
|
||||
|
||||
val dateTime = dateTimeRegex.find(raw)?.groups?.get(1)?.value?.trim() ?: ""
|
||||
val transportType = transportTypeRegex.find(raw)?.groups?.get(1)?.value?.trim() ?: ""
|
||||
val transportName = transportNameRegex.find(raw)?.groups?.get(1)?.value?.trim() ?: ""
|
||||
val balance = balanceRegex.find(raw)?.groups?.get(1)?.value?.replace(",", "")?.toInt() ?: 0
|
||||
|
||||
return KakaoPayTransitHistory(dateTime, transportType, transportName, balance)
|
||||
}
|
||||
|
||||
fun afterDay(date: Long): Long {
|
||||
val cal: Calendar = Calendar.getInstance()
|
||||
cal.setTime(Date(date))
|
||||
|
||||
@ -33,9 +33,9 @@
|
||||
android:src="@drawable/back_vector"
|
||||
android:tint="@color/white"
|
||||
android:foregroundTint="@color/white"
|
||||
android:layout_width="40dp"
|
||||
android:layout_width="@dimen/main_top_height"
|
||||
tools:ignore="ContentDescription"
|
||||
android:layout_height="40dp" />
|
||||
android:layout_height="@dimen/main_top_height" />
|
||||
<ImageButton
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/fragment_container"
|
||||
@ -48,9 +48,9 @@
|
||||
android:src="@drawable/ic_refresh"
|
||||
android:tint="@color/white"
|
||||
android:foregroundTint="@color/white"
|
||||
android:layout_width="40dp"
|
||||
android:layout_width="@dimen/main_top_height"
|
||||
tools:ignore="ContentDescription"
|
||||
android:layout_height="40dp" />
|
||||
android:layout_height="@dimen/main_top_height" />
|
||||
<TextView
|
||||
android:text="asdasdsadasd"
|
||||
android:id="@+id/current_address"
|
||||
@ -63,7 +63,8 @@
|
||||
android:ellipsize="middle"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="40dp"/>
|
||||
android:layout_height="@dimen/main_top_height"/>
|
||||
"/>
|
||||
<ImageButton
|
||||
app:layout_constraintTop_toTopOf="@id/back"
|
||||
app:layout_constraintRight_toLeftOf="@id/share"
|
||||
@ -76,9 +77,9 @@
|
||||
android:tint="@color/white"
|
||||
android:foregroundTint="@color/white"
|
||||
android:src="@drawable/dl_vid"
|
||||
android:layout_width="40dp"
|
||||
android:layout_width="@dimen/main_top_height"
|
||||
tools:ignore="ContentDescription"
|
||||
android:layout_height="40dp" />
|
||||
android:layout_height="@dimen/main_top_height" />
|
||||
<ImageButton
|
||||
app:layout_constraintTop_toTopOf="@id/back"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
@ -92,9 +93,9 @@
|
||||
android:tint="@color/white"
|
||||
android:foregroundTint="@color/white"
|
||||
android:src="@drawable/ic_share"
|
||||
android:layout_width="40dp"
|
||||
android:layout_width="@dimen/main_top_height"
|
||||
tools:ignore="ContentDescription"
|
||||
android:layout_height="40dp" />
|
||||
android:layout_height="@dimen/main_top_height" />
|
||||
<bums.lunatic.launcher.view.FloatingActionMenu
|
||||
android:id="@+id/floating_action_menu"
|
||||
android:layout_margin="5dp"
|
||||
@ -138,6 +139,14 @@
|
||||
android:onClick="floatClick"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"/>
|
||||
<bums.lunatic.launcher.view.FloatingActionButton
|
||||
app:fab_label="youtube"
|
||||
android:id="@+id/youtube"
|
||||
app:fab_showShadow="true"
|
||||
app:fab_size="mini"
|
||||
android:onClick="floatClick"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"/>
|
||||
<bums.lunatic.launcher.view.FloatingActionButton
|
||||
app:fab_label="perplexity"
|
||||
android:id="@+id/perplexity"
|
||||
|
||||
@ -33,9 +33,9 @@
|
||||
android:visibility="visible"
|
||||
android:background="@null"
|
||||
android:src="@drawable/saved"
|
||||
android:layout_width="40dp"
|
||||
android:layout_width="@dimen/main_top_height"
|
||||
tools:ignore="ContentDescription"
|
||||
android:layout_height="40dp" />
|
||||
android:layout_height="@dimen/main_top_height" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/hide"
|
||||
@ -46,11 +46,11 @@
|
||||
android:layout_marginLeft="12dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@null"
|
||||
android:layout_width="40dp"
|
||||
android:layout_width="@dimen/main_top_height"
|
||||
android:visibility="visible"
|
||||
android:adjustViewBounds="true"
|
||||
tools:ignore="ContentDescription"
|
||||
android:layout_height="40dp"
|
||||
android:layout_height="@dimen/main_top_height"
|
||||
/>
|
||||
|
||||
|
||||
@ -62,9 +62,9 @@
|
||||
android:tintMode="multiply"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@null"
|
||||
android:layout_width="40dp"
|
||||
android:layout_width="@dimen/main_top_height"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_height="40dp"
|
||||
android:layout_height="@dimen/main_top_height"
|
||||
app:tint="@color/white"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
@ -75,10 +75,10 @@
|
||||
android:src="@drawable/bookmark"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@null"
|
||||
android:layout_width="40dp"
|
||||
android:layout_width="@dimen/main_top_height"
|
||||
android:adjustViewBounds="true"
|
||||
tools:ignore="ContentDescription"
|
||||
android:layout_height="40dp"/>
|
||||
android:layout_height="@dimen/main_top_height"/>
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user