diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2c3996b0..0f7bd25e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -125,7 +125,15 @@
-
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/BookmarkUploader.kt b/app/src/main/kotlin/bums/lunatic/launcher/BookmarkUploader.kt
index db6afad6..9b0311da 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/BookmarkUploader.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/BookmarkUploader.kt
@@ -2,6 +2,7 @@ package bums.lunatic.launcher
// ui/bookmark/BookmarkUploader.kt
+import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import bums.lunatic.launcher.home.adapters.VoteResponse
@@ -23,6 +24,7 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.logging.HttpLoggingInterceptor
+import java.io.File
object OkHttpClientInstance {
val client: OkHttpClient by lazy {
@@ -163,6 +165,7 @@ object BookmarkUploader {
fun saveBookmarkWithImageUpload(
+ context: Context, // ๐ก ํ๋ผ๋ฏธํฐ ์ถ๊ฐ
pageUrl: String,
selectedImageUrl: String,
comment: String,
@@ -182,6 +185,9 @@ object BookmarkUploader {
return@launch
}
+
+ // ๐ก ๋ค์ด๋ก๋ ์๋ฃ ์งํ ๋ก์ปฌ ํด๋์ ๋จผ์ ์ ์ฅ
+ saveImageToLocal(context, imageData, selectedImageUrl)
// --- 2๋จ๊ณ: Multipart ์์ฒญ ์์ฑ ๋ฐ ์
๋ก๋ (๊ธฐ์กด๊ณผ ๋์ผ) ---
val bookmarkDataMap = mapOf(
"url" to pageUrl,
@@ -224,6 +230,28 @@ object BookmarkUploader {
}
}
+ private fun saveImageToLocal(context: Context, imageData: ByteArray, imageUrl: String) {
+ try {
+ val saveDir = File(context.getExternalFilesDir(null), "completed_torrents")
+ if (!saveDir.exists()) {
+ saveDir.mkdirs()
+ }
+
+ // URL์์ ์๋ณธ ํ์ผ๋ช
์ถ์ถ ์๋ (์คํจ ์ ํ์์คํฌํ๋ก ๋์ฒด)
+ var fileName = android.net.Uri.parse(imageUrl).lastPathSegment ?: "img_${System.currentTimeMillis()}.jpg"
+
+ // ์ด๋ฆ์ด ์ค๋ณต๋์ด ๋ฎ์ด์จ์ง๋ ๊ฒ์ ๋ง๊ธฐ ์ํด ํ์์คํฌํ๋ฅผ ์์ ๋ถ์ฌ์ค๋๋ค.
+ fileName = "${System.currentTimeMillis()}_$fileName"
+
+ val file = File(saveDir, fileName)
+ file.writeBytes(imageData)
+ println("โ
๋ก์ปฌ ๋ณด๊ดํจ ์ ์ฅ ์ฑ๊ณต: ${file.absolutePath}")
+
+ } catch (e: Exception) {
+ println("๐ฅ ๋ก์ปฌ ๋ณด๊ดํจ ์ ์ฅ ์คํจ: ${e.message}")
+ }
+ }
+
/**
* ์ฃผ์ด์ง URL์์ ์ด๋ฏธ์ง๋ฅผ ๋ค์ด๋ก๋ํ์ฌ ByteArray๋ก ๋ฐํํ๋ ํฌํผ ํจ์ (Referer ์ถ๊ฐ)
*/
@@ -258,6 +286,7 @@ object BookmarkUploader {
* @param visibility ๊ณต๊ฐ ๋ฒ์ (PUBLIC, MEMBERS, PRIVATE)
*/
fun saveBookmarkWithContent(
+ context: Context, // ๐ก ํ๋ผ๋ฏธํฐ ์ถ๊ฐ
pageUrl: String,
imageUrls: List,
comment: String,
@@ -272,14 +301,21 @@ object BookmarkUploader {
try {
// --- 1๋จ๊ณ: URL ๋ชฉ๋ก์ ๋ชจ๋ ์ด๋ฏธ์ง๋ฅผ ๋ณ๋ ฌ๋ก ๋ค์ด๋ก๋ ---
val downloadedImages = imageUrls.map { imageUrl ->
- async { downloadImage(imageUrl, pageUrl) }
- }.awaitAll().filterNotNull() // ๋ค์ด๋ก๋ ์ฑ๊ณตํ ๊ฒ๋ง ํํฐ๋ง
+ async {
+ val bytes = downloadImage(imageUrl, pageUrl)
+ if (bytes != null) Pair(imageUrl, bytes) else null
+ }
+ }.awaitAll().filterNotNull()
if (downloadedImages.isEmpty()) {
println("โ ๋ชจ๋ ์ด๋ฏธ์ง ๋ค์ด๋ก๋์ ์คํจํ์ต๋๋ค.")
return@launch
}
+ downloadedImages.forEach { (imageUrl, imageData) ->
+ saveImageToLocal(context, imageData, imageUrl)
+ }
+
// --- 2๋จ๊ณ: Multipart ์์ฒญ ์์ฑ ---
val bookmarkDataMap = mapOf(
"url" to pageUrl,
@@ -294,9 +330,9 @@ object BookmarkUploader {
.addFormDataPart("bookmarkData", bookmarkDataJson)
// ๋ค์ด๋ก๋๋ ๊ฐ ์ด๋ฏธ์ง๋ฅผ 'files'๋ผ๋ ํํธ ์ด๋ฆ์ผ๋ก ์ถ๊ฐ
- downloadedImages.forEachIndexed { index, imageData ->
+ downloadedImages.forEachIndexed { index, (_, imageData) ->
multipartBodyBuilder.addFormDataPart(
- "files", // ์๋ฒ์ @RequestPart("files")์ ์ผ์นํด์ผ ํจ
+ "files",
"image_$index.jpg",
imageData.toRequestBody("image/jpeg".toMediaTypeOrNull())
)
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt
index 83785534..8494fcfd 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt
@@ -18,6 +18,7 @@ import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
+import android.provider.OpenableColumns
import android.provider.Settings
import android.view.GestureDetector
import android.view.KeyEvent
@@ -69,11 +70,13 @@ import io.realm.kotlin.ext.query
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.json.JSONObject
import org.mozilla.geckoview.ExperimentDelegate
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings
+import java.io.File
import java.util.Calendar
import java.util.Date
import kotlin.jvm.java
@@ -212,6 +215,57 @@ open class LauncherActivity : CommonActivity() {
} catch (e: Exception) { e.printStackTrace() }
}
+ private fun handleSharedIntent(intent: Intent) {
+ if (intent.action == Intent.ACTION_SEND) {
+ val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM)
+ uri?.let { saveToPrivateVault(it) }
+ } else if (intent.action == Intent.ACTION_SEND_MULTIPLE) {
+ val uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
+ uris?.forEach { saveToPrivateVault(it) }
+ }
+ }
+
+ // ๐ก URI๋ก๋ถํฐ ํ์ผ์ ์ฝ์ด์ ํ๋ผ์ด๋น ํด๋๋ก ๋ณต์ฌํ๋ ํจ์
+ private fun saveToPrivateVault(uri: Uri) {
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ // 1. ์๋ณธ ํ์ผ๋ช
์์๋ด๊ธฐ
+ var fileName = "shared_${System.currentTimeMillis()}"
+ 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)
+ }
+ }
+ }
+
+ val vaultDir = File(getExternalFilesDir(null), "completed_torrents")
+ if (!vaultDir.exists()) vaultDir.mkdirs()
+
+ // 2. ์ด๋ฆ ์ค๋ณต ๋ฎ์ด์ฐ๊ธฐ ๋ฐฉ์ง๋ฅผ ์ํด ํ์์คํฌํ ์ถ๊ฐ
+ val destFile = File(vaultDir, "${System.currentTimeMillis()}_$fileName")
+
+ // 3. ์คํธ๋ฆผ ๋ณต์ฌ (์ธ๋ถ ํ์ผ -> ๋ด ํ๋ผ์ด๋น ํด๋)
+ contentResolver.openInputStream(uri)?.use { input ->
+ destFile.outputStream().use { output ->
+ input.copyTo(output)
+ }
+ }
+
+ // 4. ์๋ฃ ํ UI ์ค๋ ๋์์ ํ ์คํธ ๋์ฐ๊ธฐ
+ withContext(Dispatchers.Main) {
+ showToast("๋ณด๊ดํจ์ ์ ์ฅ๋์์ต๋๋ค: $fileName")
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ withContext(Dispatchers.Main) {
+ showToast("ํ์ผ์ ๋ณด๊ดํจ์ผ๋ก ๋ณต์ฌํ์ง ๋ชปํ์ต๋๋ค.")
+ }
+ }
+ }
+ }
+
fun onSwipeLeft() {
showAppDrawer()
}
@@ -268,7 +322,11 @@ open class LauncherActivity : CommonActivity() {
override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ setIntent(intent) // ๐ก ํ์ฌ ์ธํ
ํธ๋ฅผ ์๋ก ๋ค์ด์จ ์ธํ
ํธ๋ก ๊ฐฑ์ !
try {
+
+
supportFragmentManager.findFragmentByTag(AppDrawerBottomSheet.TAG)?.let {
try { if (it is AppDrawerBottomSheet) it.dismissAllowingStateLoss() } catch (e : Exception) {}
}
@@ -284,13 +342,7 @@ open class LauncherActivity : CommonActivity() {
} else {
intent?.extras?.keySet()?.forEach {
try {
- Blog.LOGE(
- "onNewIntent :: key >> ${it} :: value >> ${
- intent?.extras?.getString(
- it
- )
- }"
- )
+ Blog.LOGE("onNewIntent :: key >> $it :: value >> ${intent.extras?.get(it)}")
} catch (e: Exception) {
e.printStackTrace()
}
@@ -300,9 +352,7 @@ open class LauncherActivity : CommonActivity() {
} catch (e : Exception) {
e.printStackTrace()
}
-
-// }
- super.onNewIntent(intent)
+ handleSharedIntent(intent)
}
private fun openWithIntent(intent: Intent){
@@ -490,6 +540,8 @@ open class LauncherActivity : CommonActivity() {
android.Manifest.permission.RECEIVE_SMS,
android.Manifest.permission.READ_SMS
))
+
+ handleSharedIntent(intent)
}
private var smsReceiver: SmsReceiver? = null
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/LunaticLauncher.kt b/app/src/main/kotlin/bums/lunatic/launcher/LunaticLauncher.kt
index 43311dfe..565f7051 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/LunaticLauncher.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/LunaticLauncher.kt
@@ -48,7 +48,7 @@ internal class LunaticLauncher : Application() {
appContext?.initGeckoRuntime()
return sRuntime
}
-
+ var privateCompletedDir : File? = null
}
private fun initGeckoRuntime() {
@@ -89,7 +89,7 @@ prefs:
override fun onCreate() {
super.onCreate()
appContext = this
-// Base.initialize(this)
+ privateCompletedDir = File(getExternalFilesDir(null), "completed_torrents")
PrefHelper.initialize(this)
Base64ImageCache.init(this)
val dir = File("/storage/emulated/0/bums_ob/BUM'S PACED /scraped/logs")
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 61841ab7..fccc34f3 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt
@@ -171,10 +171,11 @@ class ForeGroundService : Service() {
try {
- val youtubeDLDir = File(
- Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
- "youtubedl-android"
- )
+// val youtubeDLDir = File(
+// Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
+// "youtubedl-android"
+// )
+ val youtubeDLDir = File(getExternalFilesDir(null), "completed_torrents")
val command = YoutubeDLRequest(url)
command.addOption("-o", youtubeDLDir.getAbsolutePath() + "/%(title)s.%(ext)s");
currentProcessId = UUID.randomUUID().toString()
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt
new file mode 100644
index 00000000..0da14a1d
--- /dev/null
+++ b/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt
@@ -0,0 +1,368 @@
+package bums.lunatic.launcher.home
+
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.MimeTypeMap
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.ImageView
+import android.widget.Spinner
+import android.widget.TextView
+import android.widget.Toast
+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 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 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 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 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")
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_completed_files, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupRecyclerView(view)
+ setupControls(view)
+ loadFiles()
+ }
+
+ private fun setupRecyclerView(view: View) {
+ recyclerView = view.findViewById(R.id.recyclerViewCompletedFiles)
+ recyclerView.layoutManager = LinearLayoutManager(requireContext())
+
+ adapter = CompletedFilesAdapter(
+ onItemClick = { file ->
+ openPrivateFile(requireContext(), file)
+ },
+ onItemLongClick = { file ->
+ android.app.AlertDialog.Builder(context)
+ .setTitle("ํ์ผ ์ญ์ ")
+ .setMessage("ํด๋น ํ์ผ์ ์ญ์ ํ์๊ฒ ์ต๋๊น?")
+ .setPositiveButton("ํ์ธ") { _, _ ->
+ CoroutineScope(Dispatchers.IO).launch {
+ if (file.delete()) {
+ CoroutineScope(Dispatchers.Main).launch {
+ Toast.makeText(requireContext(), "์ญ์ ๋์์ต๋๋ค.", Toast.LENGTH_SHORT).show()
+ loadFiles()
+ }
+ }
+ }
+ }
+ .setNegativeButton("์ทจ์", null)
+ .show()
+
+ }
+ )
+ recyclerView.adapter = adapter
+ }
+
+ 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 ๋ ๊ฐ๋ฅ)
+ }
+ }
+
+ private fun setupControls(view: View) {
+ 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)
+
+ // ์ด๊ธฐ ๋ทฐ ๋ชจ๋์ ๋ง๋ ์ด๋ชจ์ง ์ธํ
+ btnViewMode.text = getViewModeSymbolName(currentViewMode)
+ adapter.setViewMode(currentViewMode)
+ btnViewMode.setOnClickListener {
+ currentViewMode = when (currentViewMode) {
+ FileViewMode.LIST_TEXT -> FileViewMode.LIST_THUMB
+ FileViewMode.LIST_THUMB -> FileViewMode.GRID_LARGE
+ FileViewMode.GRID_LARGE -> FileViewMode.GRID_SMALL
+ FileViewMode.GRID_SMALL -> FileViewMode.LIST_TEXT
+ }
+ 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.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+ override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
+ currentFilter = FileFilterType.values()[position]
+ applyFilterAndSort()
+ }
+ 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.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+ override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
+ currentSort = FileSortType.values()[position]
+ applyFilterAndSort()
+ }
+ 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 loadFiles() {
+ val completedDir = File(requireContext().getExternalFilesDir(null), "completed_torrents")
+ if (completedDir.exists()) {
+ allFiles = completedDir.walkTopDown().filter { it.isFile }.toList()
+ } else {
+ allFiles = emptyList()
+ }
+ applyFilterAndSort()
+ }
+
+ override fun onHiddenChanged(hidden: Boolean) {
+ super.onHiddenChanged(hidden)
+ loadFiles()
+ }
+
+ // ๐ก ํต์ฌ ๋ก์ง: ํํฐ ๋ฐ ์ ๋ ฌ ์ ์ฉ
+ private fun applyFilterAndSort() {
+ // 1. ํํฐ๋ง
+ var processedList = allFiles.filter { file ->
+ val ext = file.extension.lowercase()
+ when (currentFilter) {
+ FileFilterType.ALL -> true
+ FileFilterType.IMAGE -> extImages.contains(ext)
+ FileFilterType.VIDEO -> extVideos.contains(ext)
+ FileFilterType.DOCUMENT -> extDocs.contains(ext)
+ FileFilterType.OTHER -> !extImages.contains(ext) && !extVideos.contains(ext) && !extDocs.contains(ext)
+ }
+ }
+
+ // 2. ์ ๋ ฌ
+ processedList = when (currentSort) {
+ FileSortType.DOWNLOAD_DATE -> {
+ if (isDescending) processedList.sortedByDescending { it.lastModified() }
+ else processedList.sortedBy { it.lastModified() }
+ }
+ FileSortType.LAST_USED -> {
+ if (isDescending) processedList.sortedByDescending { getLastAccessedTime(it.name) }
+ else processedList.sortedBy { getLastAccessedTime(it.name) }
+ }
+ FileSortType.SIZE -> {
+ if (isDescending) processedList.sortedByDescending { it.length() }
+ else processedList.sortedBy { it.length() }
+ }
+ FileSortType.FREQUENTLY_USED -> {
+ if (isDescending) processedList.sortedByDescending { getAccessCount(it.name) }
+ else processedList.sortedBy { getAccessCount(it.name) }
+ }
+ }
+
+ adapter.submitList(processedList)
+ }
+ 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) // ํ์ ๊ธฐ๋ก ์ถ๊ฐ
+ .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
+ )
+
+ 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()
+ }
+ }
+
+ private fun updateRecyclerViewLayoutManager() {
+ recyclerView.layoutManager = when (currentViewMode) {
+ FileViewMode.LIST_TEXT, FileViewMode.LIST_THUMB ->
+ LinearLayoutManager(requireContext())
+ FileViewMode.GRID_LARGE ->
+ GridLayoutManager(requireContext(), 2) // 3์ด ๊ทธ๋ฆฌ๋
+ FileViewMode.GRID_SMALL ->
+ GridLayoutManager(requireContext(), 4) // 5์ด ๊ทธ๋ฆฌ๋
+ }
+ }
+
+}
+
+// --- RecyclerView ์ด๋ํฐ ---
+class CompletedFilesAdapter(
+ private val onItemClick: (File) -> Unit,
+ private val onItemLongClick: (File) -> Unit
+) : RecyclerView.Adapter() {
+
+ private var fileList: List = emptyList()
+ private var viewMode: FileViewMode = FileViewMode.LIST_THUMB
+
+ fun submitList(files: List) {
+ fileList = files
+ notifyDataSetChanged()
+ }
+
+ fun setViewMode(mode: FileViewMode) {
+ viewMode = mode
+ notifyDataSetChanged() // ๋ชจ๋๊ฐ ๋ฐ๋๋ฉด ์ ์ฒด ๋ฆฌ์คํธ๋ฅผ ๋ค์ ๊ทธ๋ฆฝ๋๋ค.
+ }
+
+ // ๐ก ๋ชจ๋์ ๋ฐ๋ผ ViewType์ ๋ค๋ฅด๊ฒ ๋ฐํ
+ override fun getItemViewType(position: Int): Int {
+ return viewMode.ordinal
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder {
+ val layoutRes = when (FileViewMode.values()[viewType]) {
+ FileViewMode.LIST_TEXT -> R.layout.item_file_list_text
+ FileViewMode.LIST_THUMB -> R.layout.item_file_list_thumb
+ FileViewMode.GRID_LARGE, FileViewMode.GRID_SMALL -> R.layout.item_file_grid
+ }
+ val view = LayoutInflater.from(parent.context).inflate(layoutRes, parent, false)
+ return FileViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: FileViewHolder, position: Int) {
+ holder.bind(fileList[position], viewMode)
+ }
+
+ 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
+
+ val sizeMb = file.length() / (1024.0 * 1024.0)
+ tvFileSize?.text = String.format("%.2f MB โข %s", sizeMb, file.extension.uppercase())
+
+ // ๐ก ์ธ๋ค์ผ ๋ทฐ๊ฐ ์กด์ฌํ๋ ๋ชจ๋์ผ ๊ฒฝ์ฐ Glide๋ก ์ด๋ฏธ์ง ๋ก๋ฉ
+ 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)
+ true
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt
index 0efebe30..1e15a2cc 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt
@@ -674,7 +674,7 @@ open class GeckoWeb @JvmOverloads constructor(
AlertDialog.Builder(context).setTitle("๋ถ๋งํฌ ์ ์ฅ").setView(container)
.setPositiveButton("์ ์ฅ") { _, _ ->
val vis = rg.findViewById(rg.checkedRadioButtonId).text.toString()
- BookmarkUploader.saveBookmarkWithContent(pageUrl, mediaUrls, commentInput.text.toString(), vis)
+ BookmarkUploader.saveBookmarkWithContent(context,pageUrl, mediaUrls, commentInput.text.toString(), vis)
}
.setNegativeButton("์ทจ์", null).show()
}
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt
index 81f113f1..13ec88c7 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt
@@ -61,13 +61,6 @@ import io.realm.kotlin.ext.query
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import org.json.JSONObject
-import org.mozilla.geckoview.ExperimentDelegate
-import org.mozilla.geckoview.GeckoResult
-import org.mozilla.geckoview.GeckoRuntime
-import org.mozilla.geckoview.GeckoRuntimeSettings
-import org.mozilla.geckoview.GeckoRuntimeSettings.ALLOW_ALL
-import java.io.File
import kotlin.math.abs
interface KeyEventHandler {
@@ -537,6 +530,7 @@ open class NeoRssActivity : CommonActivity() {
R.id.btn_i -> TokiFragment.newInstanceI()
R.id.btn_torrent -> TorrentListFragment()
R.id.btn_info -> SystemStatusFragment()
+ R.id.btn_completed_files -> CompletedFilesFragment()
R.id.close -> {
finish()
return
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt b/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt
index 54258935..c064ed94 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt
@@ -123,7 +123,7 @@ object CommonUtils {
val request = Request.Builder().url(fileUrl).addHeader("Referer", refferer.toString())
.addHeader("User-Agent", "Mozilla/5.0").build()
- val dir = File("/storage/emulated/0/bums_ob/BUM'S PACED /scraped/md")
+ val dir = File(context.getExternalFilesDir(null), "completed_torrents")
if (!dir.exists()) {
dir.mkdirs()
} else {
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 935bc348..af38fda7 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt
@@ -21,6 +21,10 @@ import com.frostwire.jlibtorrent.alerts.*
import android.app.*
import android.content.*
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
import android.os.*
import androidx.annotation.RequiresApi
import bums.lunatic.launcher.home.toast
@@ -49,6 +53,59 @@ data class TorrentTask(
)
class TorrentService : Service() {
+ private lateinit var connectivityManager: ConnectivityManager
+ private var networkCallback: ConnectivityManager.NetworkCallback? = null
+ private var isWifiConnected = false
+
+ private fun registerNetworkCallback() {
+ connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val request = NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build()
+
+ networkCallback = object : ConnectivityManager.NetworkCallback() {
+ override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
+ super.onCapabilitiesChanged(network, networkCapabilities)
+ val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
+
+ if (isWifi) {
+ if (!isWifiConnected) {
+ isWifiConnected = true
+ println("TorrentService: Wi-Fi ์ฐ๊ฒฐ๋จ. ์ ์ฒด ๋ค์ด๋ก๋ ์ธ์
์ฌ๊ฐ.")
+ session.resume() // ๐ก ์ธ์
์ ์ฒด ํธ๋ํฝ ์ฌ๊ฐ
+ }
+ } else {
+ if (isWifiConnected) {
+ isWifiConnected = false
+ println("TorrentService: Wi-Fi ์ฐ๊ฒฐ ์๋(์
๋ฃฐ๋ฌ ๋ฑ). ์ ์ฒด ๋ค์ด๋ก๋ ์ธ์
์ผ์์ ์ง.")
+ session.pause() // ๐ก ์ธ์
์ ์ฒด ํธ๋ํฝ ์ฐจ๋จ
+ }
+ }
+ }
+
+ override fun onLost(network: Network) {
+ super.onLost(network)
+ if (isWifiConnected) {
+ isWifiConnected = false
+ println("TorrentService: ๋คํธ์ํฌ ์์ ํ ๋๊น. ์ ์ฒด ๋ค์ด๋ก๋ ์ธ์
์ผ์์ ์ง.")
+ session.pause()
+ }
+ }
+ }
+
+ // ์ด๊ธฐ ์ํ ์ฒดํฌ (์๋น์ค ์์ ์์ ์ด Wi-Fi ํ๊ฒฝ์ธ์ง ํ์ธ)
+ val activeNetwork = connectivityManager.activeNetwork
+ val caps = connectivityManager.getNetworkCapabilities(activeNetwork)
+ isWifiConnected = caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
+
+ // ์ฒ์ ์ผฐ์ ๋ Wi-Fi๊ฐ ์๋๋ฉด ๋ฐ๋ก ์ผ์์ ์ง
+ if (!isWifiConnected) {
+ println("TorrentService: ํ์ฌ Wi-Fi๊ฐ ์๋๋๋ค. ์ธ์
์ ์ผ์์ ์ง ์ํ๋ก ์์ํฉ๋๋ค.")
+ session.pause()
+ }
+
+ connectivityManager.registerNetworkCallback(request, networkCallback!!)
+ }
private lateinit var session: SessionManager
private val binder = TorrentBinder()
@@ -75,6 +132,7 @@ class TorrentService : Service() {
super.onCreate()
startForegroundService()
initLibTorrent()
+ registerNetworkCallback()
startPolling() // ํด๋ง ์์
}
@@ -189,6 +247,7 @@ class TorrentService : Service() {
override fun onDestroy() {
super.onDestroy()
+ networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) }
serviceScope.cancel() // ์๋น์ค ์ข
๋ฃ ์ ์ฝ๋ฃจํด ์ ๋ฆฌ
}
@@ -273,7 +332,8 @@ class TorrentService : Service() {
when (alert.type()) {
AlertType.TORRENT_FINISHED -> {
val ta = alert as TorrentFinishedAlert
- moveToPublicDownload(ta.handle())
+// moveToPublicDownload(ta.handle())
+ moveToPrivateStorage(ta.handle())
}
AlertType.METADATA_RECEIVED -> {
val ma = alert as MetadataReceivedAlert
@@ -359,6 +419,69 @@ class TorrentService : Service() {
}
}
+ @RequiresApi(Build.VERSION_CODES.Q)
+ private fun moveToPrivateStorage(handle: TorrentHandle) {
+ try {
+ val status = handle.status()
+ val infoHash = status.infoHash().toString()
+ var torrentName = status.name()
+
+ if (status.hasMetadata()) {
+ val torrentInfo = handle.torrentFile()
+ if (torrentInfo != null && torrentInfo.isValid) {
+ val realName = torrentInfo.name()
+ if (!realName.isNullOrEmpty()) {
+ torrentName = realName
+ }
+ }
+ }
+
+ if (torrentName.isNullOrEmpty() || torrentName == infoHash) {
+ println("TorrentService: ์ด๋ํ ์ ํจํ ํ์ผ/ํด๋ ์ด๋ฆ์ ์ฐพ์ง ๋ชปํ์ต๋๋ค.")
+ return
+ }
+
+ val sourcePath = File(tempDir, torrentName)
+ if (!sourcePath.exists()) {
+ println("TorrentService: ์๋ณธ ํ์ผ/ํด๋๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค -> ${sourcePath.absolutePath}")
+ return
+ }
+
+ // ๐ก ์ฑ ์ ์ฉ ํ๋ผ์ด๋น ํด๋ ์ค์ (๋ค๋ฅธ ์ฑ ์ ๊ทผ ๋ถ๊ฐ)
+ // ์๋๋ก์ด๋/data/bums.lunatic.launcher/files/completed_torrents ์ ์ ์ฅ๋ฉ๋๋ค.
+ val privateCompletedDir = File(getExternalFilesDir(null), "completed_torrents")
+ if (!privateCompletedDir.exists()) {
+ privateCompletedDir.mkdirs()
+ }
+
+ val destPath = File(privateCompletedDir, torrentName)
+
+ // 1. renameTo๋ก ๋น ๋ฅธ ํ์ผ/ํด๋ ์ด๋ ์๋ (๊ฐ์ ํํฐ์
๋ด ์ด๋ ์ ์ฆ์ ์๋ฃ๋จ)
+ val success = sourcePath.renameTo(destPath)
+
+ // 2. ๋ง์ฝ renameTo๊ฐ ์คํจํ ๊ฒฝ์ฐ (ํํฐ์
์ด ๋ค๋ฅด๊ฑฐ๋ ๊ถํ ๋ฌธ์ ๋ฑ), ์ง์ ๋ณต์ฌ ํ ์ญ์
+ if (!success) {
+ sourcePath.copyRecursively(destPath, overwrite = true)
+ sourcePath.deleteRecursively()
+ }
+
+ // ์ธ์
์์ ์ ๊ฑฐ ๋ฐ ๋ฆฌ์ฅผ ํ์ผ ์ญ์
+ session.remove(handle)
+
+ val resumeFile = File(resumeDir, "$infoHash.resume")
+ if (resumeFile.exists()) {
+ resumeFile.delete()
+ println("TorrentService: ๋ฆฌ์ฅผ ํ์ผ ์ญ์ ์๋ฃ ($infoHash.resume)")
+ }
+
+ println("TorrentService: ๋ค์ด๋ก๋ ์๋ฃ ๋ฐ ํ๋ผ์ด๋น ํด๋ ์ด๋ ์ฑ๊ณต ($torrentName)")
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ println("TorrentService: ํ์ผ ์ด๋ ์ค ์๋ฌ ๋ฐ์ - ${e.message}")
+ }
+ }
+
@RequiresApi(Build.VERSION_CODES.Q)
private fun moveToPublicDownload(handle: TorrentHandle) {
try {
diff --git a/app/src/main/res/layout/fragment_completed_files.xml b/app/src/main/res/layout/fragment_completed_files.xml
new file mode 100644
index 00000000..029d558f
--- /dev/null
+++ b/app/src/main/res/layout/fragment_completed_files.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_completed_file.xml b/app/src/main/res/layout/item_completed_file.xml
new file mode 100644
index 00000000..82c4660a
--- /dev/null
+++ b/app/src/main/res/layout/item_completed_file.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_file_grid.xml b/app/src/main/res/layout/item_file_grid.xml
new file mode 100644
index 00000000..75d6ce4a
--- /dev/null
+++ b/app/src/main/res/layout/item_file_grid.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_file_list_text.xml b/app/src/main/res/layout/item_file_list_text.xml
new file mode 100644
index 00000000..7236c302
--- /dev/null
+++ b/app/src/main/res/layout/item_file_list_text.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_file_list_thumb.xml b/app/src/main/res/layout/item_file_list_thumb.xml
new file mode 100644
index 00000000..d60b6f8a
--- /dev/null
+++ b/app/src/main/res/layout/item_file_list_thumb.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/rss_activity.xml b/app/src/main/res/layout/rss_activity.xml
index 712da641..1f340761 100644
--- a/app/src/main/res/layout/rss_activity.xml
+++ b/app/src/main/res/layout/rss_activity.xml
@@ -83,6 +83,11 @@
style="@style/CommonFabStyle"
android:id="@+id/btn_torrent"
/>
+
+
\ No newline at end of file