...
This commit is contained in:
parent
b73fbf2160
commit
614e244be7
@ -359,7 +359,10 @@ function toast(msg) {
|
|||||||
port.postMessage(JSON.stringify({type:"MSG",msg:msg}));
|
port.postMessage(JSON.stringify({type:"MSG",msg:msg}));
|
||||||
}
|
}
|
||||||
|
|
||||||
var mainContentsEl = null
|
var mainContentsEl = null;
|
||||||
|
let lastState = null; // 이전 상태 저장 (-1, 0, 1)
|
||||||
|
let throttleTimer = null;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
const currentUrl = location.href;
|
const currentUrl = location.href;
|
||||||
@ -382,6 +385,40 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
// toast("connect port on " + location.href);
|
// toast("connect port on " + location.href);
|
||||||
time1 = setTimeout(autoScrollAndSave(false), 3500);
|
time1 = setTimeout(autoScrollAndSave(false), 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
window.onscroll = function() {
|
||||||
|
if (throttleTimer) return; // 이미 타이머가 동작 중이면 무시
|
||||||
|
|
||||||
|
throttleTimer = setTimeout(() => {
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
const innerHeight = window.innerHeight;
|
||||||
|
const offsetHeight = document.body.offsetHeight;
|
||||||
|
|
||||||
|
let currentState;
|
||||||
|
|
||||||
|
if (scrollY <= 2) {
|
||||||
|
currentState = -1; // 최상단
|
||||||
|
} else if ((innerHeight + scrollY) >= offsetHeight - 10) {
|
||||||
|
currentState = 1; // 최하단
|
||||||
|
} else {
|
||||||
|
currentState = 0; // 중간
|
||||||
|
}
|
||||||
|
|
||||||
|
// 💡 상태가 바뀌었을 때만 앱으로 전송
|
||||||
|
if (currentState !== lastState) {
|
||||||
|
lastState = currentState;
|
||||||
|
sendMessage({
|
||||||
|
type: "SCROLL_STATE",
|
||||||
|
value: currentState
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throttleTimer = null;
|
||||||
|
}, 150); // 0.15초 간격으로 체크 (사용성에 따라 조절 가능)
|
||||||
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
const keywords = ["youtube", "mojeek"];
|
const keywords = ["youtube", "mojeek"];
|
||||||
|
|||||||
@ -218,8 +218,10 @@ open class LauncherActivity : CommonActivity() {
|
|||||||
|
|
||||||
fun onSwipeRight() {
|
fun onSwipeRight() {
|
||||||
startActivity(Intent(this, NeoRssActivity::class.java).apply {
|
startActivity(Intent(this, NeoRssActivity::class.java).apply {
|
||||||
|
// 이미 실행 중인 액티비티가 있다면 스택의 맨 위로 올리고 재사용합니다.
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||||
|
// 기존에 있던 SINGLE_TOP과 함께 사용하면 중복 생성을 더 확실히 방지합니다.
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,10 +269,14 @@ open class LauncherActivity : CommonActivity() {
|
|||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
try {
|
try {
|
||||||
|
supportFragmentManager.findFragmentByTag(AppDrawerBottomSheet.TAG)?.let {
|
||||||
|
try { if (it is AppDrawerBottomSheet) it.dismissAllowingStateLoss() } catch (e : Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
val fragment = supportFragmentManager.findFragmentByTag(AppDrawerBottomSheet.TAG)
|
val fragment = supportFragmentManager.findFragmentByTag(AppDrawerBottomSheet.TAG)
|
||||||
Blog.LOGE("fragment >>> $fragment")
|
Blog.LOGE("fragment >>> $fragment")
|
||||||
if (fragment is AppDrawerBottomSheet) {
|
if (fragment is AppDrawerBottomSheet) {
|
||||||
fragment.dismiss()
|
try { fragment.dismiss() } catch (e : Exception) {}
|
||||||
}
|
}
|
||||||
if (intent?.action?.equals(Intent.ACTION_MAIN) == true && intent.categories.contains(
|
if (intent?.action?.equals(Intent.ACTION_MAIN) == true && intent.categories.contains(
|
||||||
Intent.CATEGORY_HOME)) {
|
Intent.CATEGORY_HOME)) {
|
||||||
@ -357,9 +363,17 @@ open class LauncherActivity : CommonActivity() {
|
|||||||
|
|
||||||
|
|
||||||
fun showAppDrawer() {
|
fun showAppDrawer() {
|
||||||
|
// 이미 화면에 떠 있는지 확인
|
||||||
|
val existingFragment = supportFragmentManager.findFragmentByTag(AppDrawerBottomSheet.TAG)
|
||||||
|
if (existingFragment != null && existingFragment.isAdded) {
|
||||||
|
return // 이미 존재하면 새로 띄우지 않음
|
||||||
|
}
|
||||||
|
|
||||||
val bottomSheet = AppDrawerBottomSheet.newInstance()
|
val bottomSheet = AppDrawerBottomSheet.newInstance()
|
||||||
bottomSheet.show(supportFragmentManager, AppDrawerBottomSheet.TAG)
|
bottomSheet.show(supportFragmentManager, AppDrawerBottomSheet.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private lateinit var homeGestureDetector: androidx.core.view.GestureDetectorCompat
|
private lateinit var homeGestureDetector: androidx.core.view.GestureDetectorCompat
|
||||||
|
|
||||||
@SuppressLint("NewApi", "MissingPermission", "ClickableViewAccessibility")
|
@SuppressLint("NewApi", "MissingPermission", "ClickableViewAccessibility")
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package bums.lunatic.launcher.apps
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.app.SearchManager
|
import android.app.SearchManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
@ -129,11 +130,23 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
|
|||||||
val displayList = categoryMap.keys.toList()
|
val displayList = categoryMap.keys.toList()
|
||||||
|
|
||||||
// 2. 어댑터 설정 (기본 안드로이드 레이아웃 사용)
|
// 2. 어댑터 설정 (기본 안드로이드 레이아웃 사용)
|
||||||
val adapter = ArrayAdapter(
|
val adapter = object : ArrayAdapter<String>(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
R.layout.spinner_item_dark,
|
R.layout.spinner_item_dark,
|
||||||
displayList
|
displayList
|
||||||
)
|
){
|
||||||
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
val view = super.getView(position, convertView, parent)
|
||||||
|
view.setBackgroundColor(Color.parseColor("#000000")) // 펼쳐진 리스트의 배경
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
val view = super.getDropDownView(position, convertView, parent)
|
||||||
|
view.setBackgroundColor(Color.parseColor("#000000")) // 펼쳐진 리스트의 배경
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
}
|
||||||
adapter.setDropDownViewResource(R.layout.spinner_item_dark)
|
adapter.setDropDownViewResource(R.layout.spinner_item_dark)
|
||||||
binding.categorySpinner.adapter = adapter
|
binding.categorySpinner.adapter = adapter
|
||||||
|
|
||||||
@ -147,6 +160,8 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
|
|||||||
fetchApps(binding.searchInput.text.toString())
|
fetchApps(binding.searchInput.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import bums.lunatic.launcher.utils.Blog
|
|||||||
import bums.lunatic.launcher.workers.TaskAggregator
|
import bums.lunatic.launcher.workers.TaskAggregator
|
||||||
import com.yausername.youtubedl_android.YoutubeDL
|
import com.yausername.youtubedl_android.YoutubeDL
|
||||||
import com.yausername.youtubedl_android.YoutubeDLRequest
|
import com.yausername.youtubedl_android.YoutubeDLRequest
|
||||||
|
import io.realm.kotlin.internal.interop.sync.CancellableTimer
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -45,7 +46,11 @@ import okhttp3.ResponseBody
|
|||||||
import java.io.File
|
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.media.AudioManager
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
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 {
|
||||||
@ -63,6 +68,19 @@ class AggregatedNewsWorker(private val context: Context, params: WorkerParameter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class VibrationWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as android.os.Vibrator
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||||
|
vibrator.vibrate(android.os.VibrationEffect.createOneShot(1000, android.os.VibrationEffect.DEFAULT_AMPLITUDE))
|
||||||
|
} else {
|
||||||
|
vibrator.vibrate(1000)
|
||||||
|
}
|
||||||
|
// 필요 시 여기서 상단 알림(Notification)을 한 번 더 띄워줄 수 있습니다.
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ForeGroundService : Service() {
|
class ForeGroundService : Service() {
|
||||||
companion object {
|
companion object {
|
||||||
val ACTION_SENDMSG = "ACTION_SEND_TO_LOVE"
|
val ACTION_SENDMSG = "ACTION_SEND_TO_LOVE"
|
||||||
@ -70,6 +88,7 @@ class ForeGroundService : Service() {
|
|||||||
|
|
||||||
val ACTION_VIDEO_DOWNLOAD = "ACTION_YTURL_DOWNLOAD"
|
val ACTION_VIDEO_DOWNLOAD = "ACTION_YTURL_DOWNLOAD"
|
||||||
val EXTRA_TARGET_URL = "ACTION_SEND_TO_LOVE"
|
val EXTRA_TARGET_URL = "ACTION_SEND_TO_LOVE"
|
||||||
|
val ACTION_SIT_DOWN = "ACTION_SIT_DOWN"
|
||||||
val targetUrls = arrayListOf<String>()
|
val targetUrls = arrayListOf<String>()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,9 +108,11 @@ 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()
|
||||||
|
startForeGround()
|
||||||
}
|
}
|
||||||
|
|
||||||
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}")
|
||||||
when(intent?.action) {
|
when(intent?.action) {
|
||||||
ACTION_SENDMSG -> {
|
ACTION_SENDMSG -> {
|
||||||
intent?.getStringExtra(EXTRA_MSGKEY)?.let {
|
intent?.getStringExtra(EXTRA_MSGKEY)?.let {
|
||||||
@ -105,9 +126,27 @@ class ForeGroundService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
ACTION_SIT_DOWN -> {
|
||||||
|
val sitDownRequest = OneTimeWorkRequestBuilder<VibrationWorker>()
|
||||||
|
.setInitialDelay(45, TimeUnit.MINUTES) // 45분 지연
|
||||||
|
.build()
|
||||||
|
|
||||||
|
WorkManager.getInstance(this).enqueueUniqueWork(
|
||||||
|
"SitDownVibration",
|
||||||
|
ExistingWorkPolicy.REPLACE, // 새로 누를 때마다 타이머 초기화 (원치 않으면 KEEP)
|
||||||
|
sitDownRequest
|
||||||
|
)
|
||||||
|
// 사용자 확인을 위해 알림 텍스트 변경 가능
|
||||||
|
startForeGround(0, 0, "45분 뒤에 진동 알림이 예약되었습니다.")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
if(intent?.action?.equals(ACTION_SIT_DOWN) == false) {
|
||||||
startForeGround()
|
startForeGround()
|
||||||
|
}
|
||||||
|
}
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +200,8 @@ class ForeGroundService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startForeGround(max : Int = 0 , progress : Int = 0, str : String = "실행중입니다.") {
|
fun startForeGround(max : Int = 0 , progress : Int = 0, str : String? = "실행중입니다.") {
|
||||||
|
Blog.LOGE(str?:"")
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
@ -189,11 +229,20 @@ class ForeGroundService : Service() {
|
|||||||
.setSmallIcon(R.drawable.ic_b)
|
.setSmallIcon(R.drawable.ic_b)
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.addAction(android.R.drawable.ic_btn_speak_now,"퇴근", makeSendMsgAction(0,"돼지 퇴근했다요~!"))
|
.addAction(android.R.drawable.ic_btn_speak_now,"퇴근", makeSendMsgAction(0,"돼지 퇴근했다요~!"))
|
||||||
|
.addAction(android.R.drawable.ic_menu_directions, "앉음", makeSitDownAction())
|
||||||
.setOngoing(true) // 사용자가 알림을 스와이프로 지울 수 없게 만듦
|
.setOngoing(true) // 사용자가 알림을 스와이프로 지울 수 없게 만듦
|
||||||
.setProgress(max, progress, false)
|
.setProgress(max, progress, false)
|
||||||
.build())
|
.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun makeSitDownAction(): PendingIntent {
|
||||||
|
val intent = Intent(this, ForeGroundService::class.java).apply {
|
||||||
|
action = ACTION_SIT_DOWN
|
||||||
|
}
|
||||||
|
return PendingIntent.getService(this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun makeSendMsgAction(code : Int, msg : String) : PendingIntent {
|
fun makeSendMsgAction(code : Int, msg : String) : PendingIntent {
|
||||||
val actionIntent = Intent(this, ForeGroundService::class.java).apply {
|
val actionIntent = Intent(this, ForeGroundService::class.java).apply {
|
||||||
action = ACTION_SENDMSG
|
action = ACTION_SENDMSG
|
||||||
@ -356,6 +405,66 @@ class ForeGroundService : Service() {
|
|||||||
if (context == null) return
|
if (context == null) return
|
||||||
if (ActivityCompat.checkSelfPermission(context!!, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return
|
if (ActivityCompat.checkSelfPermission(context!!, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return
|
||||||
getPairedDevices()
|
getPairedDevices()
|
||||||
|
|
||||||
|
if (action == BluetoothDevice.ACTION_ACL_CONNECTED) {
|
||||||
|
val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
||||||
|
|
||||||
|
// 연결된 기기가 오디오(이어폰, 스피커, 차량 블루투스)인지 확인
|
||||||
|
val majorClass = device?.bluetoothClass?.majorDeviceClass
|
||||||
|
if (majorClass == BluetoothClass.Device.Major.AUDIO_VIDEO) {
|
||||||
|
Blog.LOGE("Audio Bluetooth Connected: ${device?.name ?: "Unknown"}")
|
||||||
|
if ("BUMz pod".equals(device?.name)) {
|
||||||
|
launchAppleMusic(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchAppleMusic(context: Context) {
|
||||||
|
try {
|
||||||
|
val appleMusicPackage = "com.apple.android.music"
|
||||||
|
val launchIntent = context.packageManager.getLaunchIntentForPackage(appleMusicPackage)
|
||||||
|
|
||||||
|
if (launchIntent != null) {
|
||||||
|
// 서비스(백그라운드)에서 화면을 띄우려면 NEW_TASK 플래그가 필수입니다.
|
||||||
|
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||||
|
context.startActivity(launchIntent)
|
||||||
|
Blog.LOGE("애플뮤직 자동 실행 완료")
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
delay(1500) // 1.5초 대기 (기기 속도에 따라 1000~2000 사이로 조절 가능)
|
||||||
|
|
||||||
|
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
val eventTime = SystemClock.uptimeMillis()
|
||||||
|
|
||||||
|
// 재생 버튼 누름 (ACTION_DOWN)
|
||||||
|
val downEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY, 0)
|
||||||
|
audioManager.dispatchMediaKeyEvent(downEvent)
|
||||||
|
|
||||||
|
// 재생 버튼 뗌 (ACTION_UP)
|
||||||
|
val upEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY, 0)
|
||||||
|
audioManager.dispatchMediaKeyEvent(upEvent)
|
||||||
|
|
||||||
|
Blog.LOGE("애플뮤직 미디어 재생(Play) 명령 전송 완료 🎵")
|
||||||
|
delay(300) // 시스템이 재생 명령을 처리할 수 있도록 아주 잠깐 대기
|
||||||
|
|
||||||
|
// 💡 3. 홈 화면(런처)으로 즉시 강제 복귀
|
||||||
|
val homeIntent = Intent(Intent.ACTION_MAIN).apply {
|
||||||
|
addCategory(Intent.CATEGORY_HOME)
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}
|
||||||
|
context.startActivity(homeIntent)
|
||||||
|
Blog.LOGE("런처(홈) 화면으로 복귀 완료")
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Blog.LOGE("애플뮤직 앱이 기기에 설치되어 있지 않습니다.")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Blog.LOGE("애플뮤직 실행 중 오류 발생: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -4,14 +4,11 @@ import CustomVideoNodeRenderer
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
|
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.style.RelativeSizeSpan
|
import android.text.style.RelativeSizeSpan
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
@ -21,7 +18,6 @@ import android.view.KeyEvent
|
|||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.PointerIcon
|
import android.view.PointerIcon
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CheckBox
|
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
@ -30,7 +26,6 @@ import android.widget.RadioGroup
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContentProviderCompat.requireContext
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import bums.lunatic.launcher.BookmarkUploader
|
import bums.lunatic.launcher.BookmarkUploader
|
||||||
@ -56,30 +51,28 @@ import com.yausername.youtubedl_android.YoutubeDLRequest
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.json.JSONException
|
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.mozilla.gecko.util.ThreadUtils
|
import org.mozilla.gecko.util.ThreadUtils
|
||||||
import org.mozilla.geckoview.GeckoResult
|
import org.mozilla.geckoview.GeckoResult
|
||||||
import org.mozilla.geckoview.GeckoSession
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import org.mozilla.geckoview.GeckoSession.CompositorScrollDelegate
|
||||||
import org.mozilla.geckoview.GeckoSessionSettings
|
import org.mozilla.geckoview.GeckoSessionSettings
|
||||||
import org.mozilla.geckoview.GeckoView
|
import org.mozilla.geckoview.GeckoView
|
||||||
import org.mozilla.geckoview.MediaSession
|
import org.mozilla.geckoview.MediaSession
|
||||||
|
import org.mozilla.geckoview.PanZoomController
|
||||||
|
import org.mozilla.geckoview.ScreenLength
|
||||||
import org.mozilla.geckoview.WebExtension
|
import org.mozilla.geckoview.WebExtension
|
||||||
import org.mozilla.geckoview.WebExtension.MessageDelegate
|
import org.mozilla.geckoview.WebExtension.MessageDelegate
|
||||||
import org.mozilla.geckoview.WebExtension.PortDelegate
|
import org.mozilla.geckoview.WebExtension.PortDelegate
|
||||||
import org.mozilla.geckoview.WebExtensionController.AddonManagerDelegate
|
import org.mozilla.geckoview.WebExtensionController.AddonManagerDelegate
|
||||||
import org.mozilla.geckoview.WebRequestError
|
|
||||||
import org.mozilla.geckoview.WebResponse
|
import org.mozilla.geckoview.WebResponse
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.InputStream
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import kotlin.jvm.java
|
|
||||||
|
|
||||||
// BWebview와 GeckoWeb을 통합한 클래스
|
// BWebview와 GeckoWeb을 통합한 클래스
|
||||||
open class GeckoWeb @JvmOverloads constructor(
|
open class GeckoWeb @JvmOverloads constructor(
|
||||||
@ -91,35 +84,29 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
// 1. 세션 상태를 저장할 SharedPreferences 키
|
// 1. 세션 상태를 저장할 SharedPreferences 키
|
||||||
private val PREF_SESSION_STATE = "gecko_session_state"
|
private val PREF_SESSION_STATE = "gecko_session_state"
|
||||||
|
|
||||||
// 2. 현재 세션 상태 저장 메서드
|
var sessionTag: String = "default" // 프레그먼트에서 설정할 고유 키
|
||||||
|
|
||||||
fun saveCurrentSessionState() {
|
fun saveCurrentSessionState() {
|
||||||
|
|
||||||
lastSessionState?.let { state ->
|
lastSessionState?.let { state ->
|
||||||
// SessionState.toString()은 내부적으로 JSON 문자열을 반환합니다.
|
|
||||||
val stateJson = state.toString()
|
val stateJson = state.toString()
|
||||||
|
// 💡 키 이름에 sessionTag를 포함시켜 분리 저장
|
||||||
context.getSharedPreferences("GeckoPrefs", Context.MODE_PRIVATE)
|
context.getSharedPreferences("GeckoPrefs", Context.MODE_PRIVATE)
|
||||||
.edit()
|
.edit()
|
||||||
.putString("gecko_session_state", stateJson)
|
.putString("gecko_session_state_$sessionTag", stateJson)
|
||||||
.apply()
|
.apply()
|
||||||
|
Log.d("GeckoWeb", "Saved State for $sessionTag")
|
||||||
Log.d("GeckoWeb", "Session State Saved: $stateJson")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// // 3. 저장된 세션 상태 복구 메서드
|
fun restoreSessionState() {
|
||||||
fun restoreSessionState(session: GeckoSession) {
|
// 💡 저장할 때와 동일한 태그로 불러오기
|
||||||
val stateJson = context.getSharedPreferences("GeckoPrefs", Context.MODE_PRIVATE)
|
val stateJson = context.getSharedPreferences("GeckoPrefs", Context.MODE_PRIVATE)
|
||||||
.getString("gecko_session_state", null)
|
.getString("gecko_session_state_$sessionTag", null)
|
||||||
|
|
||||||
if (!stateJson.isNullOrEmpty()) {
|
if (!stateJson.isNullOrEmpty()) {
|
||||||
// 문자열에서 SessionState 객체 생성
|
|
||||||
val state = GeckoSession.SessionState.fromString(stateJson)
|
val state = GeckoSession.SessionState.fromString(stateJson)
|
||||||
Blog.LOGE("Restored state >>> ${state}")
|
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
|
session?.restoreState(state)
|
||||||
session.restoreState(state)
|
|
||||||
Log.d("GeckoWeb", "Session State Restored")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -339,6 +326,7 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
R.id.reload -> view.setOnClickListener { session.reload() }
|
R.id.reload -> view.setOnClickListener { session.reload() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// scrollState = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
|
override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
|
||||||
@ -433,7 +421,7 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
buildWeb()
|
buildWeb()
|
||||||
}
|
}
|
||||||
|
var lastScrollY = 0.0f
|
||||||
private fun buildWeb() {
|
private fun buildWeb() {
|
||||||
getRuntime()?.let { runtime ->
|
getRuntime()?.let { runtime ->
|
||||||
val sessionSettings = GeckoSessionSettings.Builder()
|
val sessionSettings = GeckoSessionSettings.Builder()
|
||||||
@ -446,7 +434,7 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
.build()
|
.build()
|
||||||
val session = GeckoSession(sessionSettings)
|
val session = GeckoSession(sessionSettings)
|
||||||
|
|
||||||
restoreSessionState(session)
|
|
||||||
session.open(runtime)
|
session.open(runtime)
|
||||||
this.setSession(session)
|
this.setSession(session)
|
||||||
|
|
||||||
@ -457,6 +445,15 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
session.mediaDelegate = mediaDelegate
|
session.mediaDelegate = mediaDelegate
|
||||||
session.promptDelegate = promptDelegate
|
session.promptDelegate = promptDelegate
|
||||||
session.mediaSessionDelegate = mediaSessionDelegate
|
session.mediaSessionDelegate = mediaSessionDelegate
|
||||||
|
session.compositorScrollDelegate = object : CompositorScrollDelegate {
|
||||||
|
override fun onScrollChanged(
|
||||||
|
session: GeckoSession,
|
||||||
|
update: GeckoSession.ScrollPositionUpdate
|
||||||
|
) {
|
||||||
|
super.onScrollChanged(session, update)
|
||||||
|
lastScrollY = update.scrollY
|
||||||
|
}
|
||||||
|
}
|
||||||
runtime.settings.loginAutofillEnabled = true
|
runtime.settings.loginAutofillEnabled = true
|
||||||
|
|
||||||
runtime.webExtensionController.setAddonManagerDelegate(addonManagerDelegate)
|
runtime.webExtensionController.setAddonManagerDelegate(addonManagerDelegate)
|
||||||
@ -476,8 +473,38 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
// --- 4. Helper Methods ---
|
// --- 4. Helper Methods ---
|
||||||
|
|
||||||
|
fun checkScrollEndUsingMatrix(): Boolean {
|
||||||
|
val matrix = android.graphics.Matrix()
|
||||||
|
// 현재 페이지 좌표 -> 화면 좌표 변환 행렬 가져오기
|
||||||
|
session?.getPageToScreenMatrix(matrix)
|
||||||
|
|
||||||
|
// 웹페이지의 콘텐츠 높이를 짐작하기 위한 로직
|
||||||
|
// (GeckoView에서 scrollHeight를 직접 가져오기 어려우므로
|
||||||
|
// CompositorScrollDelegate에서 업데이트된 정보를 활용하거나
|
||||||
|
// 아래와 같이 Matrix의 역산 값을 활용합니다.)
|
||||||
|
|
||||||
|
val values = FloatArray(9)
|
||||||
|
matrix.getValues(values)
|
||||||
|
|
||||||
|
// values[Matrix.MTRANS_Y]는 현재 페이지의 Top이 화면상 어디에 있는지(y축 오프셋)를 나타냅니다.
|
||||||
|
// values[Matrix.MSCALE_Y]는 현재 줌 배율입니다.
|
||||||
|
val currentTopInScreen = values[android.graphics.Matrix.MTRANS_Y]
|
||||||
|
val currentZoom = values[android.graphics.Matrix.MSCALE_Y]
|
||||||
|
|
||||||
|
// 💡 논리:
|
||||||
|
// 손가락 스크롤이나 볼륨키 스크롤 모두 이 Matrix 값을 실시간으로 변화시킵니다.
|
||||||
|
// 특정 시점에 '스크롤 다운'을 명령했는데 이 Matrix의 변환 값(MTRANS_Y)이
|
||||||
|
// 더 이상 변하지 않는다면 그것이 물리적인 끝입니다.
|
||||||
|
|
||||||
|
return false // 실제 구현 시에는 이전 MTRANS_Y 값과 비교 로직 추가
|
||||||
|
}
|
||||||
|
|
||||||
private fun handlePortMessage(msg: PortMessage) {
|
private fun handlePortMessage(msg: PortMessage) {
|
||||||
when (msg.type) {
|
when (msg.type) {
|
||||||
|
"SCROLL_STATE" -> {
|
||||||
|
Blog.LOGE("${msg.type} : ${msg.value}")
|
||||||
|
scrollState = msg.value?.toInt() ?: 0
|
||||||
|
}
|
||||||
"allImagesFound" -> lastedUrl?.let { startBookmarkSaveProcessForMultipleImages(it, msg.urls) }
|
"allImagesFound" -> lastedUrl?.let { startBookmarkSaveProcessForMultipleImages(it, msg.urls) }
|
||||||
"SINGLE_IMAGE_DATA" -> if (msg.imgSrc != null && msg.base64Data != null) {
|
"SINGLE_IMAGE_DATA" -> if (msg.imgSrc != null && msg.base64Data != null) {
|
||||||
Base64ImageCache.put(msg.imgSrc!!, msg.base64Data!!)
|
Base64ImageCache.put(msg.imgSrc!!, msg.base64Data!!)
|
||||||
@ -517,7 +544,7 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
return super.dispatchKeyEvent(ev)
|
return super.dispatchKeyEvent(ev)
|
||||||
}
|
}
|
||||||
|
var scrollState = 0
|
||||||
// Message Sending Helpers
|
// Message Sending Helpers
|
||||||
fun sendScrollDown(isUp: Boolean) = sendJsonMsg("scrollDown", "isUpDown" to if(isUp) 1 else -1)
|
fun sendScrollDown(isUp: Boolean) = sendJsonMsg("scrollDown", "isUpDown" to if(isUp) 1 else -1)
|
||||||
fun sendSearch(keyword: String) = sendJsonMsg("search", "keyword" to keyword)
|
fun sendSearch(keyword: String) = sendJsonMsg("search", "keyword" to keyword)
|
||||||
@ -529,6 +556,29 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
params.forEach { json.put(it.first, it.second) }
|
params.forEach { json.put(it.first, it.second) }
|
||||||
mPort?.postMessage(json)
|
mPort?.postMessage(json)
|
||||||
}
|
}
|
||||||
|
var HH = 0.3
|
||||||
|
fun pageDown() {
|
||||||
|
val session = this.session ?: return
|
||||||
|
val pzc = session.panZoomController
|
||||||
|
|
||||||
|
// 💡 현재 눈에 보이는 GeckoView의 높이를 가져옵니다.
|
||||||
|
val viewHeight = this.height.toDouble().times(HH)
|
||||||
|
|
||||||
|
// 세로 방향(y)으로 뷰 높이만큼 아래로 스크롤 (픽셀 단위)
|
||||||
|
// 세 번째 인자는 애니메이션 여부입니다.
|
||||||
|
pzc.scrollBy(ScreenLength.fromPixels(0.0), ScreenLength.fromPixels(viewHeight),
|
||||||
|
PanZoomController.SCROLL_BEHAVIOR_SMOOTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pageUp() {
|
||||||
|
val session = this.session ?: return
|
||||||
|
val pzc = session.panZoomController
|
||||||
|
val viewHeight = this.height.toDouble().times(HH)
|
||||||
|
|
||||||
|
// 위로 스크롤
|
||||||
|
pzc.scrollBy(ScreenLength.fromPixels(0.0), ScreenLength.fromPixels(-viewHeight),
|
||||||
|
PanZoomController.SCROLL_BEHAVIOR_SMOOTH)
|
||||||
|
}
|
||||||
|
|
||||||
// File/Download Helpers
|
// File/Download Helpers
|
||||||
private fun downloadFile(url: String) {
|
private fun downloadFile(url: String) {
|
||||||
@ -631,4 +681,15 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun View.post(action: () -> Unit) = this.post(Runnable(action))
|
private fun View.post(action: () -> Unit) = this.post(Runnable(action))
|
||||||
|
fun onPause() {
|
||||||
|
saveCurrentSessionState()
|
||||||
|
session?.stop()
|
||||||
|
session?.setActive(false)
|
||||||
|
Blog.LOGE("called onstop $lastedUrl")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onResume() {
|
||||||
|
restoreSessionState()
|
||||||
|
session?.setActive(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -31,6 +31,7 @@ import androidx.core.view.ViewCompat
|
|||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import bums.lunatic.launcher.LunaticLauncher
|
import bums.lunatic.launcher.LunaticLauncher
|
||||||
import bums.lunatic.launcher.R
|
import bums.lunatic.launcher.R
|
||||||
import bums.lunatic.launcher.common.CommonActivity
|
import bums.lunatic.launcher.common.CommonActivity
|
||||||
@ -61,6 +62,10 @@ import org.mozilla.geckoview.GeckoRuntimeSettings
|
|||||||
import org.mozilla.geckoview.GeckoRuntimeSettings.ALLOW_ALL
|
import org.mozilla.geckoview.GeckoRuntimeSettings.ALLOW_ALL
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
interface KeyEventHandler {
|
||||||
|
// 이벤트를 소비(consume)했으면 true, 아니면 false 반환
|
||||||
|
fun onKeyEvent(event: KeyEvent): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
open class NeoRssActivity : CommonActivity() {
|
open class NeoRssActivity : CommonActivity() {
|
||||||
|
|
||||||
@ -95,6 +100,10 @@ open class NeoRssActivity : CommonActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
setIntent(intent)
|
||||||
|
}
|
||||||
@SuppressLint("MissingSuperCall")
|
@SuppressLint("MissingSuperCall")
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
@ -154,33 +163,11 @@ open class NeoRssActivity : CommonActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
val currentFragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
|
|
||||||
Blog.LOGE("currentFragment >>> ${currentFragment} ${currentFragment is TokiFragment && currentFragment.isbooktoki()}")
|
|
||||||
if (currentFragment is TokiFragment && currentFragment.isbooktoki()) {
|
|
||||||
if(MotionEvent.ACTION_UP.equals(ev?.action ?: MotionEvent.ACTION_CANCEL) == true) {
|
|
||||||
return when (ev.keyCode) {
|
|
||||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
|
||||||
if(currentFragment is TokiFragment){ currentFragment.actionNextEvent() }
|
|
||||||
true
|
|
||||||
}
|
|
||||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
|
||||||
if(currentFragment is TokiFragment){ currentFragment.actionPrevEvent() }
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return when (ev.keyCode) {
|
|
||||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
val currentFragment = fragmentMap.values.find { it.isAdded && (!it.isHidden || it.isVisible) }
|
||||||
true
|
if (currentFragment is KeyEventHandler) {
|
||||||
}
|
if (currentFragment.onKeyEvent(ev)) {
|
||||||
|
return true
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else { return super.dispatchKeyEvent(ev) }
|
else { return super.dispatchKeyEvent(ev) }
|
||||||
@ -373,7 +360,7 @@ open class NeoRssActivity : CommonActivity() {
|
|||||||
startActivity(shareIntent)
|
startActivity(shareIntent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
showContents(R.id.feeds)
|
showContents(R.id.btn_info)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@ -390,78 +377,64 @@ open class NeoRssActivity : CommonActivity() {
|
|||||||
private var lastTouchY = 0f
|
private var lastTouchY = 0f
|
||||||
|
|
||||||
|
|
||||||
|
private val fragmentMap = mutableMapOf<Int, Fragment>()
|
||||||
|
|
||||||
fun showContents(id : Int) {
|
fun showContents(id : Int) {
|
||||||
binding.fragmentLayer.visibility = View.VISIBLE
|
binding.fragmentLayer.visibility = View.VISIBLE
|
||||||
binding.fragmentContainer.visibility = View.VISIBLE
|
binding.fragmentContainer.visibility = View.VISIBLE
|
||||||
binding.controllPanel.visibility = View.VISIBLE
|
binding.controllPanel.visibility = View.VISIBLE
|
||||||
binding.floatingActionMenu.visibility = View.VISIBLE
|
binding.floatingActionMenu.visibility = View.VISIBLE
|
||||||
when(id) {
|
|
||||||
R.id.feeds -> {
|
val transaction = supportFragmentManager.beginTransaction()
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_container, RssHome())
|
fragmentMap.values.forEach { fragment ->
|
||||||
.commit()
|
if (fragment.isAdded) {
|
||||||
|
transaction.hide(fragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. 요청된 ID에 해당하는 프래그먼트가 이미 생성되었는지 확인
|
||||||
|
var targetFragment = fragmentMap[id]
|
||||||
|
|
||||||
|
if (targetFragment == null) {
|
||||||
|
targetFragment = supportFragmentManager.findFragmentByTag("TAG_$id")
|
||||||
|
|
||||||
|
// 시스템이 복구해 놓은 게 있다면, 맵에 다시 등록(싱크 맞추기)
|
||||||
|
if (targetFragment != null) {
|
||||||
|
fragmentMap[id] = targetFragment
|
||||||
}
|
}
|
||||||
R.id.books ->{
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_container, TokiFragment.newInstanceNovels())
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.webtoons ->{
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_container, TokiFragment.newInstanceWebtoons())
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
R.id.comics ->{
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_container, TokiFragment.newInstanceComics())
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
R.id.youtube ->{
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_container, TokiFragment.newInstanceYouTube())
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
R.id.perplexity ->{
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_container, TokiFragment.newInstancePerplexity())
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
R.id.zzalbang ->{
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_container, BookmarkPagerFragment())
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.btn_x ->{
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_container, TokiFragment.newInstanceX())
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.btn_torrent ->{
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_container, TorrentListFragment())
|
|
||||||
.commit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetFragment == null) {
|
||||||
|
// 처음 호출되는 메뉴라면 인스턴스 생성 및 추가
|
||||||
|
targetFragment = when(id) {
|
||||||
|
R.id.feeds -> RssHome()
|
||||||
|
R.id.books -> TokiFragment.newInstanceNovels()
|
||||||
|
R.id.webtoons -> TokiFragment.newInstanceWebtoons()
|
||||||
|
R.id.comics -> TokiFragment.newInstanceComics()
|
||||||
|
R.id.youtube -> TokiFragment.newInstanceYouTube()
|
||||||
|
R.id.perplexity -> TokiFragment.newInstancePerplexity()
|
||||||
|
R.id.zzalbang -> BookmarkPagerFragment()
|
||||||
|
R.id.btn_x -> TokiFragment.newInstanceX()
|
||||||
|
R.id.btn_i -> TokiFragment.newInstanceI()
|
||||||
|
R.id.btn_torrent -> TorrentListFragment()
|
||||||
|
R.id.btn_info -> SystemStatusFragment()
|
||||||
R.id.close -> {
|
R.id.close -> {
|
||||||
supportFragmentManager.findFragmentById(R.id.fragment_container)?.let {
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.remove(it)
|
|
||||||
.commit()
|
|
||||||
binding.fragmentLayer.visibility = View.GONE
|
|
||||||
binding.fragmentContainer.visibility = View.GONE
|
|
||||||
binding.controllPanel.visibility = View.GONE
|
|
||||||
binding.floatingActionMenu.visibility = View.GONE
|
|
||||||
}
|
|
||||||
// sRuntime?.shutdown()
|
|
||||||
// sRuntime = null
|
|
||||||
finish()
|
finish()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetFragment?.let {
|
||||||
|
fragmentMap[id] = it
|
||||||
|
transaction.add(R.id.fragment_container, it, "TAG_$id")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 이미 생성된 프래그먼트가 있다면 다시 보여줌
|
||||||
|
transaction.show(targetFragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.commit()
|
||||||
binding.floatingActionMenu.close(false)
|
binding.floatingActionMenu.close(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -524,14 +497,14 @@ open class NeoRssActivity : CommonActivity() {
|
|||||||
currentFragment.doNextPage()
|
currentFragment.doNextPage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// is YouTube -> {
|
is TokiFragment -> {
|
||||||
// currentFragment.back()
|
currentFragment.back()
|
||||||
// }
|
}
|
||||||
// is Novels -> {
|
// is Novels -> {
|
||||||
// currentFragment.actionNextEvent(false)
|
// currentFragment.actionNextEvent(false)
|
||||||
// }
|
// }
|
||||||
else -> {
|
else -> {
|
||||||
showContents(R.id.close)
|
// showContents(R.id.close)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.PointerIcon
|
import android.view.PointerIcon
|
||||||
@ -82,7 +83,7 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
|
||||||
internal class RssHome : Fragment() {
|
internal class RssHome : Fragment() , KeyEventHandler {
|
||||||
|
|
||||||
lateinit var binding: LauncherHomeBinding
|
lateinit var binding: LauncherHomeBinding
|
||||||
private lateinit var fragManager: FragmentManager
|
private lateinit var fragManager: FragmentManager
|
||||||
@ -213,6 +214,36 @@ internal class RssHome : Fragment() {
|
|||||||
home = this
|
home = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onKeyEvent(event: KeyEvent): Boolean {
|
||||||
|
|
||||||
|
val isUp = event.action == MotionEvent.ACTION_UP
|
||||||
|
return when (event.keyCode) {
|
||||||
|
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||||
|
if (isUp) {
|
||||||
|
if (binding.geckoWeb.scrollState > 0) {
|
||||||
|
// 사용자가 손으로 끝까지 내렸든, 볼륨키로 내렸든 상관없이 이곳에 도달함
|
||||||
|
doNextPage()
|
||||||
|
} else {
|
||||||
|
binding.geckoWeb.pageDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||||
|
if (isUp) {
|
||||||
|
if (binding.geckoWeb.scrollState < 0) {
|
||||||
|
binding.geckoWeb.session?.goBack()
|
||||||
|
} else {
|
||||||
|
binding.geckoWeb.pageUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var targetList = arrayListOf<String>()
|
var targetList = arrayListOf<String>()
|
||||||
val dateViewClick = View.OnClickListener { v ->
|
val dateViewClick = View.OnClickListener { v ->
|
||||||
Blog.LOGE("click view >> ${v}")
|
Blog.LOGE("click view >> ${v}")
|
||||||
@ -576,7 +607,7 @@ internal class RssHome : Fragment() {
|
|||||||
binding.geckoWeb.decoViews.add(activity.findViewById<ImageButton>(R.id.reload))
|
binding.geckoWeb.decoViews.add(activity.findViewById<ImageButton>(R.id.reload))
|
||||||
binding.geckoWeb.decoViews.add(activity.findViewById<ImageButton>(R.id.dl_video))
|
binding.geckoWeb.decoViews.add(activity.findViewById<ImageButton>(R.id.dl_video))
|
||||||
}
|
}
|
||||||
|
binding.geckoWeb.restoreSessionState()
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -584,6 +615,20 @@ internal class RssHome : Fragment() {
|
|||||||
binding.geckoWeb?.saveMd(true)
|
binding.geckoWeb?.saveMd(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TokiFragment 또는 GeckoView를 사용하는 프래그먼트 내부
|
||||||
|
override fun onHiddenChanged(hidden: Boolean) {
|
||||||
|
super.onHiddenChanged(hidden)
|
||||||
|
if (hidden) {
|
||||||
|
// 💡 화면에서 사라질 때: 타이머 중지 및 애니메이션 중지
|
||||||
|
binding.geckoWeb?.onPause()
|
||||||
|
// 일반 WebView라면: webView.onPause() 및 webView.pauseTimers()
|
||||||
|
} else {
|
||||||
|
// 💡 다시 나타날 때: 다시 시작
|
||||||
|
// binding.geckoWeb?.onResume()
|
||||||
|
// 일반 WebView라면: webView.onResume() 및 webView.resumeTimers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
fun doNextPage() {
|
fun doNextPage() {
|
||||||
|
|||||||
@ -0,0 +1,582 @@
|
|||||||
|
package bums.lunatic.launcher.home
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.AppOpsManager
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.usage.NetworkStatsManager
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.bluetooth.BluetoothProfile
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.media.AudioDeviceInfo
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.TrafficStats
|
||||||
|
import android.net.Uri
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
|
import android.os.BatteryManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.StatFs
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.SeekBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import bums.lunatic.launcher.databinding.FragmentSystemStatusBinding
|
||||||
|
import bums.lunatic.launcher.utils.Blog
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
class SystemStatusFragment : Fragment() {
|
||||||
|
|
||||||
|
private var _binding: FragmentSystemStatusBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
private var monitorJob: Job? = null
|
||||||
|
|
||||||
|
private var lastRxBytes: Long = 0
|
||||||
|
private var lastTxBytes: Long = 0
|
||||||
|
private var lastUpdateTime: Long = 0
|
||||||
|
|
||||||
|
private lateinit var audioManager: AudioManager
|
||||||
|
|
||||||
|
// 앱 데이터 사용량 계산용 데이터 클래스
|
||||||
|
data class AppUsage(val uid: Int, var name: String = "", var wifiBytes: Long = 0L, var mobileBytes: Long = 0L) {
|
||||||
|
val totalBytes get() = wifiBytes + mobileBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentSystemStatusBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
audioManager = requireContext().getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
|
||||||
|
setupClickListeners()
|
||||||
|
setupQuickControls()
|
||||||
|
|
||||||
|
resetNetworkCounters()
|
||||||
|
startMonitoring()
|
||||||
|
updateMonthlyDataUsage()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupClickListeners() {
|
||||||
|
binding.textMonthlyUsage.setOnClickListener {
|
||||||
|
if (!hasUsageStatsPermission()) {
|
||||||
|
startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
|
||||||
|
} else {
|
||||||
|
updateMonthlyDataUsage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.textStorage.setOnClickListener {
|
||||||
|
try {
|
||||||
|
startActivity(Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.progressStorage.setOnClickListener { binding.textStorage.performClick() }
|
||||||
|
|
||||||
|
binding.textRam.setOnClickListener {
|
||||||
|
try {
|
||||||
|
val samsungIntent = Intent().apply {
|
||||||
|
setClassName("com.samsung.android.lool", "com.samsung.android.sm.ui.ram.RamActivity")
|
||||||
|
}
|
||||||
|
startActivity(samsungIntent)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
try {
|
||||||
|
startActivity(Intent(Settings.ACTION_APPLICATION_SETTINGS))
|
||||||
|
Toast.makeText(requireContext(), "앱 설정 화면으로 이동합니다.", Toast.LENGTH_SHORT).show()
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.progressRam.setOnClickListener { binding.textRam.performClick() }
|
||||||
|
|
||||||
|
binding.textBattery.setOnClickListener {
|
||||||
|
try {
|
||||||
|
startActivity(Intent(Intent.ACTION_POWER_USAGE_SUMMARY))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.progressBattery.setOnClickListener { binding.textBattery.performClick() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupQuickControls() {
|
||||||
|
binding.btnQuickWifi.setOnClickListener {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
startActivity(Intent(Settings.Panel.ACTION_WIFI))
|
||||||
|
} else {
|
||||||
|
startActivity(Intent(Settings.ACTION_WIFI_SETTINGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnQuickBt.setOnClickListener {
|
||||||
|
startActivity(Intent(Settings.ACTION_BLUETOOTH_SETTINGS))
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnQuickRinger.setOnClickListener {
|
||||||
|
val nm = requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !nm.isNotificationPolicyAccessGranted) {
|
||||||
|
Toast.makeText(requireContext(), "무음 모드 제어를 위해 권한을 허용해주세요.", Toast.LENGTH_LONG).show()
|
||||||
|
startActivity(Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS))
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
when (audioManager.ringerMode) {
|
||||||
|
AudioManager.RINGER_MODE_NORMAL -> audioManager.ringerMode = AudioManager.RINGER_MODE_VIBRATE
|
||||||
|
AudioManager.RINGER_MODE_VIBRATE -> audioManager.ringerMode = AudioManager.RINGER_MODE_SILENT
|
||||||
|
AudioManager.RINGER_MODE_SILENT -> audioManager.ringerMode = AudioManager.RINGER_MODE_NORMAL
|
||||||
|
}
|
||||||
|
updateQuickControlUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.seekVolume.max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||||
|
binding.seekVolume.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||||
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||||
|
if (fromUser) audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, progress, 0)
|
||||||
|
}
|
||||||
|
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
||||||
|
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.seekBrightness.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||||
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||||
|
if (!fromUser) return
|
||||||
|
if (Settings.System.canWrite(requireContext())) {
|
||||||
|
val cr = requireContext().contentResolver
|
||||||
|
Settings.System.putInt(cr, Settings.System.SCREEN_BRIGHTNESS_MODE, Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL)
|
||||||
|
Settings.System.putInt(cr, Settings.System.SCREEN_BRIGHTNESS, progress)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(requireContext(), "밝기 조절을 위해 '시스템 설정 수정' 권한이 필요합니다.", Toast.LENGTH_LONG).show()
|
||||||
|
val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply {
|
||||||
|
data = Uri.parse("package:${requireContext().packageName}")
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
||||||
|
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
updateQuickControlUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateQuickControlUI() {
|
||||||
|
val ringerText = when (audioManager.ringerMode) {
|
||||||
|
AudioManager.RINGER_MODE_NORMAL -> "🔔 소리"
|
||||||
|
AudioManager.RINGER_MODE_VIBRATE -> "📳 진동"
|
||||||
|
AudioManager.RINGER_MODE_SILENT -> "🔕 무음"
|
||||||
|
else -> "소리"
|
||||||
|
}
|
||||||
|
binding.btnQuickRinger.text = ringerText
|
||||||
|
binding.seekVolume.progress = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val brightness = Settings.System.getInt(requireContext().contentResolver, Settings.System.SCREEN_BRIGHTNESS)
|
||||||
|
binding.seekBrightness.progress = brightness
|
||||||
|
} catch (e: Settings.SettingNotFoundException) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHiddenChanged(hidden: Boolean) {
|
||||||
|
super.onHiddenChanged(hidden)
|
||||||
|
if (hidden) {
|
||||||
|
stopMonitoring()
|
||||||
|
} else {
|
||||||
|
resetNetworkCounters()
|
||||||
|
startMonitoring()
|
||||||
|
updateMonthlyDataUsage()
|
||||||
|
updateQuickControlUI()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
stopMonitoring()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (!isHidden) {
|
||||||
|
resetNetworkCounters()
|
||||||
|
startMonitoring()
|
||||||
|
updateMonthlyDataUsage()
|
||||||
|
updateQuickControlUI()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetNetworkCounters() {
|
||||||
|
lastRxBytes = TrafficStats.getTotalRxBytes()
|
||||||
|
lastTxBytes = TrafficStats.getTotalTxBytes()
|
||||||
|
lastUpdateTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startMonitoring() {
|
||||||
|
if (monitorJob?.isActive == true) return
|
||||||
|
|
||||||
|
monitorJob = CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
while (isActive) {
|
||||||
|
updateSystemInfo()
|
||||||
|
updateNetworkSpeed()
|
||||||
|
if (!binding.seekVolume.isPressed && !binding.seekBrightness.isPressed) {
|
||||||
|
updateQuickControlUI()
|
||||||
|
}
|
||||||
|
delay(2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopMonitoring() {
|
||||||
|
monitorJob?.cancel()
|
||||||
|
monitorJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSystemInfo() {
|
||||||
|
val context = requireContext()
|
||||||
|
|
||||||
|
// 1. RAM
|
||||||
|
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||||
|
val memoryInfo = ActivityManager.MemoryInfo()
|
||||||
|
activityManager.getMemoryInfo(memoryInfo)
|
||||||
|
|
||||||
|
val totalRam = memoryInfo.totalMem / (1024 * 1024)
|
||||||
|
val availRam = memoryInfo.availMem / (1024 * 1024)
|
||||||
|
val usedRam = totalRam - availRam
|
||||||
|
val ramPercent = if (totalRam > 0) (usedRam.toFloat() / totalRam.toFloat() * 100).toInt() else 0
|
||||||
|
|
||||||
|
binding.textRam.text = "RAM: ${usedRam}MB / ${totalRam}MB ($ramPercent%)"
|
||||||
|
binding.progressRam.progress = ramPercent
|
||||||
|
|
||||||
|
// 2. Storage
|
||||||
|
val statFs = StatFs(Environment.getDataDirectory().path)
|
||||||
|
val totalStorage = statFs.totalBytes / (1024 * 1024 * 1024)
|
||||||
|
val availStorage = statFs.availableBytes / (1024 * 1024 * 1024)
|
||||||
|
val usedStorage = totalStorage - availStorage
|
||||||
|
val storagePercent = if (totalStorage > 0) (usedStorage.toFloat() / totalStorage.toFloat() * 100).toInt() else 0
|
||||||
|
|
||||||
|
binding.textStorage.text = "Storage: ${usedStorage}GB / ${totalStorage}GB ($storagePercent%)"
|
||||||
|
binding.progressStorage.progress = storagePercent
|
||||||
|
|
||||||
|
// 3. Battery
|
||||||
|
val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
|
||||||
|
context.registerReceiver(null, ifilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
batteryStatus?.let { intent ->
|
||||||
|
val level: Int = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
|
||||||
|
val scale: Int = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
|
||||||
|
val batteryPct = if (scale > 0) level * 100 / scale.toFloat() else 0f
|
||||||
|
|
||||||
|
val tempInt = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0)
|
||||||
|
val temperature = tempInt / 10f
|
||||||
|
|
||||||
|
val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
||||||
|
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
|
||||||
|
val chargePlug = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
|
||||||
|
|
||||||
|
val chargingText = when {
|
||||||
|
status == BatteryManager.BATTERY_STATUS_FULL -> "충전 완료 (100%)"
|
||||||
|
isCharging && chargePlug == BatteryManager.BATTERY_PLUGGED_AC -> "충전 중 (AC 어댑터)"
|
||||||
|
isCharging && chargePlug == BatteryManager.BATTERY_PLUGGED_USB -> "충전 중 (USB 연결)"
|
||||||
|
isCharging && chargePlug == BatteryManager.BATTERY_PLUGGED_WIRELESS -> "충전 중 (무선 충전)"
|
||||||
|
isCharging -> "충전 중"
|
||||||
|
else -> "방전 중"
|
||||||
|
}
|
||||||
|
binding.textChargingStatus.text = "Status: $chargingText"
|
||||||
|
|
||||||
|
binding.textBattery.text = "Battery: ${batteryPct.toInt()}%"
|
||||||
|
binding.progressBattery.progress = batteryPct.toInt()
|
||||||
|
|
||||||
|
if (temperature >= 40.0f) {
|
||||||
|
binding.textTemperature.setTextColor(Color.parseColor("#F44336"))
|
||||||
|
} else {
|
||||||
|
binding.textTemperature.setTextColor(Color.parseColor("#FF9800"))
|
||||||
|
}
|
||||||
|
binding.textTemperature.text = "Temp: ${temperature}°C"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Connectivity
|
||||||
|
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
val activeNetwork = cm.activeNetwork
|
||||||
|
val caps = cm.getNetworkCapabilities(activeNetwork)
|
||||||
|
|
||||||
|
var networkName = "오프라인"
|
||||||
|
if (caps != null) {
|
||||||
|
when {
|
||||||
|
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {
|
||||||
|
networkName = "Wi-Fi"
|
||||||
|
try {
|
||||||
|
val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||||
|
var ssid = wifiManager.connectionInfo.ssid
|
||||||
|
if (ssid.startsWith("\"") && ssid.endsWith("\"")) {
|
||||||
|
ssid = ssid.substring(1, ssid.length - 1)
|
||||||
|
}
|
||||||
|
if (ssid != "<unknown ssid>" && ssid.isNotBlank()) {
|
||||||
|
networkName = "Wi-Fi ($ssid)"
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) { }
|
||||||
|
}
|
||||||
|
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> networkName = "Mobile Data"
|
||||||
|
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> networkName = "Ethernet"
|
||||||
|
caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> networkName = "VPN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.textNetworkState.text = "Net: $networkName"
|
||||||
|
|
||||||
|
// 5. Bluetooth
|
||||||
|
var btState = "OFF"
|
||||||
|
var connectedDevicesStr = ""
|
||||||
|
try {
|
||||||
|
val btManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
|
val btAdapter = btManager.adapter
|
||||||
|
if (btAdapter?.isEnabled == true) {
|
||||||
|
btState = "ON"
|
||||||
|
val connectedNames = mutableSetOf<String>()
|
||||||
|
|
||||||
|
val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
||||||
|
audioDevices.forEach { device ->
|
||||||
|
if (device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
|
||||||
|
device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
|
||||||
|
device.type == AudioDeviceInfo.TYPE_BLE_HEADSET ||
|
||||||
|
device.type == AudioDeviceInfo.TYPE_BLE_SPEAKER) {
|
||||||
|
|
||||||
|
if (!device.productName.isNullOrBlank()) {
|
||||||
|
connectedNames.add(device.productName.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
btManager.getConnectedDevices(BluetoothProfile.GATT).forEach { device ->
|
||||||
|
if (!device.name.isNullOrBlank()) {
|
||||||
|
connectedNames.add(device.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) { }
|
||||||
|
|
||||||
|
if (connectedNames.isNotEmpty()) {
|
||||||
|
connectedDevicesStr = " (${connectedNames.joinToString(", ")})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
btState = "권한 필요"
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.textBluetoothState.text = "BT: $btState$connectedDevicesStr"
|
||||||
|
|
||||||
|
// 6. Uptime
|
||||||
|
val uptimeMillis = SystemClock.elapsedRealtime()
|
||||||
|
val hours = (uptimeMillis / (1000 * 60 * 60)).toInt()
|
||||||
|
val minutes = ((uptimeMillis / (1000 * 60)) % 60).toInt()
|
||||||
|
val seconds = ((uptimeMillis / 1000) % 60).toInt()
|
||||||
|
|
||||||
|
binding.textUptime.text = String.format("System Uptime: %02d:%02d:%02d", hours, minutes, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateNetworkSpeed() {
|
||||||
|
val currentRxBytes = TrafficStats.getTotalRxBytes()
|
||||||
|
val currentTxBytes = TrafficStats.getTotalTxBytes()
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
if (currentRxBytes == TrafficStats.UNSUPPORTED.toLong() || lastUpdateTime == 0L) return
|
||||||
|
|
||||||
|
val timeDiff = (currentTime - lastUpdateTime) / 1000f
|
||||||
|
if (timeDiff > 0) {
|
||||||
|
val rxSpeed = (currentRxBytes - lastRxBytes) / timeDiff
|
||||||
|
val txSpeed = (currentTxBytes - lastTxBytes) / timeDiff
|
||||||
|
|
||||||
|
binding.textDownload.text = "↓ DL: ${formatSpeed(rxSpeed)}"
|
||||||
|
binding.textUpload.text = "↑ UL: ${formatSpeed(txSpeed)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRxBytes = currentRxBytes
|
||||||
|
lastTxBytes = currentTxBytes
|
||||||
|
lastUpdateTime = currentTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// 💡 앱별 다중 색상 데이터 사용량 뷰 생성 로직
|
||||||
|
private fun updateMonthlyDataUsage() {
|
||||||
|
if (!hasUsageStatsPermission()) {
|
||||||
|
binding.textMonthlyUsage.text = "Monthly Usage: 권한 필요 (여기를 클릭하여 허용)"
|
||||||
|
binding.layoutTopApps.visibility = View.GONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val networkStatsManager = requireContext().getSystemService(Context.NETWORK_STATS_SERVICE) as NetworkStatsManager
|
||||||
|
val calendar = Calendar.getInstance()
|
||||||
|
calendar.set(Calendar.DAY_OF_MONTH, 1)
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, 0)
|
||||||
|
val startTime = calendar.timeInMillis
|
||||||
|
val endTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
val wifiBucket = networkStatsManager.querySummaryForDevice(ConnectivityManager.TYPE_WIFI, null, startTime, endTime)
|
||||||
|
val mobileBucket = networkStatsManager.querySummaryForDevice(ConnectivityManager.TYPE_MOBILE, null, startTime, endTime)
|
||||||
|
|
||||||
|
val wifiTotal = wifiBucket.rxBytes + wifiBucket.txBytes
|
||||||
|
val mobileTotal = mobileBucket.rxBytes + mobileBucket.txBytes
|
||||||
|
|
||||||
|
val appUsageMap = mutableMapOf<Int, AppUsage>()
|
||||||
|
fun aggregateStats(networkType: Int, isWifi: Boolean) {
|
||||||
|
try {
|
||||||
|
val networkStats = networkStatsManager.querySummary(networkType, null, startTime, endTime)
|
||||||
|
val bucket = android.app.usage.NetworkStats.Bucket()
|
||||||
|
while (networkStats.hasNextBucket()) {
|
||||||
|
networkStats.getNextBucket(bucket)
|
||||||
|
val uid = bucket.uid
|
||||||
|
val totalBytes = bucket.rxBytes + bucket.txBytes
|
||||||
|
|
||||||
|
val appUsage = appUsageMap.getOrPut(uid) { AppUsage(uid) }
|
||||||
|
if (isWifi) appUsage.wifiBytes += totalBytes
|
||||||
|
else appUsage.mobileBytes += totalBytes
|
||||||
|
}
|
||||||
|
networkStats.close()
|
||||||
|
} catch (e: Exception) { e.printStackTrace() }
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregateStats(ConnectivityManager.TYPE_WIFI, true)
|
||||||
|
aggregateStats(ConnectivityManager.TYPE_MOBILE, false)
|
||||||
|
|
||||||
|
val packageManager = requireContext().packageManager
|
||||||
|
val topApps = appUsageMap.values
|
||||||
|
.filter { it.totalBytes > 0 }
|
||||||
|
.sortedByDescending { it.totalBytes }
|
||||||
|
.take(10)
|
||||||
|
|
||||||
|
topApps.forEach { app ->
|
||||||
|
app.name = "Unknown (UID: ${app.uid})"
|
||||||
|
try {
|
||||||
|
val packages = packageManager.getPackagesForUid(app.uid)
|
||||||
|
if (!packages.isNullOrEmpty()) {
|
||||||
|
val appInfo = packageManager.getApplicationInfo(packages[0], 0)
|
||||||
|
app.name = packageManager.getApplicationLabel(appInfo).toString()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
val maxBytes = topApps.maxOfOrNull { it.totalBytes }?.toFloat() ?: 1f
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
// 전체 요약 (색상 아이콘 추가)
|
||||||
|
binding.textMonthlyUsage.text = "이번 달 누적 (Wi-Fi 🟩: ${formatSize(wifiTotal)} / 모바일 🟦: ${formatSize(mobileTotal)})"
|
||||||
|
|
||||||
|
val container = binding.layoutTopApps
|
||||||
|
container.removeAllViews()
|
||||||
|
container.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
if (topApps.isEmpty()) {
|
||||||
|
val emptyText = TextView(context).apply {
|
||||||
|
text = "앱별 데이터 없음"
|
||||||
|
setTextColor(Color.parseColor("#BBBBBB"))
|
||||||
|
}
|
||||||
|
container.addView(emptyText)
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// 💡 동적 멀티 컬러 프로그레스바 그리기 (LinearLayout + layout_weight)
|
||||||
|
topApps.forEachIndexed { index, app ->
|
||||||
|
// 텍스트 라벨
|
||||||
|
val textView = TextView(context).apply {
|
||||||
|
text = "${index + 1}. ${app.name}\n(총: ${formatSize(app.totalBytes)} | Wi-Fi 🟩: ${formatSize(app.wifiBytes)} | 모바일 🟦: ${formatSize(app.mobileBytes)})"
|
||||||
|
setTextColor(Color.parseColor("#CCCCCC"))
|
||||||
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
|
||||||
|
setPadding(0, 16, 0, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전체 게이지 바 컨테이너
|
||||||
|
val barContainer = LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
weightSum = maxBytes
|
||||||
|
val heightPx = (10 * resources.displayMetrics.density).toInt()
|
||||||
|
val marginPx = (8 * resources.displayMetrics.density).toInt()
|
||||||
|
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, heightPx).apply {
|
||||||
|
setMargins(0, 0, 0, marginPx)
|
||||||
|
}
|
||||||
|
setBackgroundColor(Color.parseColor("#333333")) // 빈 부분 어두운 배경
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Wi-Fi 사용량 막대 (초록색)
|
||||||
|
if (app.wifiBytes > 0) {
|
||||||
|
val wifiView = View(context).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, app.wifiBytes.toFloat())
|
||||||
|
setBackgroundColor(Color.parseColor("#4CAF50"))
|
||||||
|
}
|
||||||
|
barContainer.addView(wifiView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 모바일 사용량 막대 (파란색)
|
||||||
|
if (app.mobileBytes > 0) {
|
||||||
|
val mobileView = View(context).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, app.mobileBytes.toFloat())
|
||||||
|
setBackgroundColor(Color.parseColor("#2196F3"))
|
||||||
|
}
|
||||||
|
barContainer.addView(mobileView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 남은 여백 (가장 많이 쓴 앱 기준 스케일 맞추기 위함)
|
||||||
|
val emptyWeight = maxBytes - app.totalBytes.toFloat()
|
||||||
|
if (emptyWeight > 0) {
|
||||||
|
val emptyView = View(context).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, emptyWeight)
|
||||||
|
}
|
||||||
|
barContainer.addView(emptyView)
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addView(textView)
|
||||||
|
container.addView(barContainer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasUsageStatsPermission(): Boolean {
|
||||||
|
val appOps = requireContext().getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
||||||
|
val mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, android.os.Process.myUid(), requireContext().packageName)
|
||||||
|
return mode == AppOpsManager.MODE_ALLOWED
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatSpeed(bytesPerSec: Float): String {
|
||||||
|
return when {
|
||||||
|
bytesPerSec >= 1024 * 1024 -> String.format("%.2f MB/s", bytesPerSec / (1024 * 1024))
|
||||||
|
bytesPerSec >= 1024 -> String.format("%.1f KB/s", bytesPerSec / 1024)
|
||||||
|
else -> "${bytesPerSec.toInt()} B/s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatSize(bytes: Long): String {
|
||||||
|
return when {
|
||||||
|
bytes >= 1024 * 1024 * 1024 -> String.format("%.2f GB", bytes / (1024f * 1024f * 1024f))
|
||||||
|
bytes >= 1024 * 1024 -> String.format("%.1f MB", bytes / (1024f * 1024f))
|
||||||
|
else -> "${bytes / 1024} KB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
stopMonitoring()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -39,7 +39,7 @@ class TorrentListAdapter(
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val task = tasks[position]
|
val task = tasks[position]
|
||||||
holder.tvName.text = task.name
|
holder.tvName.text = task.name.plus("(${task.stateText})")
|
||||||
holder.progressBar.progress = task.progress.toInt()
|
holder.progressBar.progress = task.progress.toInt()
|
||||||
holder.tvProgress.text = String.format("%.1f%%", task.progress)
|
holder.tvProgress.text = String.format("%.1f%%", task.progress)
|
||||||
|
|
||||||
|
|||||||
@ -103,6 +103,7 @@ class PortMessage {
|
|||||||
var urls : List<String> = emptyList()
|
var urls : List<String> = emptyList()
|
||||||
var imgSrc: String? = null
|
var imgSrc: String? = null
|
||||||
var base64Data: String? = null
|
var base64Data: String? = null
|
||||||
|
var value : String? = null
|
||||||
}
|
}
|
||||||
class BookContents {
|
class BookContents {
|
||||||
var chapterTitle : String? = null
|
var chapterTitle : String? = null
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import android.text.InputType
|
|||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.style.RelativeSizeSpan
|
import android.text.style.RelativeSizeSpan
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.PointerIcon
|
import android.view.PointerIcon
|
||||||
@ -34,6 +35,7 @@ import bums.lunatic.launcher.R
|
|||||||
import bums.lunatic.launcher.databinding.BooktokiBinding
|
import bums.lunatic.launcher.databinding.BooktokiBinding
|
||||||
import bums.lunatic.launcher.home.GeckoWeb
|
import bums.lunatic.launcher.home.GeckoWeb
|
||||||
import bums.lunatic.launcher.home.GeckoWeb.JxEvent
|
import bums.lunatic.launcher.home.GeckoWeb.JxEvent
|
||||||
|
import bums.lunatic.launcher.home.KeyEventHandler
|
||||||
import bums.lunatic.launcher.home.NeoRssActivity
|
import bums.lunatic.launcher.home.NeoRssActivity
|
||||||
import bums.lunatic.launcher.home.toast
|
import bums.lunatic.launcher.home.toast
|
||||||
import bums.lunatic.launcher.home.tokiz.view.PagedTextLayout
|
import bums.lunatic.launcher.home.tokiz.view.PagedTextLayout
|
||||||
@ -64,12 +66,12 @@ import java.util.Date
|
|||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
// 기존 BaseToki 및 하위 클래스들을 통합한 단일 클래스
|
// 기존 BaseToki 및 하위 클래스들을 통합한 단일 클래스
|
||||||
class TokiFragment : Fragment(), PagedTextViewInterface {
|
class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
|
||||||
|
|
||||||
// --- Configuration Properties (Arguments에서 로드) ---
|
// --- Configuration Properties (Arguments에서 로드) ---
|
||||||
private lateinit var contentsType: String
|
private lateinit var contentsType: String
|
||||||
private var lastNumber: Int = 0
|
private var lastNumber: Int = 0
|
||||||
private lateinit var webcontentsName: String
|
lateinit var webcontentsName: String
|
||||||
private lateinit var afterDot: String
|
private lateinit var afterDot: String
|
||||||
private var useNumberInUrl: Boolean = true
|
private var useNumberInUrl: Boolean = true
|
||||||
private var enableGestures: Boolean = true
|
private var enableGestures: Boolean = true
|
||||||
@ -160,6 +162,17 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun newInstanceI(): TokiFragment = TokiFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(ARG_TYPE, "ijavtorrent")
|
||||||
|
putInt(ARG_LAST_NUM, 143)
|
||||||
|
putString(ARG_NAME, "ijavtorrent")
|
||||||
|
putString(ARG_DOT, "com")
|
||||||
|
putBoolean(ARG_USE_NUM_URL, false)
|
||||||
|
putBoolean(ARG_ENABLE_GESTURE, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun newInstanceNovels(): TokiFragment = TokiFragment().apply {
|
fun newInstanceNovels(): TokiFragment = TokiFragment().apply {
|
||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putString(ARG_TYPE, "book")
|
putString(ARG_TYPE, "book")
|
||||||
@ -236,6 +249,24 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
|||||||
fun isbooktoki() : Boolean {
|
fun isbooktoki() : Boolean {
|
||||||
return webcontentsName.contains("booktoki")
|
return webcontentsName.contains("booktoki")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onKeyEvent(event: KeyEvent): Boolean {
|
||||||
|
if (!isbooktoki()) return false // 조건부 처리
|
||||||
|
|
||||||
|
val isUp = event.action == MotionEvent.ACTION_UP
|
||||||
|
return when (event.keyCode) {
|
||||||
|
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||||
|
if (isUp) actionNextEvent()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||||
|
if (isUp) actionPrevEvent()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSwipeLeft(count: Int) {
|
override fun onSwipeLeft(count: Int) {
|
||||||
if (!enableGestures) return
|
if (!enableGestures) return
|
||||||
Blog.LOGD(log = "onSwipeLeft ${count}")
|
Blog.LOGD(log = "onSwipeLeft ${count}")
|
||||||
@ -283,8 +314,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
|||||||
if (contentsType == "youtube") {
|
if (contentsType == "youtube") {
|
||||||
binding.menuWeb.session?.goBack()
|
binding.menuWeb.session?.goBack()
|
||||||
} else {
|
} else {
|
||||||
// binding.menuWeb.session?.goBack()
|
actionNextEvent(false)
|
||||||
// 기존 BaseToki 주석 처리됨
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,6 +425,21 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
|||||||
return workingUrl
|
return workingUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onHiddenChanged(hidden: Boolean) {
|
||||||
|
super.onHiddenChanged(hidden)
|
||||||
|
if (hidden) {
|
||||||
|
// 💡 화면에서 사라질 때: 타이머 중지 및 애니메이션 중지
|
||||||
|
binding.menuWeb?.onPause()
|
||||||
|
// 일반 WebView라면: webView.onPause() 및 webView.pauseTimers()
|
||||||
|
} else {
|
||||||
|
// 💡 다시 나타날 때: 다시 시작
|
||||||
|
// binding.menuWeb?.onResume()
|
||||||
|
// 일반 WebView라면: webView.onResume() 및 webView.resumeTimers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@ -403,6 +448,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
|||||||
Blog.LOGD(log = "onCreate ${this::class.java.name} >> savedInstanceState ${savedInstanceState}")
|
Blog.LOGD(log = "onCreate ${this::class.java.name} >> savedInstanceState ${savedInstanceState}")
|
||||||
binding = BooktokiBinding.inflate(inflater)
|
binding = BooktokiBinding.inflate(inflater)
|
||||||
binding.menuWeb.let {
|
binding.menuWeb.let {
|
||||||
|
it.sessionTag = webcontentsName
|
||||||
it.visibility = View.VISIBLE
|
it.visibility = View.VISIBLE
|
||||||
it.lastDomain = getLastedDoamin()
|
it.lastDomain = getLastedDoamin()
|
||||||
it.setOnLongClickListener {
|
it.setOnLongClickListener {
|
||||||
@ -416,27 +462,27 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
|||||||
if (lastedUrl?.contains("youtube.com") == true) {
|
if (lastedUrl?.contains("youtube.com") == true) {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
binding.menuWeb.postDelayed({
|
// binding.menuWeb.postDelayed({
|
||||||
|
//
|
||||||
Blog.LOGE("onPageStop $success from WebExtension ${mPort!!.name}")
|
// Blog.LOGE("onPageStop $success from WebExtension ${mPort!!.name}")
|
||||||
val message: JSONObject = JSONObject()
|
// val message: JSONObject = JSONObject()
|
||||||
try {
|
// try {
|
||||||
message.put("type", "getList")
|
// message.put("type", "getList")
|
||||||
message.put("event", "sadsadds")
|
// message.put("event", "sadsadds")
|
||||||
// message.put("tab", session.settings.screenId)
|
//// message.put("tab", session.settings.screenId)
|
||||||
} catch (ex: JSONException) {
|
// } catch (ex: JSONException) {
|
||||||
throw RuntimeException(ex)
|
// throw RuntimeException(ex)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
mPort!!.postMessage(message)
|
// mPort!!.postMessage(message)
|
||||||
|
//
|
||||||
|
//
|
||||||
// 타입별 분기 처리 (기존 when 절 대체)
|
// // 타입별 분기 처리 (기존 when 절 대체)
|
||||||
if (contentsType == "comics" || contentsType == "webtoon") {
|
// if (contentsType == "comics" || contentsType == "webtoon") {
|
||||||
lastInfo
|
// lastInfo
|
||||||
}
|
// }
|
||||||
binding.progress.visibility = GONE
|
// binding.progress.visibility = GONE
|
||||||
}, 10L)
|
// }, 10L)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -531,6 +577,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
|||||||
it.decoViews.add(activity.findViewById<ImageButton>(R.id.reload))
|
it.decoViews.add(activity.findViewById<ImageButton>(R.id.reload))
|
||||||
it.decoViews.add(activity.findViewById<ImageButton>(R.id.dl_video))
|
it.decoViews.add(activity.findViewById<ImageButton>(R.id.dl_video))
|
||||||
}
|
}
|
||||||
|
it.restoreSessionState()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.btnList.setOnClickListener { v ->
|
binding.btnList.setOnClickListener { v ->
|
||||||
|
|||||||
@ -106,6 +106,14 @@ class FloatingActionMenu @JvmOverloads constructor(
|
|||||||
|
|
||||||
private var isDragMenuDisabled = false
|
private var isDragMenuDisabled = false
|
||||||
|
|
||||||
|
// 💡 라벨 위치 동적 변경을 위한 프로퍼티
|
||||||
|
var labelsPosition: Int
|
||||||
|
get() = mLabelsPosition
|
||||||
|
set(position) {
|
||||||
|
mLabelsPosition = position
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
interface OnMenuToggleListener {
|
interface OnMenuToggleListener {
|
||||||
fun onMenuToggle(opened: Boolean)
|
fun onMenuToggle(opened: Boolean)
|
||||||
}
|
}
|
||||||
@ -388,8 +396,14 @@ class FloatingActionMenu @JvmOverloads constructor(
|
|||||||
if (label != null) {
|
if (label != null) {
|
||||||
val labelOffset =
|
val labelOffset =
|
||||||
(mMaxButtonWidth - child.getMeasuredWidth()) / (if (mUsingMenuLabel) 1 else 2)
|
(mMaxButtonWidth - child.getMeasuredWidth()) / (if (mUsingMenuLabel) 1 else 2)
|
||||||
val labelUsedWidth: Int =
|
|
||||||
|
// 💡 [수정] Center 옵션일 경우 라벨을 버튼 위로 겹쳐 그리므로 공간을 추가하지 않음
|
||||||
|
val labelUsedWidth: Int = if (mLabelsPosition == LABELS_POSITION_CENTER) {
|
||||||
|
child.getMeasuredWidth()
|
||||||
|
} else {
|
||||||
child.getMeasuredWidth() + label.calculateShadowWidth() + mLabelsMargin + labelOffset
|
child.getMeasuredWidth() + label.calculateShadowWidth() + mLabelsMargin + labelOffset
|
||||||
|
}
|
||||||
|
|
||||||
measureChildWithMargins(
|
measureChildWithMargins(
|
||||||
label,
|
label,
|
||||||
widthMeasureSpec,
|
widthMeasureSpec,
|
||||||
@ -397,9 +411,11 @@ class FloatingActionMenu @JvmOverloads constructor(
|
|||||||
heightMeasureSpec,
|
heightMeasureSpec,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (mLabelsPosition != LABELS_POSITION_CENTER) {
|
||||||
usedWidth += label.getMeasuredWidth()
|
usedWidth += label.getMeasuredWidth()
|
||||||
maxLabelWidth =
|
maxLabelWidth = max(maxLabelWidth.toDouble(), (usedWidth + labelOffset).toDouble()).toInt()
|
||||||
max(maxLabelWidth.toDouble(), (usedWidth + labelOffset).toDouble()).toInt()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,6 +499,16 @@ class FloatingActionMenu @JvmOverloads constructor(
|
|||||||
|
|
||||||
val label = fab.getTag(R.id.fab_label) as View?
|
val label = fab.getTag(R.id.fab_label) as View?
|
||||||
if (label != null) {
|
if (label != null) {
|
||||||
|
val labelLeft: Int
|
||||||
|
val labelRight: Int
|
||||||
|
val labelTop: Int
|
||||||
|
|
||||||
|
// 💡 [수정] Center 옵션일 경우 X,Y축 모두 버튼의 중앙으로 계산
|
||||||
|
if (mLabelsPosition == LABELS_POSITION_CENTER) {
|
||||||
|
labelLeft = childX + (fab.getMeasuredWidth() - label.getMeasuredWidth()) / 2
|
||||||
|
labelRight = labelLeft + label.getMeasuredWidth()
|
||||||
|
labelTop = childY + (fab.getMeasuredHeight() - label.getMeasuredHeight()) / 2
|
||||||
|
} else {
|
||||||
val labelsOffset =
|
val labelsOffset =
|
||||||
(if (mUsingMenuLabel) mMaxButtonWidth / 2 else fab.getMeasuredWidth() / 2) + mLabelsMargin
|
(if (mUsingMenuLabel) mMaxButtonWidth / 2 else fab.getMeasuredWidth() / 2) + mLabelsMargin
|
||||||
val labelXNearButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
val labelXNearButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||||
@ -495,18 +521,18 @@ class FloatingActionMenu @JvmOverloads constructor(
|
|||||||
else
|
else
|
||||||
labelXNearButton + label.getMeasuredWidth()
|
labelXNearButton + label.getMeasuredWidth()
|
||||||
|
|
||||||
val labelLeft = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
labelLeft = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||||
labelXAwayFromButton
|
labelXAwayFromButton
|
||||||
else
|
else
|
||||||
labelXNearButton
|
labelXNearButton
|
||||||
|
|
||||||
val labelRight = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
labelRight = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||||
labelXNearButton
|
labelXNearButton
|
||||||
else
|
else
|
||||||
labelXAwayFromButton
|
labelXAwayFromButton
|
||||||
|
|
||||||
val labelTop = childY - mLabelsVerticalOffset + (fab.getMeasuredHeight()
|
labelTop = childY - mLabelsVerticalOffset + (fab.getMeasuredHeight() - label.getMeasuredHeight()) / 2
|
||||||
- label.getMeasuredHeight()) / 2
|
}
|
||||||
|
|
||||||
label.layout(labelLeft, labelTop, labelRight, labelTop + label.getMeasuredHeight())
|
label.layout(labelLeft, labelTop, labelRight, labelTop + label.getMeasuredHeight())
|
||||||
|
|
||||||
@ -667,7 +693,6 @@ class FloatingActionMenu @JvmOverloads constructor(
|
|||||||
fab = child as FloatingActionButton
|
fab = child as FloatingActionButton
|
||||||
if (fab === mMenuButton) continue
|
if (fab === mMenuButton) continue
|
||||||
|
|
||||||
// only child buttons and imageToggle on menuBtn.
|
|
||||||
child.layout(
|
child.layout(
|
||||||
childX, childY, childX + child.getMeasuredWidth(),
|
childX, childY, childX + child.getMeasuredWidth(),
|
||||||
childY + child.getMeasuredHeight()
|
childY + child.getMeasuredHeight()
|
||||||
@ -681,6 +706,16 @@ class FloatingActionMenu @JvmOverloads constructor(
|
|||||||
if (child !== mImageToggle) label = fab.getTag(R.id.fab_label) as View?
|
if (child !== mImageToggle) label = fab.getTag(R.id.fab_label) as View?
|
||||||
|
|
||||||
if (label != null) {
|
if (label != null) {
|
||||||
|
val labelLeft: Int
|
||||||
|
val labelRight: Int
|
||||||
|
val labelTop: Int
|
||||||
|
|
||||||
|
// 💡 [수정] Center 옵션일 경우 드래그 중에도 중앙으로 정렬
|
||||||
|
if (mLabelsPosition == LABELS_POSITION_CENTER) {
|
||||||
|
labelLeft = childX + (fab.getMeasuredWidth() - label.getMeasuredWidth()) / 2
|
||||||
|
labelRight = labelLeft + label.getMeasuredWidth()
|
||||||
|
labelTop = childY + (fab.getMeasuredHeight() - label.getMeasuredHeight()) / 2
|
||||||
|
} else {
|
||||||
val labelsOffset =
|
val labelsOffset =
|
||||||
(if (mUsingMenuLabel) mMaxButtonWidth / 2 else fab.getMeasuredWidth() / 2) + mLabelsMargin
|
(if (mUsingMenuLabel) mMaxButtonWidth / 2 else fab.getMeasuredWidth() / 2) + mLabelsMargin
|
||||||
val labelXNearButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
val labelXNearButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||||
@ -693,18 +728,18 @@ class FloatingActionMenu @JvmOverloads constructor(
|
|||||||
else
|
else
|
||||||
labelXNearButton + label.getMeasuredWidth()
|
labelXNearButton + label.getMeasuredWidth()
|
||||||
|
|
||||||
val labelLeft = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
labelLeft = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||||
labelXAwayFromButton
|
labelXAwayFromButton
|
||||||
else
|
else
|
||||||
labelXNearButton
|
labelXNearButton
|
||||||
|
|
||||||
val labelRight = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
labelRight = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||||
labelXNearButton
|
labelXNearButton
|
||||||
else
|
else
|
||||||
labelXAwayFromButton
|
labelXAwayFromButton
|
||||||
|
|
||||||
val labelTop = childY - mLabelsVerticalOffset + (fab.getMeasuredHeight()
|
labelTop = childY - mLabelsVerticalOffset + (fab.getMeasuredHeight() - label.getMeasuredHeight()) / 2
|
||||||
- label.getMeasuredHeight()) / 2
|
}
|
||||||
|
|
||||||
label.layout(
|
label.layout(
|
||||||
labelLeft,
|
labelLeft,
|
||||||
@ -738,6 +773,17 @@ class FloatingActionMenu @JvmOverloads constructor(
|
|||||||
label.setTextAppearance(getContext(), mLabelsStyle)
|
label.setTextAppearance(getContext(), mLabelsStyle)
|
||||||
label.setShowShadow(false)
|
label.setShowShadow(false)
|
||||||
label.setUsingStyle(true)
|
label.setUsingStyle(true)
|
||||||
|
} else {
|
||||||
|
// 💡 [수정] Center 옵션일 경우 배경 및 테두리 효과를 제거하여 내장 텍스트처럼 보이게 연출
|
||||||
|
if (mLabelsPosition == LABELS_POSITION_CENTER) {
|
||||||
|
label.setColors(Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT)
|
||||||
|
label.setShowShadow(false)
|
||||||
|
label.setCornerRadius(0)
|
||||||
|
label.updateBackground()
|
||||||
|
|
||||||
|
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, mLabelsTextSize)
|
||||||
|
label.setTextColor(mLabelsTextColor)
|
||||||
|
label.setPadding(0, 0, 0, 0)
|
||||||
} else {
|
} else {
|
||||||
label.setColors(mLabelsColorNormal, mLabelsColorPressed, mLabelsColorRipple)
|
label.setColors(mLabelsColorNormal, mLabelsColorPressed, mLabelsColorRipple)
|
||||||
label.setShowShadow(mLabelsShowShadow)
|
label.setShowShadow(mLabelsShowShadow)
|
||||||
@ -764,6 +810,7 @@ class FloatingActionMenu @JvmOverloads constructor(
|
|||||||
mLabelsPaddingLeft,
|
mLabelsPaddingLeft,
|
||||||
mLabelsPaddingTop
|
mLabelsPaddingTop
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (mLabelsMaxLines < 0 || mLabelsSingleLine) {
|
if (mLabelsMaxLines < 0 || mLabelsSingleLine) {
|
||||||
label.setSingleLine(mLabelsSingleLine)
|
label.setSingleLine(mLabelsSingleLine)
|
||||||
@ -1224,7 +1271,9 @@ class FloatingActionMenu @JvmOverloads constructor(
|
|||||||
private const val OPEN_UP = 0
|
private const val OPEN_UP = 0
|
||||||
private const val OPEN_DOWN = 1
|
private const val OPEN_DOWN = 1
|
||||||
|
|
||||||
private const val LABELS_POSITION_LEFT = 0
|
// 💡 [수정] 상수 공개 선언 및 CENTER 속성(2) 추가
|
||||||
private const val LABELS_POSITION_RIGHT = 1
|
const val LABELS_POSITION_LEFT = 0
|
||||||
|
const val LABELS_POSITION_RIGHT = 1
|
||||||
|
const val LABELS_POSITION_CENTER = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,12 +109,16 @@ object TaskAggregator {
|
|||||||
|
|
||||||
// [일괄 저장]
|
// [일괄 저장]
|
||||||
if (allFeeds.isNotEmpty()) {
|
if (allFeeds.isNotEmpty()) {
|
||||||
|
try {
|
||||||
WorkersDb.getRealm().write {
|
WorkersDb.getRealm().write {
|
||||||
// 1. 새 데이터 저장
|
// 1. 새 데이터 저장
|
||||||
allFeeds.forEach { feed ->
|
allFeeds.forEach { feed ->
|
||||||
copyToRealm(feed, UpdatePolicy.ALL)
|
try { copyToRealm(feed, UpdatePolicy.ERROR) }catch (e: Exception){}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}catch (e: Exception){}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Blog.LOGE("NewsFeed Aggregation finished in ${System.currentTimeMillis() - startTime}ms. Items: ${allFeeds.size}")
|
Blog.LOGE("NewsFeed Aggregation finished in ${System.currentTimeMillis() - startTime}ms. Items: ${allFeeds.size}")
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import bums.lunatic.launcher.utils.Blog
|
|||||||
import com.frostwire.jlibtorrent.swig.error_code
|
import com.frostwire.jlibtorrent.swig.error_code
|
||||||
import com.frostwire.jlibtorrent.swig.libtorrent
|
import com.frostwire.jlibtorrent.swig.libtorrent
|
||||||
import com.frostwire.jlibtorrent.swig.settings_pack
|
import com.frostwire.jlibtorrent.swig.settings_pack
|
||||||
|
import com.frostwire.jlibtorrent.swig.torrent_status
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.NonCancellable.isActive
|
import kotlinx.coroutines.NonCancellable.isActive
|
||||||
@ -41,8 +42,10 @@ import kotlinx.coroutines.launch
|
|||||||
data class TorrentTask(
|
data class TorrentTask(
|
||||||
val infoHash: String,
|
val infoHash: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val progress: Float, // 0.0 ~ 100.0
|
val progress: Float,
|
||||||
val isPaused: Boolean
|
val isPaused: Boolean,
|
||||||
|
val isQueued: Boolean, // 대기열 여부
|
||||||
|
val stateText: String
|
||||||
)
|
)
|
||||||
|
|
||||||
class TorrentService : Service() {
|
class TorrentService : Service() {
|
||||||
@ -87,6 +90,16 @@ class TorrentService : Service() {
|
|||||||
} else if (hasActiveTorrents && taskCount == 0) {
|
} else if (hasActiveTorrents && taskCount == 0) {
|
||||||
// 활성화된 적이 있었는데 0개가 되었다면 (모두 완료되어 이동되었거나 삭제됨)
|
// 활성화된 적이 있었는데 0개가 되었다면 (모두 완료되어 이동되었거나 삭제됨)
|
||||||
println("TorrentService: 모든 작업이 완료되어 서비스를 자동 종료합니다.")
|
println("TorrentService: 모든 작업이 완료되어 서비스를 자동 종료합니다.")
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (tempDir.exists()) {
|
||||||
|
tempDir.listFiles()?.forEach { it.deleteRecursively() }
|
||||||
|
println("TorrentService: 임시 다운로드 폴더 정리를 완료했습니다.")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
stopForeground(true) // 💡 상단바 알림 즉시 제거
|
stopForeground(true) // 💡 상단바 알림 즉시 제거
|
||||||
stopSelf() // 💡 서비스 스스로 종료 (메모리 해제)
|
stopSelf() // 💡 서비스 스스로 종료 (메모리 해제)
|
||||||
break // 무한 루프 탈출
|
break // 무한 루프 탈출
|
||||||
@ -103,6 +116,30 @@ class TorrentService : Service() {
|
|||||||
if (!handle.isValid) continue
|
if (!handle.isValid) continue
|
||||||
|
|
||||||
val status = handle.status()
|
val status = handle.status()
|
||||||
|
val state = status.state()
|
||||||
|
|
||||||
|
// 1. 현재 다운로드 대기 중(Queued)인지 확인
|
||||||
|
// 'auto_managed'이면서, 실제 'downloading' 상태가 아닌 경우를 체크합니다.
|
||||||
|
val isQueued = status.flags().and_(libtorrent.getAuto_managed()).nonZero() &&
|
||||||
|
(state.swig() == torrent_status.state_t.downloading_metadata.swigValue() ||
|
||||||
|
state.swig() == torrent_status.state_t.checking_files.swigValue() ||
|
||||||
|
state.swig() == torrent_status.state_t.checking_resume_data.swigValue())
|
||||||
|
|
||||||
|
// 2. 더 확실한 방법 (상태값이 'queued_for_checking' 등인 경우 포함)
|
||||||
|
// 실제 데이터 전송이 일어나지 않고 대기 순번을 기다리는 상태인지 확인
|
||||||
|
val isDownloading = (state.swig() == torrent_status.state_t.downloading.swigValue())
|
||||||
|
val isFinished = status.isFinished
|
||||||
|
val isPaused = status.flags().and_(libtorrent.getPaused()).nonZero()
|
||||||
|
val stateText = when {
|
||||||
|
status.flags().and_(libtorrent.getPaused()).nonZero() -> "일시정지"
|
||||||
|
state.swig() == torrent_status.state_t.checking_files.swigValue() -> "파일 검사 중"
|
||||||
|
state.swig() == torrent_status.state_t.downloading_metadata.swigValue() -> "메타데이터 수신 중"
|
||||||
|
state.swig() == torrent_status.state_t.downloading.swigValue() -> "다운로드 중"
|
||||||
|
state.swig() == torrent_status.state_t.finished.swigValue() || state.swig() == torrent_status.state_t.seeding.swigValue() -> "완료"
|
||||||
|
else -> "대기 중" // activeDownloads 제한에 걸려 대기하는 경우 여기로 들어옵니다.
|
||||||
|
}
|
||||||
|
// "일시정지도 아니고, 완료도 아닌데, 현재 다운로드 중도 아니면" -> 대기열 상태로 간주
|
||||||
|
val realIsQueued = !isPaused && !isFinished && !isDownloading
|
||||||
val hashStr = status.infoHash().toString()
|
val hashStr = status.infoHash().toString()
|
||||||
var rawName = status.name()
|
var rawName = status.name()
|
||||||
|
|
||||||
@ -133,7 +170,9 @@ class TorrentService : Service() {
|
|||||||
infoHash = hashStr,
|
infoHash = hashStr,
|
||||||
name = displayName,
|
name = displayName,
|
||||||
progress = status.progress() * 100f,
|
progress = status.progress() * 100f,
|
||||||
isPaused = status.flags().and_(com.frostwire.jlibtorrent.swig.libtorrent.getPaused()).nonZero()
|
isPaused = status.flags().and_(com.frostwire.jlibtorrent.swig.libtorrent.getPaused()).nonZero(),
|
||||||
|
isQueued = isQueued,
|
||||||
|
stateText = stateText
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -199,12 +238,24 @@ class TorrentService : Service() {
|
|||||||
sp.setBoolean(settings_pack.bool_types.enable_dht.swigValue(), true) // DHT 활성화
|
sp.setBoolean(settings_pack.bool_types.enable_dht.swigValue(), true) // DHT 활성화
|
||||||
// sp.setBoolean(settings_pack.bool_types.enable_pex.swigValue(), true) // Peer Exchange (피어 교환)
|
// sp.setBoolean(settings_pack.bool_types.enable_pex.swigValue(), true) // Peer Exchange (피어 교환)
|
||||||
sp.setBoolean(settings_pack.bool_types.enable_lsd.swigValue(), true) // 로컬 네트워크 탐색
|
sp.setBoolean(settings_pack.bool_types.enable_lsd.swigValue(), true) // 로컬 네트워크 탐색
|
||||||
|
sp.setBoolean(settings_pack.bool_types.dont_count_slow_torrents.swigValue(), true)
|
||||||
|
// 2. '느리다'고 판단할 다운로드 속도 기준 (예: 10KB/s 미만)
|
||||||
|
sp.setInteger(settings_pack.int_types.inactive_down_rate.swigValue(), 10 * 1024)
|
||||||
|
|
||||||
|
// 3. '느리다'고 판단할 업로드 속도 기준 (예: 5KB/s 미만)
|
||||||
|
sp.setInteger(settings_pack.int_types.inactive_up_rate.swigValue(), 5 * 1024)
|
||||||
|
|
||||||
|
// 4. 이 느린 속도가 몇 초간 지속될 때 대기열로 미룰 것인지 판단 (예: 20초)
|
||||||
|
sp.setInteger(settings_pack.int_types.inactivity_timeout.swigValue(), 20)
|
||||||
|
|
||||||
|
// [3] 얼마나 오래 느려야 대기열로 보낼지 결정 (초 단위)
|
||||||
|
sp.setInteger(settings_pack.int_types.auto_manage_interval.swigValue(), 20)
|
||||||
// [3] 모바일 맞춤형 연결 최적화
|
// [3] 모바일 맞춤형 연결 최적화
|
||||||
// 안드로이드는 데스크탑처럼 수천 개를 뚫으면 공유기가 뻗거나 앱 OOM이 발생할 수 있습니다.
|
// 안드로이드는 데스크탑처럼 수천 개를 뚫으면 공유기가 뻗거나 앱 OOM이 발생할 수 있습니다.
|
||||||
sp.connectionsLimit(400) // 글로벌 최대 연결 수 (기본값 보통 200)
|
sp.connectionsLimit(400) // 글로벌 최대 연결 수 (기본값 보통 200)
|
||||||
sp.activeDownloads(3) // 동시에 속도를 끌어올릴 다운로드 개수 (너무 많으면 속도가 분산됨)
|
sp.activeDownloads(4) // 동시에 속도를 끌어올릴 다운로드 개수 (너무 많으면 속도가 분산됨)
|
||||||
sp.activeLimit(5) // 활성 상태(업/다운 포함) 유지 최대 개수
|
sp.activeLimit(6) // 활성 상태(업/다운 포함) 유지 최대 개수
|
||||||
|
sp.activeSeeds(2)
|
||||||
|
|
||||||
// 세팅을 세션에 반영
|
// 세팅을 세션에 반영
|
||||||
session.applySettings(sp)
|
session.applySettings(sp)
|
||||||
@ -254,15 +305,14 @@ class TorrentService : Service() {
|
|||||||
println("TorrentService: 마그넷 파싱 에러 - ${error.message()} / URI: $magnetUri")
|
println("TorrentService: 마그넷 파싱 에러 - ${error.message()} / URI: $magnetUri")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
swigParams.max_connections = 150
|
swigParams.flags = swigParams.flags.or_(libtorrent.getAuto_managed())
|
||||||
|
swigParams.max_connections = Int.MAX_VALUE
|
||||||
// 다운로드/업로드 제한 해제 (기본값이 -1이긴 하나 명시적으로 박아줌)
|
// 다운로드/업로드 제한 해제 (기본값이 -1이긴 하나 명시적으로 박아줌)
|
||||||
swigParams.download_limit = -1
|
swigParams.download_limit = -1
|
||||||
swigParams.upload_limit = -1
|
swigParams.upload_limit = -1
|
||||||
|
|
||||||
// 참고: 만약 "순차 다운로드(동영상 스트리밍용)"가 필요하다면 아래 주석을 푸세요.
|
// 참고: 만약 "순차 다운로드(동영상 스트리밍용)"가 필요하다면 아래 주석을 푸세요.
|
||||||
// 단, 전체 다운로드 완료 속도는 일반 다운로드보다 느려집니다.
|
// 단, 전체 다운로드 완료 속도는 일반 다운로드보다 느려집니다.
|
||||||
// swigParams.flags = swigParams.flags.or_(libtorrent.getSequential_download())
|
// swigParams.flags = swigParams.flags.or_(libtorrent.getSequential_download())
|
||||||
|
|
||||||
swigParams.setSave_path(tempDir.absolutePath)
|
swigParams.setSave_path(tempDir.absolutePath)
|
||||||
session.swig().async_add_torrent(swigParams)
|
session.swig().async_add_torrent(swigParams)
|
||||||
|
|
||||||
@ -289,7 +339,10 @@ class TorrentService : Service() {
|
|||||||
|
|
||||||
/** 제어 기능 */
|
/** 제어 기능 */
|
||||||
fun pauseTorrent(infoHash: String) {
|
fun pauseTorrent(infoHash: String) {
|
||||||
session.find(Sha1Hash(infoHash))?.pause()
|
session.find(Sha1Hash(infoHash))?.let {
|
||||||
|
it.pause()
|
||||||
|
it.queuePositionBottom()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -125,23 +125,22 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/quickSearch"
|
android:id="@+id/quickSearch"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="30dp"
|
android:layout_height="32dp"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:background="@color/black"
|
android:background="@color/black"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
>
|
>
|
||||||
<androidx.appcompat.widget.AppCompatSpinner
|
<androidx.appcompat.widget.AppCompatSpinner
|
||||||
android:paddingLeft="5dp"
|
style="@style/SearchAccs"
|
||||||
android:paddingRight="5dp"
|
|
||||||
android:layout_marginLeft="6dp"
|
android:layout_marginLeft="6dp"
|
||||||
android:layout_marginRight="6dp"
|
android:layout_marginRight="6dp"
|
||||||
android:layout_marginTop="12dp"
|
android:paddingRight="5dp"
|
||||||
android:layout_marginBottom="12dp"
|
android:paddingLeft="5dp"
|
||||||
android:id="@+id/categorySpinner"
|
android:id="@+id/categorySpinner"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="30dp"
|
android:layout_height="match_parent"
|
||||||
android:spinnerMode="dropdown"
|
android:background="@color/black"
|
||||||
android:background="@drawable/base_bg"/>
|
android:spinnerMode="dropdown" />
|
||||||
<TextView
|
<TextView
|
||||||
app:autoSizeTextType="uniform"
|
app:autoSizeTextType="uniform"
|
||||||
style="@style/SearchAccs"
|
style="@style/SearchAccs"
|
||||||
|
|||||||
327
app/src/main/res/layout/fragment_system_status.xml
Normal file
327
app/src/main/res/layout/fragment_system_status.xml
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true"
|
||||||
|
android:background="#E6121212">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="32dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:text="System Status"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="26sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="#1AFFFFFF"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:layout_marginBottom="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Quick Controls"
|
||||||
|
android:textColor="#AAAAAA"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:layout_marginBottom="8dp"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:weightSum="3">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btnQuickWifi"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:text="📶 Wi-Fi"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btnQuickBt"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:text="🛜 Bluetooth"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btnQuickRinger"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:text="🔔 소리"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="☀️"
|
||||||
|
android:textSize="18sp"/>
|
||||||
|
<SeekBar
|
||||||
|
android:id="@+id/seekBrightness"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:max="255"
|
||||||
|
android:progressTint="#FFEB3B"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="🎵"
|
||||||
|
android:textSize="18sp"/>
|
||||||
|
<SeekBar
|
||||||
|
android:id="@+id/seekVolume"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:progressTint="#03A9F4"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="24dp">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Memory (RAM) - 터치 시 정리"
|
||||||
|
android:textColor="#AAAAAA"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textRam"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="RAM: 계산 중..."
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressRam"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="12dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:max="100"
|
||||||
|
android:progress="0"
|
||||||
|
android:progressTint="#4CAF50" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="24dp">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Internal Storage - 터치 시 관리"
|
||||||
|
android:textColor="#AAAAAA"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textStorage"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="Storage: 계산 중..."
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressStorage"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="12dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:max="100"
|
||||||
|
android:progress="0"
|
||||||
|
android:progressTint="#2196F3" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="24dp">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Battery & Thermal"
|
||||||
|
android:textColor="#AAAAAA"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textChargingStatus"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="Status: 상태 확인 중..."
|
||||||
|
android:textColor="#888888"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textBattery"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Battery: 계산 중..."
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textTemperature"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Temp: 0.0°C"
|
||||||
|
android:textColor="#FF9800"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
</LinearLayout>
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBattery"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="12dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:max="100"
|
||||||
|
android:progress="0"
|
||||||
|
android:progressTint="#FFC107" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="24dp">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Network & Connectivity"
|
||||||
|
android:textColor="#AAAAAA"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textNetworkState"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="Net: 확인 중..."
|
||||||
|
android:textColor="#2196F3"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textBluetoothState"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="BT: 확인 중..."
|
||||||
|
android:textColor="#03A9F4"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textDownload"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="↓ DL: 0 KB/s"
|
||||||
|
android:textColor="#4CAF50"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textUpload"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="↑ UL: 0 KB/s"
|
||||||
|
android:textColor="#F44336"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textMonthlyUsage"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:paddingVertical="4dp"
|
||||||
|
android:text="Monthly Usage: 권한 필요 (여기를 클릭)"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutTopApps"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textUptime"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="System Uptime: 00:00:00"
|
||||||
|
android:textColor="#888888"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
@ -11,13 +11,13 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvName"
|
android:id="@+id/tvName"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
tools:text="Ubuntu-22.04-desktop-amd64.iso"
|
tools:text="Ubuntu-22.04-desktop-amd64.iso"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:ellipsize="middle"
|
android:ellipsize="marquee"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/btnPauseResume"
|
app:layout_constraintEnd_toStartOf="@+id/btnPauseResume"
|
||||||
|
|||||||
@ -89,6 +89,7 @@
|
|||||||
app:menu_colorNormal="#80FF0000"
|
app:menu_colorNormal="#80FF0000"
|
||||||
app:menu_fab_size="mini"
|
app:menu_fab_size="mini"
|
||||||
app:menu_icon="@drawable/ic_add"
|
app:menu_icon="@drawable/ic_add"
|
||||||
|
app:menu_labels_position="center"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@ -102,6 +103,7 @@
|
|||||||
android:onClick="floatClick"
|
android:onClick="floatClick"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="20dp"/>
|
android:layout_height="20dp"/>
|
||||||
|
|
||||||
<bums.lunatic.launcher.view.FloatingActionButton
|
<bums.lunatic.launcher.view.FloatingActionButton
|
||||||
app:fab_label="booktoki"
|
app:fab_label="booktoki"
|
||||||
android:id="@+id/books"
|
android:id="@+id/books"
|
||||||
@ -160,6 +162,15 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="20dp"/>
|
android:layout_height="20dp"/>
|
||||||
|
|
||||||
|
<bums.lunatic.launcher.view.FloatingActionButton
|
||||||
|
app:fab_label="I"
|
||||||
|
android:id="@+id/btn_i"
|
||||||
|
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
|
<bums.lunatic.launcher.view.FloatingActionButton
|
||||||
app:fab_label="torrent"
|
app:fab_label="torrent"
|
||||||
android:id="@+id/btn_torrent"
|
android:id="@+id/btn_torrent"
|
||||||
@ -168,7 +179,14 @@
|
|||||||
android:onClick="floatClick"
|
android:onClick="floatClick"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="20dp"/>
|
android:layout_height="20dp"/>
|
||||||
|
<bums.lunatic.launcher.view.FloatingActionButton
|
||||||
|
app:fab_label="system"
|
||||||
|
android:id="@+id/btn_info"
|
||||||
|
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
|
<bums.lunatic.launcher.view.FloatingActionButton
|
||||||
app:fab_label="close"
|
app:fab_label="close"
|
||||||
android:id="@+id/close"
|
android:id="@+id/close"
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@android:id/text1"
|
android:id="@android:id/text1"
|
||||||
style="@style/SearchAccs"
|
android:layout_margin="10dp"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textColor="@color/bottom_option"
|
||||||
|
android:layout_height="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:background="@color/black"
|
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
android:singleLine="true" />
|
android:singleLine="true" />
|
||||||
@ -29,6 +29,7 @@
|
|||||||
<enum name="normal" value="0" />
|
<enum name="normal" value="0" />
|
||||||
<enum name="mini" value="1" />
|
<enum name="mini" value="1" />
|
||||||
</attr>
|
</attr>
|
||||||
|
|
||||||
<attr name="fab_showAnimation" format="reference" />
|
<attr name="fab_showAnimation" format="reference" />
|
||||||
<attr name="fab_hideAnimation" format="reference" />
|
<attr name="fab_hideAnimation" format="reference" />
|
||||||
<attr name="fab_label" format="string" />
|
<attr name="fab_label" format="string" />
|
||||||
@ -62,6 +63,7 @@
|
|||||||
<attr name="menu_labels_position" format="enum">
|
<attr name="menu_labels_position" format="enum">
|
||||||
<enum name="left" value="0" />
|
<enum name="left" value="0" />
|
||||||
<enum name="right" value="1" />
|
<enum name="right" value="1" />
|
||||||
|
<enum name="center" value="2" />
|
||||||
</attr>
|
</attr>
|
||||||
<attr name="menu_icon" format="reference" />
|
<attr name="menu_icon" format="reference" />
|
||||||
<attr name="menu_animationDelayPerItem" format="integer" />
|
<attr name="menu_animationDelayPerItem" format="integer" />
|
||||||
|
|||||||
@ -44,7 +44,7 @@
|
|||||||
<item name="android:layout_width">wrap_content</item>
|
<item name="android:layout_width">wrap_content</item>
|
||||||
<item name="android:layout_height">35dp</item>
|
<item name="android:layout_height">35dp</item>
|
||||||
<item name="android:gravity">center</item>
|
<item name="android:gravity">center</item>
|
||||||
<item name="android:textColor">@color/tabs_black</item>
|
<item name="android:textColor">@color/bottom_option</item>
|
||||||
<item name="android:visibility">visible</item>
|
<item name="android:visibility">visible</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="asdda" parent="Widget.Material3.Button.OutlinedButton">
|
<style name="asdda" parent="Widget.Material3.Button.OutlinedButton">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user