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" + /> + +