...
This commit is contained in:
parent
1383781dc6
commit
d4ec1d5652
@ -125,7 +125,15 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" /> </intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<receiver android:name=".receiver.CallReceiver" android:exported="true">
|
||||
<intent-filter>
|
||||
|
||||
@ -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<String>,
|
||||
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())
|
||||
)
|
||||
|
||||
@ -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<Uri>(Intent.EXTRA_STREAM)
|
||||
uri?.let { saveToPrivateVault(it) }
|
||||
} else if (intent.action == Intent.ACTION_SEND_MULTIPLE) {
|
||||
val uris = intent.getParcelableArrayListExtra<Uri>(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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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<File>()
|
||||
|
||||
// 현재 선택된 상태값
|
||||
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<Spinner>(R.id.spinnerFilter)
|
||||
val spinnerSort = view.findViewById<Spinner>(R.id.spinnerSort)
|
||||
val tvSortOrder = view.findViewById<TextView>(R.id.tvSortOrder)
|
||||
|
||||
val btnViewMode = view.findViewById<TextView>(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<CompletedFilesAdapter.FileViewHolder>() {
|
||||
|
||||
private var fileList: List<File> = emptyList()
|
||||
private var viewMode: FileViewMode = FileViewMode.LIST_THUMB
|
||||
|
||||
fun submitList(files: List<File>) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -674,7 +674,7 @@ open class GeckoWeb @JvmOverloads constructor(
|
||||
AlertDialog.Builder(context).setTitle("북마크 저장").setView(container)
|
||||
.setPositiveButton("저장") { _, _ ->
|
||||
val vis = rg.findViewById<RadioButton>(rg.checkedRadioButtonId).text.toString()
|
||||
BookmarkUploader.saveBookmarkWithContent(pageUrl, mediaUrls, commentInput.text.toString(), vis)
|
||||
BookmarkUploader.saveBookmarkWithContent(context,pageUrl, mediaUrls, commentInput.text.toString(), vis)
|
||||
}
|
||||
.setNegativeButton("취소", null).show()
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
62
app/src/main/res/layout/fragment_completed_files.xml
Normal file
62
app/src/main/res/layout/fragment_completed_files.xml
Normal file
@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="?android:attr/windowBackground">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:text="다운로드 보관함"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:padding="16dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnViewMode"
|
||||
style="@style/MaterialIconButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:text="view_headline" />
|
||||
|
||||
<Space
|
||||
android:layout_weight="1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
<Spinner
|
||||
android:id="@+id/spinnerFilter"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinnerSort"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSortOrder"
|
||||
style="@style/MaterialIconButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:text="arrow_downward" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerViewCompletedFiles"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="8dp"/>
|
||||
</LinearLayout>
|
||||
26
app/src/main/res/layout/item_completed_file.xml
Normal file
26
app/src/main/res/layout/item_completed_file.xml
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="?android:attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvFileName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@android:color/black"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="middle"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvFileSize"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginTop="4dp"/>
|
||||
</LinearLayout>
|
||||
10
app/src/main/res/layout/item_file_grid.xml
Normal file
10
app/src/main/res/layout/item_file_grid.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="4dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
<ImageView android:id="@+id/ivThumb" android:layout_width="match_parent" android:layout_height="100dp" android:scaleType="centerCrop" android:background="#E0E0E0"/>
|
||||
<TextView android:id="@+id/tvFileName" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="12sp" android:textColor="@android:color/white" android:singleLine="true" android:ellipsize="end" android:gravity="center" android:layout_marginTop="4dp"/>
|
||||
</LinearLayout>
|
||||
10
app/src/main/res/layout/item_file_list_text.xml
Normal file
10
app/src/main/res/layout/item_file_list_text.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
<TextView android:id="@+id/tvFileName" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="16sp" android:textColor="@android:color/white" android:singleLine="true" android:ellipsize="middle"/>
|
||||
<TextView android:id="@+id/tvFileSize" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="12sp" android:textColor="@android:color/darker_gray" android:layout_marginTop="4dp"/>
|
||||
</LinearLayout>
|
||||
14
app/src/main/res/layout/item_file_list_thumb.xml
Normal file
14
app/src/main/res/layout/item_file_list_thumb.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical">
|
||||
<ImageView android:id="@+id/ivThumb" android:layout_width="60dp" android:layout_height="60dp" android:scaleType="centerCrop" android:background="#E0E0E0"/>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:layout_marginStart="12dp">
|
||||
<TextView android:id="@+id/tvFileName" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="16sp" android:textColor="@android:color/white" android:singleLine="true" android:ellipsize="middle"/>
|
||||
<TextView android:id="@+id/tvFileSize" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="12sp" android:textColor="@android:color/darker_gray" android:layout_marginTop="4dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@ -83,6 +83,11 @@
|
||||
style="@style/CommonFabStyle"
|
||||
android:id="@+id/btn_torrent"
|
||||
/>
|
||||
<bums.lunatic.launcher.view.FloatingActionButton
|
||||
app:fab_label="📦"
|
||||
style="@style/CommonFabStyle"
|
||||
android:id="@+id/btn_completed_files"
|
||||
/>
|
||||
<bums.lunatic.launcher.view.FloatingActionButton
|
||||
app:fab_label="📊"
|
||||
style="@style/CommonFabStyle"
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<paths>
|
||||
<external-path name="apk_files" path="." />
|
||||
<external-path name="external_files" path="." />
|
||||
<external-files-path name="completed_videos" path="completed_torrents" />
|
||||
</paths>
|
||||
Loading…
x
Reference in New Issue
Block a user