diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0f7bd25e..92c08fe6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -55,6 +55,7 @@
+
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt
index 8494fcfd..f00ee07a 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt
@@ -51,6 +51,10 @@ import bums.lunatic.launcher.common.CommonActivity
import bums.lunatic.launcher.databinding.LauncherActivityBinding
import bums.lunatic.launcher.feeds.WidgetHost
import bums.lunatic.launcher.helpers.ForeGroundService
+import bums.lunatic.launcher.helpers.ForeGroundService.Companion.ACTION_VIDEO_DOWNLOAD
+import bums.lunatic.launcher.helpers.ForeGroundService.Companion.EXTRA_MSGKEY
+import bums.lunatic.launcher.helpers.ForeGroundService.Companion.EXTRA_PROGRESS
+import bums.lunatic.launcher.helpers.ForeGroundService.Companion.EXTRA_TARGET_URL
import bums.lunatic.launcher.helpers.HeadsetActionButtonReceiver
import bums.lunatic.launcher.home.GeckoWeb
import bums.lunatic.launcher.home.NeoRssActivity
@@ -227,45 +231,73 @@ open class LauncherActivity : CommonActivity() {
// ๐ก URI๋ก๋ถํฐ ํ์ผ์ ์ฝ์ด์ ํ๋ผ์ด๋น ํด๋๋ก ๋ณต์ฌํ๋ ํจ์
private fun saveToPrivateVault(uri: Uri) {
+ var fileName = "shared_${System.currentTimeMillis()}"
CoroutineScope(Dispatchers.IO).launch {
try {
- // 1. ์๋ณธ ํ์ผ๋ช
์์๋ด๊ธฐ
- var fileName = "shared_${System.currentTimeMillis()}"
+ // 1. ์๋ณธ ํ์ผ๋ช
๋ฐ ์ด ํ์ผ ํฌ๊ธฐ(์ฉ๋) ์์๋ด๊ธฐ
+ var totalFileSize = 0L // ๐ก ํ์ผ ํฌ๊ธฐ๋ฅผ ๋ด์ ๋ณ์ ์ถ๊ฐ
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (nameIndex != -1) {
fileName = cursor.getString(nameIndex)
}
+ // ๐ก SIZE ์ปฌ๋ผ์ ํตํด ํ์ผ์ ์ ์ฒด ๋ฐ์ดํธ ํฌ๊ธฐ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
+ val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
+ if (sizeIndex != -1) {
+ totalFileSize = cursor.getLong(sizeIndex)
+ }
}
}
val vaultDir = File(getExternalFilesDir(null), "completed_torrents")
if (!vaultDir.exists()) vaultDir.mkdirs()
- // 2. ์ด๋ฆ ์ค๋ณต ๋ฎ์ด์ฐ๊ธฐ ๋ฐฉ์ง๋ฅผ ์ํด ํ์์คํฌํ ์ถ๊ฐ
val destFile = File(vaultDir, "${System.currentTimeMillis()}_$fileName")
-
- // 3. ์คํธ๋ฆผ ๋ณต์ฌ (์ธ๋ถ ํ์ผ -> ๋ด ํ๋ผ์ด๋น ํด๋)
+ notiState("${destFile.name} ๋ณต์ฌ ์์",0)
+ // 3. ์๋ ์คํธ๋ฆผ ๋ณต์ฌ ๋ฐ ์งํ๋ฅ ๊ณ์ฐ
contentResolver.openInputStream(uri)?.use { input ->
destFile.outputStream().use { output ->
- input.copyTo(output)
+ val buffer = ByteArray(8 * 1024) // 8KB์ฉ ์ชผ๊ฐ์ ๋ณต์ฌ
+ var bytesRead: Int
+ var copiedSize = 0L
+ var lastReportedProgress = -1
+
+ // ๐ก ํ์ผ ๋์ ๋๋ฌํ ๋๊น์ง 8KB์ฉ ๊ณ์ ์ฝ๊ณ ์๋๋ค.
+ while (input.read(buffer).also { bytesRead = it } >= 0) {
+ output.write(buffer, 0, bytesRead)
+ copiedSize += bytesRead
+
+ // ์งํ๋ฅ ๊ณ์ฐ ๋ก์ง (์ด ํฌ๊ธฐ๋ฅผ ์๋ ๊ฒฝ์ฐ์๋ง)
+ if (totalFileSize > 0) {
+ val progress = ((copiedSize.toDouble() / totalFileSize.toDouble()) * 100).toInt()
+
+ // 1% ๋จ์๋ก ๋ณ๊ฒฝ๋ ๋๋ง ์๋ฆผ์ ๊ฐฑ์ ํฉ๋๋ค (์์คํ
๊ณผ๋ถํ ๋ฐฉ์ง)
+ if (progress != lastReportedProgress) {
+ lastReportedProgress = progress
+ notiState("${destFile.name} ${progress}% ๋ณต์ฌ ์ค",progress)
+ }
+ }
+ }
}
}
-
- // 4. ์๋ฃ ํ UI ์ค๋ ๋์์ ํ ์คํธ ๋์ฐ๊ธฐ
- withContext(Dispatchers.Main) {
- showToast("๋ณด๊ดํจ์ ์ ์ฅ๋์์ต๋๋ค: $fileName")
- }
+ notiState("${destFile.name} ${100}% ๋ณต์ฌ ์๋ฃ",100)
} catch (e: Exception) {
e.printStackTrace()
- withContext(Dispatchers.Main) {
- showToast("ํ์ผ์ ๋ณด๊ดํจ์ผ๋ก ๋ณต์ฌํ์ง ๋ชปํ์ต๋๋ค.")
- }
+ notiState("${fileName} ๋ณต์ฌ ์คํจ : ${e.message}",0)
}
}
}
+ fun notiState(msg : String, progress : Int) {
+ val intent = Intent(this@LauncherActivity, ForeGroundService::class.java).apply {
+ action = ForeGroundService.ACTION_COPY_COMPLETE
+ putExtra(EXTRA_MSGKEY, msg)
+ putExtra(EXTRA_PROGRESS, progress)
+ }
+ startService(intent)
+ }
+
fun onSwipeLeft() {
showAppDrawer()
}
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt
index fccc34f3..660c0d60 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt
@@ -49,7 +49,10 @@ import java.util.concurrent.TimeUnit
import android.bluetooth.BluetoothClass
import android.media.AudioManager
import android.os.SystemClock
+import android.os.VibrationEffect
+import android.os.Vibrator
import android.view.KeyEvent
+import androidx.annotation.RequiresPermission
import kotlinx.coroutines.delay
class AggregatedSystemWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
@@ -85,10 +88,13 @@ class ForeGroundService : Service() {
companion object {
val ACTION_SENDMSG = "ACTION_SEND_TO_LOVE"
val EXTRA_MSGKEY = "SEND_MSG"
+ val EXTRA_PROGRESS = "EXTRA_PROGRESS"
val ACTION_VIDEO_DOWNLOAD = "ACTION_YTURL_DOWNLOAD"
val EXTRA_TARGET_URL = "ACTION_SEND_TO_LOVE"
val ACTION_SIT_DOWN = "ACTION_SIT_DOWN"
+ val ACTION_COPY_COMPLETE = "ACTION_COPY_COMPLETE"
+
val targetUrls = arrayListOf()
}
@@ -108,7 +114,7 @@ class ForeGroundService : Service() {
val filter = IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED)
registerReceiver(bluetoothreceiver, filter)
refreshFeeds()
- startForeGround()
+ startForeGround(vibrator = true)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -137,7 +143,10 @@ class ForeGroundService : Service() {
sitDownRequest
)
// ์ฌ์ฉ์ ํ์ธ์ ์ํด ์๋ฆผ ํ
์คํธ ๋ณ๊ฒฝ ๊ฐ๋ฅ
- startForeGround(0, 0, "45๋ถ ๋ค์ ์ง๋ ์๋ฆผ์ด ์์ฝ๋์์ต๋๋ค.")
+ startForeGround(max = 0, progress = 0, str = "45๋ถ ๋ค์ ์ง๋ ์๋ฆผ์ด ์์ฝ๋์์ต๋๋ค.")
+ }
+ ACTION_COPY_COMPLETE -> {
+ startForeGround(max = 100, progress = intent?.getIntExtra(EXTRA_PROGRESS,0) ?: 0, str = intent?.getStringExtra(EXTRA_MSGKEY))
}
else -> {
@@ -161,7 +170,7 @@ class ForeGroundService : Service() {
set(value) {
field = value
if (value == null) {
- startForeGround(0,0)
+ startForeGround(max= 0, progress = 0, vibrator = true)
}
}
@@ -181,7 +190,7 @@ class ForeGroundService : Service() {
currentProcessId = UUID.randomUUID().toString()
YoutubeDL.getInstance()
.execute(command, currentProcessId) { progress, est, str ->
- startForeGround(100, progress.toInt(),str)
+ startForeGround(100, progress.toInt(),str, true)
if (progress >= 100) {
targetUrls.remove(url)
currentProcessId = null
@@ -201,17 +210,40 @@ class ForeGroundService : Service() {
}
}
- fun startForeGround(max : Int = 0 , progress : Int = 0, str : String? = "์คํ์ค์
๋๋ค.") {
- Blog.LOGE(str?:"")
+ fun startForeGround(max : Int = 0, progress : Int = 0, str : String? = "์คํ์ค์
๋๋ค.", vibrator : Boolean? = false) {
+
+ val currentChannelId = if (vibrator == true) "${CHANNEL_ID}_vibrate" else "${CHANNEL_ID}_silent"
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val manager = getSystemService(NotificationManager::class.java)
+
val channel = NotificationChannel(
- CHANNEL_ID,
+ currentChannelId,
"BUM'S ์๋น์ค",
NotificationManager.IMPORTANCE_HIGH
- )
- val manager = getSystemService(NotificationManager::class.java)
+ ).apply {
+ if (vibrator == true) {
+ enableVibration(true)
+ vibrationPattern = longArrayOf(0, 500) // 0์ด ๋๊ธฐ ํ 0.5์ด ์ง๋
+ } else {
+ enableVibration(false)
+ setSound(null, null) // ์๋ฆฌ๋ ํ์คํ๊ฒ ์ ๊ฑฐ
+ }
+ }
manager.createNotificationChannel(channel)
}
+
+ // ๐ก 2. ํ๋ผ๋ฏธํฐ๊ฐ true์ผ ๋, ์๋ฆผ ์ฑ๋๊ณผ ๋ณ๊ฐ๋ก ์ง๋ ๋ชจํฐ๋ฅผ ์ง์ ํ ๋ฒ ๊ฐ์ ๋ก ์ธ๋ ค์ค๋๋ค.
+ // (ํ๋ฉด์ด ์ผ์ ธ ์๊ฑฐ๋ ์์คํ
์ค์ ์ ์ํด ์๋ฆผ ์ง๋์ด ๋ฌด์๋๋ ๊ฒฝ์ฐ๋ฅผ ๋๋นํ ํ์คํ ์ฅ์น)
+ if (vibrator == true) {
+ val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ vibratorService.vibrate(VibrationEffect.createOneShot(500, VibrationEffect.DEFAULT_AMPLITUDE))
+ } else {
+ vibratorService.vibrate(500)
+ }
+ }
+
val intent = Intent(this, LauncherActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
@@ -223,17 +255,24 @@ class ForeGroundService : Service() {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
- startForeground(NOTIF_ID, NotificationCompat.Builder(this, CHANNEL_ID)
+ // ๐ก 3. Builder์ currentChannelId ์ ์ฉ ๋ฐ ํ์ ๋ฒ์ (Oreo ๋ฏธ๋ง) ์ง๋ ํธํ์ฑ ์ถ๊ฐ
+ val builder = NotificationCompat.Builder(this, currentChannelId)
.setContentTitle("BUM'S ์๋น์ค")
.setContentText(str)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setSmallIcon(R.drawable.ic_b)
.setContentIntent(pendingIntent)
- .addAction(android.R.drawable.ic_btn_speak_now,"ํด๊ทผ", makeSendMsgAction(0,"๋ผ์ง ํด๊ทผํ๋ค์~!"))
+ .addAction(android.R.drawable.ic_btn_speak_now, "ํด๊ทผ", makeSendMsgAction(0, "๋ผ์ง ํด๊ทผํ๋ค์~!"))
.addAction(android.R.drawable.ic_menu_directions, "์์", makeSitDownAction())
- .setOngoing(true) // ์ฌ์ฉ์๊ฐ ์๋ฆผ์ ์ค์์ดํ๋ก ์ง์ธ ์ ์๊ฒ ๋ง๋ฆ
+ .setOngoing(true)
.setProgress(max, progress, false)
- .build())
+
+ // ์๋๋ก์ด๋ 8.0 ๋ฏธ๋ง ๊ธฐ๊ธฐ๋ฅผ ์ํ ์ง๋ ์ฒ๋ฆฌ
+ if (vibrator == true) {
+ builder.setDefaults(NotificationCompat.DEFAULT_VIBRATE)
+ }
+
+ startForeground(NOTIF_ID, builder.build())
}
fun makeSitDownAction(): PendingIntent {
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt
index 0da14a1d..ceec1aa5 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt
@@ -15,51 +15,45 @@ import android.widget.ImageView
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import bums.lunatic.launcher.R
-import bums.lunatic.launcher.utils.Blog
-import bums.lunatic.launcher.utils.CommonUtils.getFileTypeBySignature
+import bums.lunatic.launcher.utils.NaturalOrderComparator
import com.bumptech.glide.Glide
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import okhttp3.OkHttpClient
-import okhttp3.Request
import java.io.File
-import java.io.FileOutputStream
-// ํํฐ ๋ฐ ์ ๋ ฌ ๊ด๋ฆฌ๋ฅผ ์ํ Enum
-//"์ ์ฒด", "์ด๋ฏธ์ง", "์์", "๋ฌธ์", "๊ธฐํ"
enum class FileFilterType(val label : String) { ALL("์ ์ฒด"), IMAGE("์ด๋ฏธ์ง"), VIDEO("์์"), DOCUMENT("๋ฌธ์"), OTHER("๊ธฐํ") }
-enum class FileSortType(val label : String) { DOWNLOAD_DATE("๋ค์ด๋ก๋์ผ"), LAST_USED("์ต๊ทผ ์ฌ์ฉ"), FREQUENTLY_USED("์์ฃผ ์ฌ์ฉ"), SIZE("์ฉ๋") }
+enum class FileSortType(val label : String) { NAME("ํ์ผ๋ช
") , DOWNLOAD_DATE("๋ค์ด๋ก๋"), LAST_USED("์ต๊ทผ ์ฌ์ฉ"), FREQUENTLY_USED("์์ฃผ ์ฌ์ฉ"), SIZE("์ฉ๋") }
enum class FileViewMode { LIST_TEXT, LIST_THUMB, GRID_LARGE, GRID_SMALL }
-//val sortOptions = arrayOf("๋ค์ด๋ก๋์ผ", "์ต๊ทผ ์ฌ์ฉ", "์์ฃผ ์ฌ์ฉ", "์ฉ๋")
-
class CompletedFilesFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: CompletedFilesAdapter
- // ์๋ณธ ํ์ผ ๋ชฉ๋ก
+ private lateinit var rootDir: File
+ private lateinit var currentDir: File
private var allFiles = listOf()
- // ํ์ฌ ์ ํ๋ ์ํ๊ฐ
private var currentFilter = FileFilterType.ALL
- private var currentSort = FileSortType.DOWNLOAD_DATE
- private var currentViewMode = FileViewMode.LIST_TEXT // ๊ธฐ๋ณธ๊ฐ: ์ธ๋ค์ผ ๋ฆฌ์คํธ
- private var isDescending = true // ๊ธฐ๋ณธ: ๋ด๋ฆผ์ฐจ์ (์ต์ ์/ํฐ์ฉ๋์)
+ private var currentSort = FileSortType.NAME
+ private var currentViewMode = FileViewMode.LIST_TEXT
+ private var isDescending = true
- // ํ์ผ ํ์ฅ์ ๋ถ๋ฅ ๊ธฐ์ค
private val extImages = setOf("jpg", "jpeg", "png", "gif", "bmp", "webp")
private val extVideos = setOf("mp4", "mkv", "avi", "mov", "wmv", "flv", "webm", "ts")
private val extDocs = setOf("pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "hwp")
+ private lateinit var backPressedCallback: OnBackPressedCallback
+
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@@ -70,6 +64,17 @@ class CompletedFilesFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ rootDir = File(requireContext().getExternalFilesDir(null), "completed_torrents")
+ if (!rootDir.exists()) rootDir.mkdirs()
+ currentDir = rootDir
+
+ backPressedCallback = object : OnBackPressedCallback(false) {
+ override fun handleOnBackPressed() {
+ navigateUp()
+ }
+ }
+ requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressedCallback)
+
setupRecyclerView(view)
setupControls(view)
loadFiles()
@@ -77,20 +82,30 @@ class CompletedFilesFragment : Fragment() {
private fun setupRecyclerView(view: View) {
recyclerView = view.findViewById(R.id.recyclerViewCompletedFiles)
- recyclerView.layoutManager = LinearLayoutManager(requireContext())
+ updateRecyclerViewLayoutManager()
+ // ๐ก ์ด๋ํฐ ์์ฑ ์ getAccessCount ํจ์๋ฅผ ๋๊ฒจ์ฃผ์ด ์ด๋ํฐ ๋ด๋ถ์์ ์กฐํ์๋ฅผ ๊ทธ๋ฆด ์ ์๊ฒ ํฉ๋๋ค.
adapter = CompletedFilesAdapter(
onItemClick = { file ->
- openPrivateFile(requireContext(), file)
+ if (file.name == "..") {
+ navigateUp()
+ } else if (file.isDirectory) {
+ currentDir = file
+ loadFiles()
+ } else {
+ openPrivateFile(requireContext(), file)
+ }
},
onItemLongClick = { file ->
+ val type = if (file.isDirectory) "ํด๋" else "ํ์ผ"
android.app.AlertDialog.Builder(context)
- .setTitle("ํ์ผ ์ญ์ ")
- .setMessage("ํด๋น ํ์ผ์ ์ญ์ ํ์๊ฒ ์ต๋๊น?")
+ .setTitle("$type ์ญ์ ")
+ .setMessage("ํด๋น ${type}๋ฅผ ์ญ์ ํ์๊ฒ ์ต๋๊น?\n(ํด๋์ผ ๊ฒฝ์ฐ ๋ด๋ถ ํ์ผ๋ ๋ชจ๋ ์ญ์ ๋ฉ๋๋ค)")
.setPositiveButton("ํ์ธ") { _, _ ->
CoroutineScope(Dispatchers.IO).launch {
- if (file.delete()) {
- CoroutineScope(Dispatchers.Main).launch {
+ val success = if (file.isDirectory) file.deleteRecursively() else file.delete()
+ if (success) {
+ withContext(Dispatchers.Main) {
Toast.makeText(requireContext(), "์ญ์ ๋์์ต๋๋ค.", Toast.LENGTH_SHORT).show()
loadFiles()
}
@@ -99,18 +114,26 @@ class CompletedFilesFragment : Fragment() {
}
.setNegativeButton("์ทจ์", null)
.show()
-
- }
+ },
+ getAccessCount = { fileName -> getAccessCount(fileName) }
)
+ adapter.setViewMode(currentViewMode)
recyclerView.adapter = adapter
}
+ private fun navigateUp() {
+ if (currentDir.absolutePath != rootDir.absolutePath) {
+ currentDir = currentDir.parentFile ?: rootDir
+ loadFiles()
+ }
+ }
+
private fun getViewModeSymbolName(mode: FileViewMode): String {
return when (mode) {
- FileViewMode.LIST_TEXT -> "view_headline" // ํ
์คํธ ๋ฆฌ์คํธ
- FileViewMode.LIST_THUMB -> "view_list" // ์ธ๋ค์ผ ๋ฆฌ์คํธ
- FileViewMode.GRID_LARGE -> "grid_view" // ํฐ ๊ทธ๋ฆฌ๋
- FileViewMode.GRID_SMALL -> "apps" // ์์ ๊ทธ๋ฆฌ๋ (view_comfy ๋ ๊ฐ๋ฅ)
+ FileViewMode.LIST_TEXT -> "view_headline"
+ FileViewMode.LIST_THUMB -> "view_list"
+ FileViewMode.GRID_LARGE -> "grid_view"
+ FileViewMode.GRID_SMALL -> "apps"
}
}
@@ -118,12 +141,15 @@ class CompletedFilesFragment : Fragment() {
val spinnerFilter = view.findViewById(R.id.spinnerFilter)
val spinnerSort = view.findViewById(R.id.spinnerSort)
val tvSortOrder = view.findViewById(R.id.tvSortOrder)
-
val btnViewMode = view.findViewById(R.id.btnViewMode)
- // ์ด๊ธฐ ๋ทฐ ๋ชจ๋์ ๋ง๋ ์ด๋ชจ์ง ์ธํ
+ // ๐ก XML์ ์ถ๊ฐ๋ ์๋ ์ ๋ฆฌ ๋ฒํผ ์ฐ๋ (ID๋ฅผ btnOrganize๋ก ๋ง๋ค์๋ค๊ณ ๊ฐ์ )
+ val btnOrganize = view.findViewById(R.id.btnOrganize)
+ btnOrganize?.setOnClickListener {
+ organizeRootFiles()
+ }
+
btnViewMode.text = getViewModeSymbolName(currentViewMode)
- adapter.setViewMode(currentViewMode)
btnViewMode.setOnClickListener {
currentViewMode = when (currentViewMode) {
FileViewMode.LIST_TEXT -> FileViewMode.LIST_THUMB
@@ -133,13 +159,14 @@ class CompletedFilesFragment : Fragment() {
}
btnViewMode.text = getViewModeSymbolName(currentViewMode)
updateRecyclerViewLayoutManager()
- // ๋ชจ๋ ๋ณ๊ฒฝ์ ์ด๋ํฐ์ ์๋ฆผ
adapter.setViewMode(currentViewMode)
}
- // 1. ํํฐ ์คํผ๋ ์ค์
val filterOptions = FileFilterType.values().map { it.label }
- spinnerFilter.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, filterOptions)
+ spinnerFilter.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, filterOptions).apply {
+ setDropDownViewResource(R.layout.spinner_item_dark)
+ }
+
spinnerFilter.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
currentFilter = FileFilterType.values()[position]
@@ -148,9 +175,11 @@ class CompletedFilesFragment : Fragment() {
override fun onNothingSelected(p0: AdapterView<*>?) {}
}
- // 2. ์ ๋ ฌ ์คํผ๋ ์ค์
+
val sortOptions = FileSortType.values().map { it.label }
- spinnerSort.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, sortOptions)
+ spinnerSort.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, sortOptions).apply {
+ setDropDownViewResource(R.layout.spinner_item_dark)
+ }
spinnerSort.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
currentSort = FileSortType.values()[position]
@@ -159,36 +188,78 @@ class CompletedFilesFragment : Fragment() {
override fun onNothingSelected(p0: AdapterView<*>?) {}
}
- // 3. ์ ์/์ญ์ ํ ๊ธ ๋ฒํผ ์ค์
tvSortOrder.text = if (isDescending) "arrow_downward" else "arrow_upward"
tvSortOrder.setOnClickListener {
isDescending = !isDescending
- // ๋ด๋ฆผ์ฐจ์์ด๋ฉด ์๋ ํ์ดํ, ์ค๋ฆ์ฐจ์์ด๋ฉด ์ ํ์ดํ
tvSortOrder.text = if (isDescending) "arrow_downward" else "arrow_upward"
applyFilterAndSort()
}
}
- // ํ์ผ ๋ชฉ๋ก ๋ถ๋ฌ์ค๊ธฐ
+ // ๐ก ๋ฃจํธ ๊ฒฝ๋ก์ ์๋ ํ์ผ๋ค์ ๊ฐ๊ฐ์ ํด๋๋ก ์ด๋์ํค๋ ์๋ ์ ๋ฆฌ ํจ์
+ private fun organizeRootFiles() {
+ val filesInRoot = rootDir.listFiles()?.filter { it.isFile } ?: emptyList()
+
+ if (filesInRoot.isEmpty()) {
+ Toast.makeText(requireContext(), "๋ฃจํธ ํด๋์ ์ ๋ฆฌํ ํ์ผ์ด ์์ต๋๋ค.", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ var movedCount = 0
+ CoroutineScope(Dispatchers.IO).launch {
+ filesInRoot.forEach { file ->
+ val ext = file.extension.lowercase()
+ val folderName = when {
+ extImages.contains(ext) -> "์ด๋ฏธ์ง"
+ extVideos.contains(ext) -> "๋น๋์ค"
+ extDocs.contains(ext) -> "๋ฌธ์"
+ else -> "๊ธฐํ"
+ }
+
+ val targetDir = File(rootDir, folderName)
+ if (!targetDir.exists()) targetDir.mkdirs()
+
+ val targetFile = File(targetDir, file.name)
+ // ๋์ผํ ์ด๋ฆ์ ํ์ผ์ด ์ด๋ฏธ ์๋ค๋ฉด ๋ฎ์ด์ฐ๊ฑฐ๋ ์ด๋ ์คํจํ ์ ์์ผ๋ฏ๋ก ์ฃผ์ (ํ์์ ์ค๋ณต ๋ค์ด๋ฐ ์ฒ๋ฆฌ)
+ if (file.renameTo(targetFile)) {
+ movedCount++
+ }
+ }
+
+ withContext(Dispatchers.Main) {
+ if (movedCount > 0) {
+ Toast.makeText(requireContext(), "${movedCount}๊ฐ์ ํ์ผ์ ์๋ ์ ๋ฆฌํ์ต๋๋ค.", Toast.LENGTH_SHORT).show()
+ // ๋ง์ฝ ์ฌ์ฉ์๊ฐ ์ง๊ธ ๋ฃจํธ ํด๋๋ฅผ ๋ณด๊ณ ์๋ค๋ฉด ๋ฆฌ์คํธ๋ฅผ ์ฆ์ ๊ฐฑ์
+ if (currentDir.absolutePath == rootDir.absolutePath) {
+ loadFiles()
+ }
+ } else {
+ Toast.makeText(requireContext(), "ํ์ผ ์ ๋ฆฌ์ ์คํจํ์ต๋๋ค.", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+
private fun loadFiles() {
- val completedDir = File(requireContext().getExternalFilesDir(null), "completed_torrents")
- if (completedDir.exists()) {
- allFiles = completedDir.walkTopDown().filter { it.isFile }.toList()
+ if (currentDir.exists()) {
+ allFiles = currentDir.listFiles()?.toList() ?: emptyList()
} else {
allFiles = emptyList()
}
+ backPressedCallback.isEnabled = currentDir.absolutePath != rootDir.absolutePath
applyFilterAndSort()
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
- loadFiles()
+ if (!hidden) loadFiles()
}
- // ๐ก ํต์ฌ ๋ก์ง: ํํฐ ๋ฐ ์ ๋ ฌ ์ ์ฉ
private fun applyFilterAndSort() {
- // 1. ํํฐ๋ง
- var processedList = allFiles.filter { file ->
+ val folders = allFiles.filter { it.isDirectory }
+ var files = allFiles.filter { it.isFile }
+
+ files = files.filter { file ->
val ext = file.extension.lowercase()
when (currentFilter) {
FileFilterType.ALL -> true
@@ -198,86 +269,84 @@ class CompletedFilesFragment : Fragment() {
FileFilterType.OTHER -> !extImages.contains(ext) && !extVideos.contains(ext) && !extDocs.contains(ext)
}
}
-
- // 2. ์ ๋ ฌ
- processedList = when (currentSort) {
+ val naturalComparator = NaturalOrderComparator()
+ val sortedFolders = folders.sortedBy { it.name.lowercase() }
+ val sortedFiles = when (currentSort) {
FileSortType.DOWNLOAD_DATE -> {
- if (isDescending) processedList.sortedByDescending { it.lastModified() }
- else processedList.sortedBy { it.lastModified() }
+ if (isDescending) files.sortedByDescending { it.lastModified() }
+ else files.sortedBy { it.lastModified() }
}
FileSortType.LAST_USED -> {
- if (isDescending) processedList.sortedByDescending { getLastAccessedTime(it.name) }
- else processedList.sortedBy { getLastAccessedTime(it.name) }
+ if (isDescending) files.sortedByDescending { getLastAccessedTime(it.name) }
+ else files.sortedBy { getLastAccessedTime(it.name) }
}
FileSortType.SIZE -> {
- if (isDescending) processedList.sortedByDescending { it.length() }
- else processedList.sortedBy { it.length() }
+ if (isDescending) files.sortedByDescending { it.length() }
+ else files.sortedBy { it.length() }
}
FileSortType.FREQUENTLY_USED -> {
- if (isDescending) processedList.sortedByDescending { getAccessCount(it.name) }
- else processedList.sortedBy { getAccessCount(it.name) }
+ if (isDescending) files.sortedByDescending { getAccessCount(it.name) }
+ else files.sortedBy { getAccessCount(it.name) }
+ }
+ FileSortType.NAME -> {
+ if (isDescending) files.sortedWith(naturalComparator.reversed())
+ else files.sortedWith(naturalComparator)
}
}
- adapter.submitList(processedList)
+ val finalItems = mutableListOf()
+
+ if (currentDir.absolutePath != rootDir.absolutePath) {
+ finalItems.add(File(currentDir, ".."))
+ }
+
+ finalItems.addAll(sortedFolders)
+ finalItems.addAll(sortedFiles)
+
+ adapter.submitList(finalItems)
}
+
private fun trackFileAccess(fileName: String) {
val prefs = requireContext().getSharedPreferences("FileAccessTracker", Context.MODE_PRIVATE)
-
- // ๊ธฐ์กด ํ์๋ฅผ ๋ถ๋ฌ์์ 1์ ๋ํจ (์ฒ์ ์ด๋ฉด 0 + 1)
val currentCount = prefs.getInt("${fileName}_count", 0)
prefs.edit()
- .putLong(fileName, System.currentTimeMillis()) // ๊ธฐ์กด ์๊ฐ ๊ธฐ๋ก ์ ์ง
- .putInt("${fileName}_count", currentCount + 1) // ํ์ ๊ธฐ๋ก ์ถ๊ฐ
+ .putLong(fileName, System.currentTimeMillis())
+ .putInt("${fileName}_count", currentCount + 1)
.apply()
}
- // '์ต๊ทผ ์ฌ์ฉ ์๊ฐ' ๋ถ๋ฌ์ค๊ธฐ (๊ธฐ์กด ํจ์ ์ ์ง)
private fun getLastAccessedTime(fileName: String): Long {
val prefs = requireContext().getSharedPreferences("FileAccessTracker", Context.MODE_PRIVATE)
return prefs.getLong(fileName, 0L)
}
- // ๐ก '์ ๊ทผ ๋น๋(ํ์)' ๋ถ๋ฌ์ค๊ธฐ (์ ๊ท)
private fun getAccessCount(fileName: String): Int {
val prefs = requireContext().getSharedPreferences("FileAccessTracker", Context.MODE_PRIVATE)
return prefs.getInt("${fileName}_count", 0)
}
- // ๐ก ํ์ฅ์์ ๋ง์ถฐ ๋์ MIME ํ์
์์ฑ
private fun getMimeType(file: File): String {
val extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(file).toString()) ?: file.extension
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.lowercase()) ?: "*/*"
}
- // ์ธ๋ถ ํ๋ ์ด์ด/๋ทฐ์ด๋ก ํ์ผ ์ด๊ธฐ
private fun openPrivateFile(context: Context, file: File) {
try {
- // ํ์ผ์ ์ด์์ผ๋ฏ๋ก ์ต๊ทผ ์ฌ์ฉ ์๊ฐ ๊ฐฑ์
trackFileAccess(file.name)
- val uri: Uri = FileProvider.getUriForFile(
- context,
- "bums.lunatic.launcher.fileprovider",
- file
- )
+ // ๐ก ํ์ผ์ ์ด๊ณ ๋์ ์ฆ์ ์ ๊ทผ ํ์ ๋ฐ ๋ทฐ ๋ฐ์์ ์ํด ๋ค์ ์ ๋ ฌ/๋ก๋ํฉ๋๋ค.
+ loadFiles()
+ val uri: Uri = FileProvider.getUriForFile(context, "bums.lunatic.launcher.fileprovider", file)
val mimeType = getMimeType(file)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, mimeType)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
-
context.startActivity(intent)
- // ์ด๊ณ ๋์ '์ต๊ทผ ์ฌ์ฉ' ์์๊ฐ ๊ฐฑ์ ๋์ด์ผ ํ๋ฏ๋ก ๋ฆฌ์คํธ ๋ค์ ์ ๋ ฌ
- if (currentSort == FileSortType.LAST_USED) {
- applyFilterAndSort()
- }
-
} catch (e: IllegalArgumentException) {
- e.printStackTrace()
Toast.makeText(context, "ํ์ผ์ ๊ณต์ ํ ์ ์์ต๋๋ค.", Toast.LENGTH_SHORT).show()
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, "์ด ํ์ผ์ ์ด ์ ์๋ ์ฑ์ด ์ค์น๋์ด ์์ง ์์ต๋๋ค.", Toast.LENGTH_SHORT).show()
@@ -289,18 +358,18 @@ class CompletedFilesFragment : Fragment() {
FileViewMode.LIST_TEXT, FileViewMode.LIST_THUMB ->
LinearLayoutManager(requireContext())
FileViewMode.GRID_LARGE ->
- GridLayoutManager(requireContext(), 2) // 3์ด ๊ทธ๋ฆฌ๋
+ GridLayoutManager(requireContext(), 2)
FileViewMode.GRID_SMALL ->
- GridLayoutManager(requireContext(), 4) // 5์ด ๊ทธ๋ฆฌ๋
+ GridLayoutManager(requireContext(), 4)
}
}
-
}
// --- RecyclerView ์ด๋ํฐ ---
class CompletedFilesAdapter(
private val onItemClick: (File) -> Unit,
- private val onItemLongClick: (File) -> Unit
+ private val onItemLongClick: (File) -> Unit,
+ private val getAccessCount: (String) -> Int // ๐ก ์ ๊ทผ ๋น๋๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํ ๋๋ค ํจ์ ์ถ๊ฐ
) : RecyclerView.Adapter() {
private var fileList: List = emptyList()
@@ -313,10 +382,9 @@ class CompletedFilesAdapter(
fun setViewMode(mode: FileViewMode) {
viewMode = mode
- notifyDataSetChanged() // ๋ชจ๋๊ฐ ๋ฐ๋๋ฉด ์ ์ฒด ๋ฆฌ์คํธ๋ฅผ ๋ค์ ๊ทธ๋ฆฝ๋๋ค.
+ notifyDataSetChanged()
}
- // ๐ก ๋ชจ๋์ ๋ฐ๋ผ ViewType์ ๋ค๋ฅด๊ฒ ๋ฐํ
override fun getItemViewType(position: Int): Int {
return viewMode.ordinal
}
@@ -338,29 +406,49 @@ class CompletedFilesAdapter(
override fun getItemCount(): Int = fileList.size
inner class FileViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
- // ๋ ์ด์์์ ๋ฐ๋ผ ํน์ ๋ทฐ๊ฐ null์ผ ์ ์์ผ๋ฏ๋ก nullable(?)๋ก ์ ์ธ
private val tvFileName: TextView? = itemView.findViewById(R.id.tvFileName)
private val tvFileSize: TextView? = itemView.findViewById(R.id.tvFileSize)
private val ivThumb: ImageView? = itemView.findViewById(R.id.ivThumb)
fun bind(file: File, mode: FileViewMode) {
- tvFileName?.text = file.name
+ if (file.name == "..") {
+ tvFileName?.text = "๐ .. (์์ ํด๋๋ก)"
+ tvFileSize?.text = "์ด์ ๊ฒฝ๋ก"
- val sizeMb = file.length() / (1024.0 * 1024.0)
- tvFileSize?.text = String.format("%.2f MB โข %s", sizeMb, file.extension.uppercase())
+ if (ivThumb != null) {
+ Glide.with(itemView.context).clear(ivThumb)
+ ivThumb.setImageResource(android.R.drawable.ic_menu_revert)
+ }
+ } else if (file.isDirectory) {
+ tvFileName?.text = "๐ ${file.name}"
+ val count = file.listFiles()?.size ?: 0
+ tvFileSize?.text = "ํญ๋ชฉ $count ๊ฐ"
- // ๐ก ์ธ๋ค์ผ ๋ทฐ๊ฐ ์กด์ฌํ๋ ๋ชจ๋์ผ ๊ฒฝ์ฐ Glide๋ก ์ด๋ฏธ์ง ๋ก๋ฉ
- if (ivThumb != null) {
- // ์ด๋ฏธ์ง๋ ์์ ํ์ผ์ธ ๊ฒฝ์ฐ ์ธ๋ค์ผ ํ์, ์๋๋ฉด ๊ธฐ๋ณธ ์์ด์ฝ ๋ฑ ์ฒ๋ฆฌ ๊ฐ๋ฅ
- Glide.with(itemView.context)
- .load(file)
- .placeholder(android.R.drawable.ic_menu_report_image) // ๋ก๋ฉ ์ค ๊ธฐ๋ณธ ์ด๋ฏธ์ง
- .into(ivThumb)
+ if (ivThumb != null) {
+ Glide.with(itemView.context).clear(ivThumb)
+ ivThumb.setImageResource(android.R.drawable.ic_menu_gallery)
+ }
+ } else {
+ tvFileName?.text = file.name
+ val sizeMb = file.length() / (1024.0 * 1024.0)
+
+ // ๐ก ์ ๊ทผ ๋น๋๋ฅผ ๊ฐ์ ธ์ ํ
์คํธ์ ํฌํจ์ํต๋๋ค.
+ val accessCount = getAccessCount(file.name)
+ val countText = if (accessCount > 0) "์กฐํ: ${accessCount}ํ" else "์ ํ์ผ"
+
+ tvFileSize?.text = String.format("%.2f MB โข %s โข %s", sizeMb, file.extension.uppercase(), countText)
+
+ if (ivThumb != null) {
+ Glide.with(itemView.context)
+ .load(file)
+ .placeholder(android.R.drawable.ic_menu_report_image)
+ .into(ivThumb)
+ }
}
itemView.setOnClickListener { onItemClick(file) }
itemView.setOnLongClickListener {
- onItemLongClick(file)
+ if (file.name != "..") onItemLongClick(file)
true
}
}
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt
index 6c48f59e..e1ce12d1 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt
@@ -48,6 +48,11 @@ import io.realm.kotlin.Realm
import io.realm.kotlin.UpdatePolicy
import io.realm.kotlin.ext.copyFromRealm
import io.realm.kotlin.ext.query
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.json.JSONException
import org.json.JSONObject
import org.mozilla.gecko.util.ThreadUtils
@@ -60,6 +65,7 @@ import org.mozilla.geckoview.WebExtension.MessageDelegate
import org.mozilla.geckoview.WebExtension.PortDelegate
import org.mozilla.geckoview.WebExtensionController.AddonManagerDelegate
import org.mozilla.geckoview.WebRequestError
+import java.io.File
import java.lang.System.currentTimeMillis
import java.net.URL
import java.text.SimpleDateFormat
@@ -72,7 +78,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
// --- Configuration Properties (Arguments์์ ๋ก๋) ---
private lateinit var contentsType: String
private var lastNumber: Int = 0
- lateinit var webcontentsName: String
+ lateinit var webcontentsName: String
private lateinit var afterDot: String
private var useNumberInUrl: Boolean = true
private var enableGestures: Boolean = true
@@ -102,7 +108,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
var canGoBack: Boolean? = null
protected lateinit var binding: BooktokiBinding
-// var mPort: WebExtension.Port? = null
+ // var mPort: WebExtension.Port? = null
val mPortNam = "browser"
val extPath = "resource://android/assets/extensions/my_extension/"
val extId = "messaging@booktoki468.com"
@@ -351,8 +357,52 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
// Implementation typically empty or same across fragments
}
+ private fun saveTextToPrivateVault(folderName: String, chapterTitle: String, content: String) {
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ // 1. ์ต์์ ๋ณด๊ดํจ ํด๋
+ val baseVaultDir = File(requireContext().getExternalFilesDir(null), "completed_torrents")
+
+ // 2. ์์ฒญ๋ฐ์ ํ์ ํด๋(folderName) ์์ฑ
+ val targetDir = File(baseVaultDir, folderName)
+ if (!targetDir.exists()) {
+ targetDir.mkdirs()
+ }
+
+ // 3. ํ์ผ๋ช
์์ฑ (ํ์ผ ์์คํ
์์ ์๋ฌ๋ฅผ ๋ผ ์ ์๋ ํน์๋ฌธ์ \ / : * ? " < > | ์๋ ์ ๊ฑฐ)
+ val safeTitle = chapterTitle.replace(Regex("[\\\\/:*?\"<>|]"), "_")
+ val fileName = if (safeTitle.endsWith(".txt", ignoreCase = true)) safeTitle else "$safeTitle.txt"
+
+ val destFile = File(targetDir, fileName)
+
+ // 4. ํ
์คํธ ํ์ผ ์ฐ๊ธฐ (๋ฎ์ด์ฐ๊ธฐ)
+ destFile.writeText(content)
+
+ // 5. ์๋ฃ ํ ํ ์คํธ ๋์ฐ๊ธฐ
+ withContext(Dispatchers.Main) {
+ showToast("[$folderName] ํด๋์ ์ ์ฅ๋จ: $fileName")
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ withContext(Dispatchers.Main) {
+ showToast("ํ
์คํธ๋ฅผ ์ ์ฅํ์ง ๋ชปํ์ต๋๋ค.")
+ }
+ }
+ }
+ }
+
override fun onLongClick() {
if (!enableGestures) return
+ if (contentsType.contains("book") &&
+ currentPage?.bookTitle?.isNotEmpty() == true &&
+ currentPage?.chapterTitle?.isNotEmpty() == true &&
+ binding.pagedLayer.text.isNotEmpty() == true && (binding.pagedLayer.text.length > 100)) {
+ currentPage?.bookTitle?.let { title ->
+ currentPage?.contents?.let { contents ->
+ saveTextToPrivateVault(title, "${this.currentChapter}_${this.currentTitle}",binding.pagedLayer.text)
+ }
+ }
+ }
Blog.LOGD(log = "onLongClick")
}
@@ -479,6 +529,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
+ saveContinuation = false
if (hidden) {
// ๐ก ํ๋ฉด์์ ์ฌ๋ผ์ง ๋: ํ์ด๋จธ ์ค์ง ๋ฐ ์ ๋๋ฉ์ด์
์ค์ง
binding.lunaticBrowser.geckoWeb?.onPause()
@@ -497,7 +548,6 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
): View {
Blog.LOGD(log = "onCreate ${this::class.java.name} >> savedInstanceState ${savedInstanceState}")
binding = BooktokiBinding.inflate(inflater)
- binding = BooktokiBinding.inflate(inflater)
// ๐ก 1. Toki์ฉ ๋ ์ด์์ ์ค์
binding.lunaticBrowser.setupForToki()
@@ -652,11 +702,11 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
else -> {}
}
}
- it.decoViews.clear()
- it.decoViews.add(binding.lunaticBrowser.findViewById(R.id.tv_address))
- it.decoViews.add(binding.lunaticBrowser.findViewById(R.id.btn_back))
- it.decoViews.add(binding.lunaticBrowser.findViewById(R.id.btn_reload))
- it.decoViews.add(binding.lunaticBrowser.findViewById(R.id.btn_dl_video))
+ it.decoViews.clear()
+ it.decoViews.add(binding.lunaticBrowser.findViewById(R.id.tv_address))
+ it.decoViews.add(binding.lunaticBrowser.findViewById(R.id.btn_back))
+ it.decoViews.add(binding.lunaticBrowser.findViewById(R.id.btn_reload))
+ it.decoViews.add(binding.lunaticBrowser.findViewById(R.id.btn_dl_video))
it.restoreSessionState()
}
@@ -1051,6 +1101,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
fun actionNextEvent(fast: Boolean = false) {
if (binding.pagedLayer.isVisible && binding.pagedLayer.size() > 0 && (binding.pagedLayer.current() < binding.pagedLayer!!.size() - 1)) {
binding.pagedLayer.doNext(fast)
+ binding.lunaticBrowser.geckoWeb.pageDown()
updateLastInfo(binding.pagedLayer!!)
} else {
moveToNext(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)
@@ -1087,6 +1138,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
fun actionPrevEvent(fast: Boolean = false) {
if (binding.pagedLayer.isVisible && binding.pagedLayer.size() > 0 && binding.pagedLayer.current() > 0) {
binding.pagedLayer.doPrev(fast)
+ binding.lunaticBrowser.geckoWeb.pageUp()
updateLastInfo(binding.pagedLayer)
} else {
moveToPrev(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)
@@ -1143,11 +1195,23 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
}
}
if (saveContinuation) {
- binding.lunaticBrowser.geckoWeb.postDelayed({
- moveToNext(
- currentPage?.pathUrl ?: lastedUrl?.toUri()?.path
- )
- }, 10000)
+ CoroutineScope(Dispatchers.Main).launch {
+ binding.pagedLayer.visibility = View.INVISIBLE
+ binding.lunaticBrowser.visibility = View.VISIBLE
+ for (i in 0..24) {
+ if (binding.lunaticBrowser.geckoWeb.scrollState < 1) {
+ binding.lunaticBrowser.geckoWeb.pageDown()
+ }
+ delay(Random.nextLong(1500L, 3000L) + 1000L)
+ }
+ binding.lunaticBrowser.geckoWeb.postDelayed({
+ moveToNext(
+ currentPage?.pathUrl ?: lastedUrl?.toUri()?.path
+ )
+ binding.pagedLayer.visibility = View.INVISIBLE
+ binding.lunaticBrowser.visibility = View.VISIBLE
+ }, (Random.nextLong(1500L, 3000L) + 1000L))
+ }
}
}
}
@@ -1168,11 +1232,12 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
binding.lunaticBrowser.binding.internalProgressBar.visibility = GONE
}, 1000)
}
+
applyReaderConfig()
activity?.runOnUiThread {
view.text = contents
- view.visibility = VISIBLE
- binding.lunaticBrowser.visibility = GONE
+ view.visibility = if (!saveContinuation) VISIBLE else GONE
+ binding.lunaticBrowser.visibility = if (saveContinuation) VISIBLE else GONE
}
// view.forceUpdateUI()
lastedUrl?.let {
@@ -1181,6 +1246,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
HistoryManager.getBookPageInfo(contentsType,it) {
it?.let {
this@TokiFragment.currentPage = it
+
currentChapter = it?.chapterNum ?: 0
view.currentPage = it?.chapterNum ?: 0
HistoryManager.save(
@@ -1194,8 +1260,29 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
HistoryManager.getBooPageInfoContentsSave(contentsType,it, contents)
}
if (saveContinuation) {
- binding.lunaticBrowser.geckoWeb.postDelayed({moveToNext(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)}, 10000)
+ onLongClick()
+ CoroutineScope(Dispatchers.Main).launch {
+ binding.pagedLayer.visibility = View.INVISIBLE
+ binding.lunaticBrowser.visibility = View.VISIBLE
+ for (i in 0..24) {
+ if (saveContinuation && binding.lunaticBrowser.geckoWeb.scrollState < 1) {
+ binding.lunaticBrowser.geckoWeb.pageDown()
+ }
+ if (saveContinuation && binding.pagedLayer.isVisible && binding.pagedLayer.size() > 0 && (binding.pagedLayer.current() < binding.pagedLayer!!.size() - 1)) {
+ binding.pagedLayer.doNext(false)
+ updateLastInfo(binding.pagedLayer!!)
+ }
+ if (saveContinuation) {
+ delay(Random.nextLong(1500L, 3000L) + 1000L)
+ }
+ }
+ if (saveContinuation) {
+ binding.pagedLayer.visibility = View.VISIBLE
+ binding.lunaticBrowser.visibility = View.VISIBLE
+ moveToNext(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)
+ }
+ }
}
}
}
@@ -1203,6 +1290,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
}
}
+
// Log.i(TAG, "onLoadedContents >> " + aContents)
}
@@ -1229,24 +1317,95 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
) { dialog, which -> dialog.cancel() }
builder.show()
}
- var testRegex = """[^0-9]""".toRegex()
- Blog.LOGI(TAG, "onFindTitle >> " + contents + " ::: ${testRegex.replace(contents, "")}")
- if (contents.contains("-")) {
- currentTitle = contents.split("-")[0]
- try {
- currentChapter = testRegex.replace(contents.split("-")[1], "").toInt()
- } catch (e: Exception) {
- currentChapter = 0
+ // --- ์ฌ๊ธฐ์๋ถํฐ ํ์ฑ ๋ก์ง ๊ฐ์ ---
+ val cleanContents = contents.trim()
+
+ // 1. "์ซ์ + ํ/ํธ/ํ/์ฅ" ํจํด์ ์ฐพ์ต๋๋ค. (์: "1ํ ์ด๋๊ตฌ", "์์ค์ ๋ชฉ 15ํ", "123ํธ")
+ val chapterPattern = Regex("""(\d+)\s*(?:ํ|ํธ|ํ|์ฅ)""")
+ val matchResult = chapterPattern.find(cleanContents)
+
+ if (matchResult != null) {
+ // ์ ๊ท์์ผ๋ก ์ฐพ์ ์ซ์ ๋ถ๋ถ๋ง ์ถ์ถ (์: "15ํ" -> 15)
+ currentChapter = matchResult.groupValues[1].toInt()
+
+ // ๋งค์นญ๋ ๋ถ๋ถ(์: "1ํ")์ ๊ธฐ์ค์ผ๋ก ์๋ค๋ฅผ ๋ถ๋ฆฌํฉ๋๋ค.
+ val beforeChapter = cleanContents.substring(0, matchResult.range.first).trim()
+ val afterChapter = cleanContents.substring(matchResult.range.last + 1).trim()
+
+ // "1ํ ์ด๋๊ตฌ" ์ฒ๋ผ ์์ ํ
์คํธ๊ฐ ์์ผ๋ฉด ๋ค์ ์๋ "์ด๋๊ตฌ"๋ฅผ ํ์ดํ๋ก ์ฌ์ฉ
+ // "์์ค์ ๋ชฉ 1ํ" ์ฒ๋ผ ์์ ํ
์คํธ๊ฐ ์์ผ๋ฉด ์๋ถ๋ถ์ ํ์ดํ๋ก ์ฌ์ฉ
+ currentTitle = if (beforeChapter.isNotEmpty()) {
+ beforeChapter.trimEnd('-', ' ', ':')
+ } else if (afterChapter.isNotEmpty()) {
+ afterChapter.trimStart('-', ' ', ':')
+ } else {
+ "์ ๋ชฉ์์"
}
- } else if (testRegex.replace(contents, "").length > 0) {
- currentChapter = testRegex.replace(contents, "").toInt()
- currentTitle = contents.split(testRegex.replace(contents, ""))[0]
- } else {
- val dateFormat = "yyyyMMdd-HH"
- val date = Date(currentTimeMillis())
- val simpleDateFormat = SimpleDateFormat(dateFormat)
- currentTitle = simpleDateFormat.format(date)
}
+ // 2. "ํ" ๊ฐ์ ๊ธ์๊ฐ ์๊ณ ํ์ดํ(-)๋ง ์๋ ๊ฒฝ์ฐ (์: "์์ค์ ๋ชฉ - 15")
+ // 2. "ํ" ๊ฐ์ ๊ธ์๊ฐ ์๊ณ ํ์ดํ(-)๋ง ์๋ ๊ฒฝ์ฐ (์: "์์ค์ ๋ชฉ - 15" ๋๋ "15 - ์์ค์ ๋ชฉ")
+ else if (cleanContents.contains("-")) {
+ // ์ ๋ชฉ ์์ฒด์ ํ์ดํ์ด ๋ค์ด์์ ์ ์์ผ๋ฏ๋ก(์: "๋ ๋ฒจ-์
- 15") ๊ฐ์ฅ ๋ง์ง๋ง ํ์ดํ์ ๊ธฐ์ค์ผ๋ก ์๋ฆ
๋๋ค.
+ val lastDashIndex = cleanContents.lastIndexOf("-")
+ val frontPart = cleanContents.substring(0, lastDashIndex).trim()
+ val backPart = cleanContents.substring(lastDashIndex + 1).trim()
+
+ // ๊ฐ ํํธ์์ ์์ ๋ฌธ์(ํ๊ธ/์์ด)๋ง ๋จ๊น (๊ธธ์ด ๋น๊ต์ฉ)
+ val textOnlyFront = frontPart.replace(Regex("""[^a-zA-Z๊ฐ-ํฃ]"""), "")
+ val textOnlyBack = backPart.replace(Regex("""[^a-zA-Z๊ฐ-ํฃ]"""), "")
+
+ // ๊ฐ ํํธ์์ ์ซ์๋ง ์ถ์ถ
+ val numFront = frontPart.replace(Regex("""[^0-9]"""), "")
+ val numBack = backPart.replace(Regex("""[^0-9]"""), "")
+
+ // ์ผ์ด์ค A: ๋ท๋ถ๋ถ์ ๋ฌธ์(๊ธ์)๊ฐ ์๊ณ ์ซ์๋ง ์๋ค๋ฉด -> ์์ด ์ ๋ชฉ, ๋ค๊ฐ ์ฑํฐ ("์์ค์ ๋ชฉ - 15")
+ if (textOnlyBack.isEmpty() && numBack.isNotEmpty()) {
+ currentTitle = frontPart
+ currentChapter = numBack.toIntOrNull() ?: 0
+ }
+ // ์ผ์ด์ค B: ์๋ถ๋ถ์ ๋ฌธ์(๊ธ์)๊ฐ ์๊ณ ์ซ์๋ง ์๋ค๋ฉด -> ์์ด ์ฑํฐ, ๋ค๊ฐ ์ ๋ชฉ ("15 - ์์ค์ ๋ชฉ")
+ else if (textOnlyFront.isEmpty() && numFront.isNotEmpty()) {
+ currentTitle = backPart
+ currentChapter = numFront.toIntOrNull() ?: 0
+ }
+ // ์ผ์ด์ค C: ์์ชฝ ๋ค ๋ฌธ์๊ฐ ์์ฌ ์๋ค๋ฉด ("์์ฆ2 - 15(๋ฒ์ธ)") -> ๋ฌธ์๊ฐ ๋ ๊ธด ์ชฝ์ ์ ๋ชฉ์ผ๋ก ๊ฐ์ฃผ
+ else {
+ if (textOnlyFront.length >= textOnlyBack.length) {
+ currentTitle = frontPart
+ currentChapter = numBack.toIntOrNull() ?: 0
+ } else {
+ currentTitle = backPart
+ currentChapter = numFront.toIntOrNull() ?: 0
+ }
+ }
+ }
+ // 3. ์์ ํจํด์ ๋ชจ๋ ์ ๋ง๋ ๊ฒฝ์ฐ (๋ง์ง๋ง ์์ ์ฅ์น)
+ else {
+ val numberString = cleanContents.replace(Regex("""[^0-9]"""), "")
+ if (numberString.isNotEmpty()) {
+ try {
+ currentChapter = numberString.toInt()
+ currentTitle = cleanContents.replace(numberString, "").trim()
+ } catch (e: Exception) {
+ currentChapter = 0
+ currentTitle = cleanContents
+ }
+ } else {
+ // ์์ ์ซ์๊ฐ ์๋ ๊ฒฝ์ฐ ํ์์คํฌํ ์ฌ์ฉ
+ currentChapter = 0
+ val dateFormat = "yyyyMMdd-HH"
+ val simpleDateFormat = SimpleDateFormat(dateFormat)
+ currentTitle = simpleDateFormat.format(Date(System.currentTimeMillis()))
+ }
+ }
+
+ // ์ต์ข
์ ์ผ๋ก ํ์ดํ์ด ๋น์ด์๊ฑฐ๋ ๊ธฐํธ๋ง ๋จ์๋ค๋ฉด ๊ธฐ๋ณธ๊ฐ ์ฒ๋ฆฌ
+ if (currentTitle.isEmpty() || currentTitle.replace(Regex("""[^a-zA-Z0-9๊ฐ-ํฃ]"""), "").isEmpty()) {
+ currentTitle = "์ ๋ชฉ์์"
+ currentChapter = 0
+ }
+
+ Blog.LOGI(TAG, "onFindTitle Result >> Title: [$currentTitle], Chapter: [$currentChapter]")
}
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/view/PagedTextLayout.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/view/PagedTextLayout.kt
index 4d32474e..9f0e282a 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/view/PagedTextLayout.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/view/PagedTextLayout.kt
@@ -177,7 +177,7 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
hanler.removeCallbacks(touchTimeover)
setOnLongClickListener { v ->
mPagedTextViewInterface?.onLongClick()
- return@setOnLongClickListener false
+ return@setOnLongClickListener true
}
setOnTouchListener(SimpleFingerGestures(omfgl = object : SimpleFingerGestures.OnFingerGestureListener{
@@ -240,7 +240,7 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
gestureDuration: Long,
gestureDistance: Double
): Boolean {
- mPagedTextViewInterface?.onLongClick()
+// mPagedTextViewInterface?.onLongClick()
return true
}
@@ -249,9 +249,9 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
fingers: Int
): Boolean {
hanler.removeCallbacks(touchTimeover)
- mPagedTextViewInterface?.onLongClick()
+// mPagedTextViewInterface?.onLongClick()
hanler?.postDelayed(touchTimeover, 3000L)
- return true
+ return false
}
override fun onLongPress(
@@ -386,6 +386,7 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
}
}
+
fun doPrev(fast : Boolean = false) {
if (fast) {
setPageBy(if((this@PagedTextLayout.currentPage - getFastPageCount()) >= 0) {
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/utils/CompressStringUtil.kt b/app/src/main/kotlin/bums/lunatic/launcher/utils/CompressStringUtil.kt
index 7f8f7737..3b5b35e1 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/utils/CompressStringUtil.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/utils/CompressStringUtil.kt
@@ -2,6 +2,7 @@ package bums.lunatic.launcher.utils
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
+import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
@@ -96,4 +97,41 @@ object CompressStringUtil {
}
return bytes
}
+
+}
+class NaturalOrderComparator : Comparator {
+ override fun compare(f1: File, f2: File): Int {
+ // ์ซ์์ ๋ฌธ์์ ๊ฒฝ๊ณ๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ฌธ์์ด์ ์ชผ๊ฐ๋ ์ ๊ท์
+ val splitRegex = Regex("(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)")
+ val parts1 = f1.name.split(splitRegex)
+ val parts2 = f2.name.split(splitRegex)
+
+ val limit = minOf(parts1.size, parts2.size)
+ for (i in 0 until limit) {
+ val p1 = parts1[i]
+ val p2 = parts2[i]
+
+ if (p1 == p2) continue
+
+ val isNum1 = p1.all { it.isDigit() }
+ val isNum2 = p2.all { it.isDigit() }
+
+ if (isNum1 && isNum2) {
+ // ๋ ๋ค ์ซ์๋ฉด ์์ 0์ ๋ผ๊ณ ์๋ฆฌ์ ๋จผ์ ๋น๊ต ํ ๋ฌธ์์ด ๋น๊ต
+ val num1 = p1.trimStart('0')
+ val num2 = p2.trimStart('0')
+ val cmp = if (num1.length != num2.length) {
+ num1.length.compareTo(num2.length)
+ } else {
+ num1.compareTo(num2)
+ }
+ if (cmp != 0) return cmp
+ } else {
+ // ์ซ์๊ฐ ์๋๋ฉด ์ผ๋ฐ ๋ฌธ์์ด(๋์๋ฌธ์ ๋ฌด์) ๋น๊ต
+ val strCmp = p1.compareTo(p2, ignoreCase = true)
+ if (strCmp != 0) return strCmp
+ }
+ }
+ return parts1.size.compareTo(parts2.size)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/RuliWebGetter.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/RuliWebGetter.kt
index 9f0ea018..2a26b4fe 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/workers/RuliWebGetter.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/RuliWebGetter.kt
@@ -81,10 +81,7 @@ class RuliWebGetter(context: Context) : BaseGetter(context) {
.header("pragma", "no-cache")
.ignoreContentType(true)
.get().let { ruli ->
-// Blog.LOGE(TAG.plus("test ${testUrl2} >> ${ruli.title()}"))
- ruli.getElementsByClass("table_body blocktarget").forEach { ruli_tr ->
- parseRuli(ruli_tr)
- }
+ ruli.getElementsByClass("table_body blocktarget").forEach { ruli_tr ->}
}
}
} catch (e:Exception){e.printStackTrace()}}
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt
index af38fda7..58e2f2e7 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt
@@ -240,7 +240,7 @@ class TorrentService : Service() {
if (tasks.isNotEmpty()) {
updateNotification(tasks)
}
- delay(1000)
+ delay(15000)
}
}
}
diff --git a/app/src/main/res/layout/fragment_completed_files.xml b/app/src/main/res/layout/fragment_completed_files.xml
index 029d558f..a99608ef 100644
--- a/app/src/main/res/layout/fragment_completed_files.xml
+++ b/app/src/main/res/layout/fragment_completed_files.xml
@@ -4,16 +4,6 @@
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/windowBackground">
-
-
-
+
+
+
+
+
-
-
+ android:background="@android:color/transparent"
+ android:spinnerMode="dropdown"
+ />
+
+