From d4ec1d565219b98b912895aef9850578d937a8da Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Thu, 19 Mar 2026 14:55:22 +0900 Subject: [PATCH] ... --- app/src/main/AndroidManifest.xml | 10 +- .../bums/lunatic/launcher/BookmarkUploader.kt | 44 ++- .../bums/lunatic/launcher/LauncherActivity.kt | 72 +++- .../bums/lunatic/launcher/LunaticLauncher.kt | 4 +- .../launcher/helpers/ForeGroundService.kt | 9 +- .../launcher/home/CompletedFilesFragment.kt | 368 ++++++++++++++++++ .../bums/lunatic/launcher/home/GeckoWeb.kt | 2 +- .../lunatic/launcher/home/NeoRssActivity.kt | 8 +- .../lunatic/launcher/utils/CommonUtils.kt | 2 +- .../launcher/workers/TorrentManager.kt | 125 +++++- .../res/layout/fragment_completed_files.xml | 62 +++ .../main/res/layout/item_completed_file.xml | 26 ++ app/src/main/res/layout/item_file_grid.xml | 10 + .../main/res/layout/item_file_list_text.xml | 10 + .../main/res/layout/item_file_list_thumb.xml | 14 + app/src/main/res/layout/rss_activity.xml | 5 + app/src/main/res/xml/file_paths.xml | 1 + 17 files changed, 741 insertions(+), 31 deletions(-) create mode 100644 app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt create mode 100644 app/src/main/res/layout/fragment_completed_files.xml create mode 100644 app/src/main/res/layout/item_completed_file.xml create mode 100644 app/src/main/res/layout/item_file_grid.xml create mode 100644 app/src/main/res/layout/item_file_list_text.xml create mode 100644 app/src/main/res/layout/item_file_list_thumb.xml 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