This commit is contained in:
lunaticbum 2026-03-20 17:55:48 +09:00
parent 406e7820bc
commit 3f0a7d0d5c
9 changed files with 598 additions and 192 deletions

View File

@ -55,6 +55,36 @@ port.onMessage.addListener(response => {
var type= response["type"];
switch (type) {
case "CLICK_PREV_CHAPTER" : {
const links = document.querySelectorAll("a");
let found = false;
for (let link of links) {
if (link.textContent.includes("이전화 보기")) {
link.click();
found = true;
break;
}
}
if (!found) {
port.postMessage({ type: "MSG", msg: "이전화 버튼을 찾을 수 없습니다." });
}
break;
}
case "CLICK_NEXT_CHAPTER" : {
const links = document.querySelectorAll("a");
let found = false;
for (let link of links) {
if (link.textContent.includes("다음화 보기")) {
link.click();
found = true;
break;
}
}
if (!found) {
port.postMessage({ type: "MSG", msg: "다음화 버튼을 찾을 수 없습니다." });
}
break;
}
case "fetchAllImages": {
const targetSrc = response["targetSrc"];
const imageUrls = findAllRelatedImages(targetSrc);

View File

@ -3,10 +3,10 @@ package bums.lunatic.launcher.apps
import android.app.Dialog
import android.app.SearchManager
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
@ -21,23 +21,17 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import bums.lunatic.launcher.BuildConfig
import bums.lunatic.launcher.R
import bums.lunatic.launcher.apps.AppMenu
import bums.lunatic.launcher.databinding.BottomSheetAppDrawerBinding // XML 이름에 맞춰 바인딩 클래스 생성됨
import bums.lunatic.launcher.helpers.PrefBoolean
import bums.lunatic.launcher.helpers.PrefLong
import bums.lunatic.launcher.databinding.BottomSheetAppDrawerBinding
import bums.lunatic.launcher.model.AppInfo
import bums.lunatic.launcher.model.SimpleContact
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.utils.CategoryGrouper
import bums.lunatic.launcher.utils.CategoryManualMapper
import bums.lunatic.launcher.utils.EnToKo
import bums.lunatic.launcher.utils.JamoUtils
import bums.lunatic.launcher.utils.SimpleTransliterater
import bums.lunatic.launcher.workers.UsageLogType
import bums.lunatic.launcher.workers.UsageUpdateType
import bums.lunatic.launcher.workers.WorkersDb
import com.google.android.gms.common.wrappers.PackageManagerWrapper
import com.google.android.gms.common.wrappers.Wrappers.packageManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@ -47,7 +41,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URLEncoder
import kotlin.math.min
class AppDrawerBottomSheet : BottomSheetDialogFragment() {
@ -252,23 +245,31 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
filterAppsList(keyword)
}
binding.searchTaxi.setOnClickListener {
//// val destName = Uri.encode("서울역")
//// val url = "kakaot://taxi?dest_lat=${URLEncoder.encode("37.467696")}&dest_lng=${URLEncoder.encode("127.101063")}&dest_name=세곡동557"
// val url = "kakaomap://search?q=세곡동 557"
// val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
// startActivity(intent)
openSearchApps("kakaot://taxi?goalx=${URLEncoder.encode("37.467696")}&goaly=${URLEncoder.encode("127.101063")}","com.kakao.taxi")
}
binding.searchMusic.setOnClickListener {
try {
// 스포티파이는 별도의 URL 인코딩 없이도 잘 동작하지만, 안전하게 인코딩 추천
val encodedKeyword = URLEncoder.encode(keyword, "UTF-8")
val uri = Uri.parse("spotify:search:$encodedKeyword")
val intent: Intent = Intent(MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH)
val intent = Intent(Intent.ACTION_VIEW, uri)
// 명시적으로 스포티파이 앱 지정 (웹 브라우저로 빠지는 것 방지)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
intent.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, "vnd.android.cursor.item/*")
intent.putExtra(SearchManager.QUERY, keyword) // 검색하고자 하는 곡명이나 아티스트명
requireContext().startActivity(intent)
intent.setPackage("com.apple.android.music") // 애플 뮤직 패키지 명시
if (intent.resolveActivity(requireContext().getPackageManager()) != null) {
startActivity(intent)
}
} catch (e: Exception) {
// 앱이 없을 경우 플레이스토어로 이동
val marketIntent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.spotify.music"))
val marketIntent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.apple.android.music"))
marketIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
requireContext().startActivity(marketIntent)
}
@ -298,7 +299,7 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
binding.searchNaver.setOnClickListener {
openSearchApps("https://search.naver.com/search.naver?where=nexearch&query=${keyword}", "com.nhn.android.search")
}
// ... 나머지 버튼들도 동일하게 추가 ...
}

View File

@ -3,6 +3,7 @@ package bums.lunatic.launcher.home
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
@ -22,7 +23,6 @@ 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.NaturalOrderComparator
import com.bumptech.glide.Glide
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -34,6 +34,36 @@ enum class FileFilterType(val label : String) { ALL("전체"), IMAGE("이미지"
enum class FileSortType(val label : String) { NAME("파일명") , DOWNLOAD_DATE("다운로드"), LAST_USED("최근 사용"), FREQUENTLY_USED("자주 사용"), SIZE("용량") }
enum class FileViewMode { LIST_TEXT, LIST_THUMB, GRID_LARGE, GRID_SMALL }
// 💡 숫자 크기를 인식하는 정렬 비교자
class NaturalOrderComparator : Comparator<File> {
override fun compare(f1: File, f2: File): Int {
val splitRegex = Regex("(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)")
val parts1 = f1.name.split(splitRegex)
val parts2 = f2.name.split(splitRegex)
val limit = minOf(parts1.size, parts2.size)
for (i in 0 until limit) {
val p1 = parts1[i]
val p2 = parts2[i]
if (p1 == p2) continue
val isNum1 = p1.all { it.isDigit() }
val isNum2 = p2.all { it.isDigit() }
if (isNum1 && isNum2) {
val num1 = p1.trimStart('0')
val num2 = p2.trimStart('0')
val cmp = if (num1.length != num2.length) num1.length.compareTo(num2.length) else num1.compareTo(num2)
if (cmp != 0) return cmp
} else {
val strCmp = p1.compareTo(p2, ignoreCase = true)
if (strCmp != 0) return strCmp
}
}
return parts1.size.compareTo(parts2.size)
}
}
class CompletedFilesFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
@ -48,12 +78,16 @@ class CompletedFilesFragment : Fragment() {
private var currentViewMode = FileViewMode.LIST_TEXT
private var isDescending = true
// 💡 멀티 선택 모드 상태 관리 변수
private var isSelectionMode = false
private val selectedFiles = mutableSetOf<File>()
private val extImages = setOf("jpg", "jpeg", "png", "gif", "bmp", "webp")
private val extVideos = setOf("mp4", "mkv", "avi", "mov", "wmv", "flv", "webm", "ts")
private val extDocs = setOf("pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "hwp")
private lateinit var backPressedCallback: OnBackPressedCallback
fun backPress() = backPressedCallback.handleOnBackPressed()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@ -70,7 +104,8 @@ class CompletedFilesFragment : Fragment() {
backPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
navigateUp()
// 선택 모드일 땐 선택 모드 먼저 해제, 아닐 땐 상위 폴더 이동
if (isSelectionMode) toggleSelectionMode(false) else navigateUp()
}
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressedCallback)
@ -80,40 +115,61 @@ class CompletedFilesFragment : Fragment() {
loadFiles()
}
// 💡 선택 모드 진입/해제 UI 토글
private fun toggleSelectionMode(enable: Boolean) {
isSelectionMode = enable
if (!enable) selectedFiles.clear()
adapter.updateSelection(selectedFiles)
// 추가한 XML 뷰 보이기/숨기기
view?.findViewById<View>(R.id.layoutSelectionActions)?.visibility = if (enable) View.VISIBLE else View.GONE
// 뒤로가기 버튼 활성화 기준 업데이트
backPressedCallback.isEnabled = isSelectionMode || currentDir.absolutePath != rootDir.absolutePath
}
private fun setupRecyclerView(view: View) {
recyclerView = view.findViewById(R.id.recyclerViewCompletedFiles)
updateRecyclerViewLayoutManager()
// 💡 어댑터 생성 시 getAccessCount 함수를 넘겨주어 어댑터 내부에서 조회수를 그릴 수 있게 합니다.
adapter = CompletedFilesAdapter(
onItemClick = { file ->
if (file.name == "..") {
navigateUp()
} else if (file.isDirectory) {
currentDir = file
loadFiles()
} else {
openPrivateFile(requireContext(), file)
if (!isSelectionMode) navigateUp()
return@CompletedFilesAdapter
}
// 💡 멀티 선택 모드일 때의 클릭 로직
if (isSelectionMode) {
if (selectedFiles.contains(file)) selectedFiles.remove(file) else selectedFiles.add(file)
if (selectedFiles.isEmpty()) toggleSelectionMode(false)
else adapter.updateSelection(selectedFiles)
}
// 일반 모드일 때의 클릭 로직
else {
if (file.isDirectory) {
currentDir = file
loadFiles()
} else {
openPrivateFile(requireContext(), file)
}
}
},
onItemLongClick = { file ->
val type = if (file.isDirectory) "폴더" else "파일"
android.app.AlertDialog.Builder(context)
.setTitle("$type 삭제")
.setMessage("해당 ${type}를 삭제하시겠습니까?\n(폴더일 경우 내부 파일도 모두 삭제됩니다)")
.setPositiveButton("확인") { _, _ ->
CoroutineScope(Dispatchers.IO).launch {
val success = if (file.isDirectory) file.deleteRecursively() else file.delete()
if (success) {
withContext(Dispatchers.Main) {
Toast.makeText(requireContext(), "삭제되었습니다.", Toast.LENGTH_SHORT).show()
loadFiles()
}
}
}
}
.setNegativeButton("취소", null)
.show()
if (file.name == "..") return@CompletedFilesAdapter
// 💡 폴더를 길게 누르면 -> 폴더 상세 삭제 다이얼로그 호출
if (file.isDirectory && !isSelectionMode) {
showFolderDeleteDialog(file)
}
// 💡 파일을 길게 누르면 -> 멀티 선택 모드 시작
else if (!isSelectionMode) {
toggleSelectionMode(true)
selectedFiles.add(file)
adapter.updateSelection(selectedFiles)
}
},
getAccessCount = { fileName -> getAccessCount(fileName) }
)
@ -121,13 +177,223 @@ class CompletedFilesFragment : Fragment() {
recyclerView.adapter = adapter
}
private fun navigateUp() {
if (currentDir.absolutePath != rootDir.absolutePath) {
currentDir = currentDir.parentFile ?: rootDir
loadFiles()
// 💡 폴더 내부 확인 및 개별 삭제 다이얼로그
private fun showFolderDeleteDialog(folder: File) {
val files = folder.listFiles()?.filter { it.isFile }?.sortedWith(NaturalOrderComparator()) ?: emptyList()
if (files.isEmpty()) {
android.app.AlertDialog.Builder(requireContext())
.setTitle("폴더 삭제")
.setMessage("빈 폴더입니다. 삭제하시겠습니까?")
.setPositiveButton("확인") { _, _ ->
folder.delete()
Toast.makeText(context, "폴더가 삭제되었습니다.", Toast.LENGTH_SHORT).show()
loadFiles()
}
.setNegativeButton("취소", null)
.show()
return
}
val fileNames = files.map { file ->
val sizeMb = file.length() / (1024.0 * 1024.0)
val accessCount = getAccessCount(file.name)
String.format("%s\n(%.2f MB, 조회 %d회)", file.name, sizeMb, accessCount)
}.toTypedArray()
val checkedItems = BooleanArray(files.size) { false }
android.app.AlertDialog.Builder(requireContext())
.setTitle("📁 ${folder.name} 정리")
.setMultiChoiceItems(fileNames, checkedItems) { _, which, isChecked ->
checkedItems[which] = isChecked
}
.setPositiveButton("선택 항목 삭제") { _, _ ->
var deletedCount = 0
files.forEachIndexed { index, file ->
if (checkedItems[index] && file.delete()) {
deletedCount++
}
}
Toast.makeText(context, "${deletedCount}개의 파일이 삭제되었습니다.", Toast.LENGTH_SHORT).show()
loadFiles()
}
.setNeutralButton("폴더 전체 삭제") { _, _ ->
// 💡 여기서도 다이얼로그 안에서 다이얼로그를 부릅니다! (안전한 패턴)
android.app.AlertDialog.Builder(requireContext())
.setMessage("정말로 폴더와 내부의 모든 파일을 삭제하시겠습니까?")
.setPositiveButton("") { _, _ ->
folder.deleteRecursively()
loadFiles()
}
.setNegativeButton("아니오", null)
.show()
}
// 💡 기존의 '취소' 자리를 '선택 이동'으로 사용
.setNegativeButton("선택 항목 이동") { _, _ ->
// 체크된 파일들만 필터링해서 리스트로 만듦
val selectedFilesToMove = files.filterIndexed { index, _ -> checkedItems[index] }
if (selectedFilesToMove.isNotEmpty()) {
// 두 번째 다이얼로그(이동 창) 호출
showMoveDialogForFiles(selectedFilesToMove)
} else {
Toast.makeText(context, "선택된 항목이 없습니다.", Toast.LENGTH_SHORT).show()
}
}
.setCancelable(true) // 빈 화면을 터치하거나 뒤로가기 키를 누르면 다이얼로그가 취소(닫힘)됨
.show()
}
private fun showUnorganizedFilesDialog(filterType: FileFilterType) {
// 사용자가 제외하고 싶어 하는 타겟 목록
val excludedDirs = listOf(rootDir.name, "이미지", "비디오", "문서", "기타")
val unorganizedFiles = mutableListOf<File>()
rootDir.walkTopDown().onEnter { dir ->
// 1. 루트 폴더 자체는 통과해야 하위 폴더를 검사할 수 있음
if (dir == rootDir) return@onEnter true
// 2. 루트 바로 아래에 있는 '이미지, 비디오, 문서, 기타' 폴더는 검사 진입 거부!
if (dir.parentFile == rootDir && excludedDirs.contains(dir.name)) {
return@onEnter false
}
true
}.forEach { file ->
if (file.isFile) {
// 3. 루트 폴더 바로 아래에 널브러진 파일도 제외하고 싶다면 (rootDir.name 이 target에 있으므로)
if (file.parentFile == rootDir && excludedDirs.contains(rootDir.name)) return@forEach
val ext = file.extension.lowercase()
val isMatch = when (filterType) {
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)
}
if (isMatch) unorganizedFiles.add(file)
}
}
if (unorganizedFiles.isEmpty()) {
Toast.makeText(context, "정리할 ${filterType.label} 파일이 없습니다.", Toast.LENGTH_SHORT).show()
return
}
// 파일 정렬 (내추럴 정렬 사용)
val sortedFiles = unorganizedFiles.sortedWith(NaturalOrderComparator())
val checkedItems = BooleanArray(sortedFiles.size) { false }
// 리스트에 보여줄 텍스트 가공 (어느 폴더에 있던 파일인지 알기 쉽게 [폴더명] 표시)
val fileNames = sortedFiles.map { file ->
val sizeMb = file.length() / (1024.0 * 1024.0)
"📁 [${file.parentFile?.name}]\n📄 ${file.name} (%.2f MB)".format(sizeMb)
}.toTypedArray()
android.app.AlertDialog.Builder(requireContext())
.setTitle("흩어진 ${filterType.label} 파일 모아보기")
.setMultiChoiceItems(fileNames, checkedItems) { _, which, isChecked ->
checkedItems[which] = isChecked
}
.setPositiveButton("선택 항목 삭제") { _, _ ->
val filesToDelete = sortedFiles.filterIndexed { index, _ -> checkedItems[index] }
if (filesToDelete.isEmpty()) return@setPositiveButton
var delCount = 0
filesToDelete.forEach { if (it.delete()) delCount++ }
Toast.makeText(context, "${delCount}개의 파일이 삭제되었습니다.", Toast.LENGTH_SHORT).show()
loadFiles()
}
.setNegativeButton("선택 항목 이동") { _, _ ->
val filesToMove = sortedFiles.filterIndexed { index, _ -> checkedItems[index] }
if (filesToMove.isNotEmpty()) {
// 이전 답변에서 만든 다이얼로그 재활용!
showMoveDialogForFiles(filesToMove)
} else {
Toast.makeText(context, "선택된 항목이 없습니다.", Toast.LENGTH_SHORT).show()
}
}
.setNeutralButton("취소", null)
.show()
}
private fun showMoveDialogForFiles(filesToMove: List<File>) {
// 이동 가능한 디렉토리 목록 수집 (현재 폴더는 뺄 필요가 있다면 필터링 로직 추가 가능)
var target = arrayListOf<String>(rootDir.name,"이미지",
"비디오",
"문서",
"기타")
val folders = rootDir.listFiles()?.filter { it.isDirectory && target.contains(it.name) }?.toMutableList() ?: mutableListOf()
// 루트 폴더로 꺼내는 옵션도 추가
folders.add(0, rootDir)
if (folders.isEmpty()) {
Toast.makeText(context, "이동할 대상 폴더가 없습니다.", Toast.LENGTH_SHORT).show()
return
}
val folderNames = folders.map {
if (it.absolutePath == rootDir.absolutePath) "📁 [최상위 폴더로 꺼내기]" else "📁 ${it.name}"
}.toTypedArray()
android.app.AlertDialog.Builder(requireContext())
.setTitle("${filesToMove.size}개 항목 이동 대상 선택")
.setItems(folderNames) { _, which ->
val targetFolder = folders[which]
var moveCount = 0
filesToMove.forEach { file ->
val targetFile = File(targetFolder, file.name)
// 이름이 겹치지 않고 이동에 성공했다면 카운트 증가
if (file.renameTo(targetFile)) {
moveCount++
}
}
Toast.makeText(context, "${moveCount}개 항목 이동 완료", Toast.LENGTH_SHORT).show()
loadFiles()
}
.setNegativeButton("취소", null)
.show()
}
// 💡 선택한 파일 이동 다이얼로그
private fun showMoveDialog() {
if (selectedFiles.isEmpty()) return
// 이동 가능한 디렉토리 목록 수집 (루트 폴더 + 자식 폴더들)
val folders = rootDir.listFiles()?.filter { it.isDirectory }?.toMutableList() ?: mutableListOf()
if (currentDir.absolutePath != rootDir.absolutePath) {
folders.add(0, rootDir) // 하위 폴더일 경우 루트로 뺄 수 있게 옵션 추가
}
folders.removeAll { it.absolutePath == currentDir.absolutePath } // 현재 폴더는 타겟에서 제외
if (folders.isEmpty()) {
Toast.makeText(context, "이동할 대상 폴더가 없습니다.", Toast.LENGTH_SHORT).show()
return
}
val folderNames = folders.map { if (it.absolutePath == rootDir.absolutePath) "📁 [최상위 폴더로 꺼내기]" else "📁 ${it.name}" }.toTypedArray()
android.app.AlertDialog.Builder(requireContext())
.setTitle("${selectedFiles.size}개 항목 이동 대상 선택")
.setItems(folderNames) { _, which ->
val targetFolder = folders[which]
var moveCount = 0
selectedFiles.forEach { file ->
val targetFile = File(targetFolder, file.name)
if (file.renameTo(targetFile)) moveCount++
}
Toast.makeText(context, "${moveCount}개 항목 이동 완료", Toast.LENGTH_SHORT).show()
toggleSelectionMode(false)
loadFiles()
}
.setNegativeButton("취소", null)
.show()
}
private fun getViewModeSymbolName(mode: FileViewMode): String {
return when (mode) {
FileViewMode.LIST_TEXT -> "view_headline"
@ -136,19 +402,36 @@ class CompletedFilesFragment : Fragment() {
FileViewMode.GRID_SMALL -> "apps"
}
}
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)
// 💡 XML에 추가된 자동 정리 버튼 연동 (ID를 btnOrganize로 만들었다고 가정)
val btnOrganize = view.findViewById<TextView>(R.id.btnOrganize)
btnOrganize?.setOnClickListener {
organizeRootFiles()
// 💡 선택 액션 바 버튼 이벤트
view.findViewById<View>(R.id.btnCancelSelection)?.setOnClickListener { toggleSelectionMode(false) }
view.findViewById<View>(R.id.btnMoveSelected)?.setOnClickListener { showMoveDialog() }
view.findViewById<View>(R.id.btnDeleteSelected)?.setOnClickListener {
if (selectedFiles.isEmpty()) return@setOnClickListener
android.app.AlertDialog.Builder(requireContext())
.setMessage("선택한 ${selectedFiles.size}개 항목을 삭제하시겠습니까?")
.setPositiveButton("삭제") { _, _ ->
var delCount = 0
selectedFiles.forEach { file ->
if (file.isDirectory) { file.deleteRecursively(); delCount++ }
else if (file.delete()) { delCount++ }
}
Toast.makeText(context, "${delCount}개 항목 삭제됨", Toast.LENGTH_SHORT).show()
toggleSelectionMode(false)
loadFiles()
}
.setNegativeButton("취소", null)
.show()
}
btnOrganize?.setOnClickListener { organizeRootFiles() }
btnViewMode.text = getViewModeSymbolName(currentViewMode)
btnViewMode.setOnClickListener {
currentViewMode = when (currentViewMode) {
@ -163,10 +446,29 @@ class CompletedFilesFragment : Fragment() {
}
val filterOptions = FileFilterType.values().map { it.label }
spinnerFilter.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, filterOptions).apply {
setDropDownViewResource(R.layout.spinner_item_dark)
}
// spinnerFilter.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, filterOptions)
spinnerFilter.adapter = object : ArrayAdapter<String>(requireContext(), android.R.layout.simple_spinner_dropdown_item, filterOptions) {
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getDropDownView(position, convertView, parent)
// 💡 드롭다운 안의 아이템에 롱클릭 리스너 장착
view.setOnLongClickListener {
val selectedFilterType = FileFilterType.values()[position]
// 롱클릭 시 방금 만든 다이얼로그 호출!
showUnorganizedFilesDialog(selectedFilterType)
// 다이얼로그가 뜨면 뒤에 열려있는 스피너 드롭다운 목록을 닫기 위해
// 시스템 뒤로가기 키 이벤트를 가짜로 발생시킵니다.
val root = spinnerFilter.rootView
root.dispatchKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_BACK))
root.dispatchKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_BACK))
true // true를 반환해야 일반 클릭(필터 적용)이 무시됩니다.
}
return view
}
}
spinnerFilter.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
currentFilter = FileFilterType.values()[position]
@ -175,11 +477,8 @@ class CompletedFilesFragment : Fragment() {
override fun onNothingSelected(p0: AdapterView<*>?) {}
}
val sortOptions = FileSortType.values().map { it.label }
spinnerSort.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, sortOptions).apply {
setDropDownViewResource(R.layout.spinner_item_dark)
}
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]
@ -196,17 +495,13 @@ class CompletedFilesFragment : Fragment() {
}
}
// 💡 루트 경로에 있는 파일들을 각각의 폴더로 이동시키는 자동 정리 함수
private fun organizeRootFiles() {
val filesInRoot = rootDir.listFiles()?.filter { it.isFile } ?: emptyList()
if (filesInRoot.isEmpty()) {
Toast.makeText(requireContext(), "루트 폴더에 정리할 파일이 없습니다.", Toast.LENGTH_SHORT).show()
return
}
var movedCount = 0
CoroutineScope(Dispatchers.IO).launch {
// 1. 루트 폴더의 파일 목록 가져오기
val filesInRoot = rootDir.listFiles()?.filter { it.isFile } ?: emptyList()
var movedCount = 0
// 2. 널브러진 파일들을 확장자별 폴더로 이동
filesInRoot.forEach { file ->
val ext = file.extension.lowercase()
val folderName = when {
@ -215,38 +510,59 @@ class CompletedFilesFragment : Fragment() {
extDocs.contains(ext) -> "문서"
else -> "기타"
}
val targetDir = File(rootDir, folderName)
if (!targetDir.exists()) targetDir.mkdirs()
val targetFile = File(targetDir, file.name)
// 동일한 이름의 파일이 이미 있다면 덮어쓰거나 이동 실패할 수 있으므로 주의 (필요시 중복 네이밍 처리)
if (file.renameTo(targetFile)) {
if (file.renameTo(File(targetDir, file.name))) {
movedCount++
}
}
withContext(Dispatchers.Main) {
if (movedCount > 0) {
Toast.makeText(requireContext(), "${movedCount}개의 파일을 자동 정리했습니다.", Toast.LENGTH_SHORT).show()
// 만약 사용자가 지금 루트 폴더를 보고 있다면 리스트를 즉시 갱신
if (currentDir.absolutePath == rootDir.absolutePath) {
loadFiles()
// 💡 3. 빈 폴더 싹쓸이 기능 추가 (Bottom-Up 방식)
var deletedFolderCount = 0
// walkBottomUp()을 사용하면 가장 깊은 하위 폴더부터 위로 올라오면서 검사합니다.
rootDir.walkBottomUp().forEach { dir ->
// 최상위 루트 폴더 자체는 지우면 안 되므로 통과
if (dir != rootDir && dir.isDirectory) {
// 폴더 내부에 아무것도 없다면 삭제
if (dir.listFiles()?.isEmpty() == true) {
if (dir.delete()) {
deletedFolderCount++
}
}
}
}
// 4. 메인 스레드에서 결과 메시지 출력 및 리스트 갱신
withContext(Dispatchers.Main) {
if (movedCount > 0 || deletedFolderCount > 0) {
val msg = buildString {
if (movedCount > 0) append("${movedCount}개의 파일을 폴더로 정리했습니다.\n")
if (deletedFolderCount > 0) append("${deletedFolderCount}개의 빈 폴더를 삭제했습니다.")
}.trim()
Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show()
// 파일 이동이나 폴더 삭제가 하나라도 일어났다면 현재 화면 새로고침
loadFiles()
} else {
Toast.makeText(requireContext(), "파일 정리에 실패했습니다.", Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), "정리할 파일이나 빈 폴더가 없습니다.", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun loadFiles() {
if (currentDir.exists()) {
allFiles = currentDir.listFiles()?.toList() ?: emptyList()
} else {
allFiles = emptyList()
private fun navigateUp() {
if (currentDir.absolutePath != rootDir.absolutePath) {
currentDir = currentDir.parentFile ?: rootDir
loadFiles()
}
backPressedCallback.isEnabled = currentDir.absolutePath != rootDir.absolutePath
}
private fun loadFiles() {
allFiles = if (currentDir.exists()) currentDir.listFiles()?.toList() ?: emptyList() else emptyList()
backPressedCallback.isEnabled = isSelectionMode || currentDir.absolutePath != rootDir.absolutePath
applyFilterAndSort()
}
@ -269,37 +585,19 @@ class CompletedFilesFragment : Fragment() {
FileFilterType.OTHER -> !extImages.contains(ext) && !extVideos.contains(ext) && !extDocs.contains(ext)
}
}
val naturalComparator = NaturalOrderComparator()
val sortedFolders = folders.sortedBy { it.name.lowercase() }
val sortedFolders = folders.sortedWith(naturalComparator)
val sortedFiles = when (currentSort) {
FileSortType.DOWNLOAD_DATE -> {
if (isDescending) files.sortedByDescending { it.lastModified() }
else files.sortedBy { it.lastModified() }
}
FileSortType.LAST_USED -> {
if (isDescending) files.sortedByDescending { getLastAccessedTime(it.name) }
else files.sortedBy { getLastAccessedTime(it.name) }
}
FileSortType.SIZE -> {
if (isDescending) files.sortedByDescending { it.length() }
else files.sortedBy { it.length() }
}
FileSortType.FREQUENTLY_USED -> {
if (isDescending) files.sortedByDescending { getAccessCount(it.name) }
else files.sortedBy { getAccessCount(it.name) }
}
FileSortType.NAME -> {
if (isDescending) files.sortedWith(naturalComparator.reversed())
else files.sortedWith(naturalComparator)
}
FileSortType.DOWNLOAD_DATE -> if (isDescending) files.sortedByDescending { it.lastModified() } else files.sortedBy { it.lastModified() }
FileSortType.LAST_USED -> if (isDescending) files.sortedByDescending { getLastAccessedTime(it.name) } else files.sortedBy { getLastAccessedTime(it.name) }
FileSortType.SIZE -> if (isDescending) files.sortedByDescending { it.length() } else files.sortedBy { it.length() }
FileSortType.FREQUENTLY_USED -> if (isDescending) files.sortedByDescending { getAccessCount(it.name) } else files.sortedBy { getAccessCount(it.name) }
FileSortType.NAME -> if (isDescending) files.sortedWith(naturalComparator.reversed()) else files.sortedWith(naturalComparator)
}
val finalItems = mutableListOf<File>()
if (currentDir.absolutePath != rootDir.absolutePath) {
finalItems.add(File(currentDir, ".."))
}
if (currentDir.absolutePath != rootDir.absolutePath) finalItems.add(File(currentDir, ".."))
finalItems.addAll(sortedFolders)
finalItems.addAll(sortedFiles)
@ -308,24 +606,13 @@ class CompletedFilesFragment : Fragment() {
private fun trackFileAccess(fileName: String) {
val prefs = requireContext().getSharedPreferences("FileAccessTracker", Context.MODE_PRIVATE)
val currentCount = prefs.getInt("${fileName}_count", 0)
prefs.edit()
.putLong(fileName, System.currentTimeMillis())
.putInt("${fileName}_count", currentCount + 1)
.putInt("${fileName}_count", prefs.getInt("${fileName}_count", 0) + 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)
}
private fun getLastAccessedTime(fileName: String) = requireContext().getSharedPreferences("FileAccessTracker", Context.MODE_PRIVATE).getLong(fileName, 0L)
private fun getAccessCount(fileName: String) = requireContext().getSharedPreferences("FileAccessTracker", Context.MODE_PRIVATE).getInt("${fileName}_count", 0)
private fun getMimeType(file: File): String {
val extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(file).toString()) ?: file.extension
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.lowercase()) ?: "*/*"
@ -334,20 +621,13 @@ class CompletedFilesFragment : Fragment() {
private fun openPrivateFile(context: Context, file: File) {
try {
trackFileAccess(file.name)
// 💡 파일을 열고나서 즉시 접근 횟수 및 뷰 반영을 위해 다시 정렬/로드합니다.
loadFiles()
val uri: Uri = FileProvider.getUriForFile(context, "bums.lunatic.launcher.fileprovider", file)
val mimeType = getMimeType(file)
val uri = FileProvider.getUriForFile(context, "bums.lunatic.launcher.fileprovider", file)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, mimeType)
setDataAndType(uri, getMimeType(file))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(intent)
} catch (e: IllegalArgumentException) {
Toast.makeText(context, "파일을 공유할 수 없습니다.", Toast.LENGTH_SHORT).show()
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, "이 파일을 열 수 있는 앱이 설치되어 있지 않습니다.", Toast.LENGTH_SHORT).show()
}
@ -355,12 +635,9 @@ class CompletedFilesFragment : Fragment() {
private fun updateRecyclerViewLayoutManager() {
recyclerView.layoutManager = when (currentViewMode) {
FileViewMode.LIST_TEXT, FileViewMode.LIST_THUMB ->
LinearLayoutManager(requireContext())
FileViewMode.GRID_LARGE ->
GridLayoutManager(requireContext(), 2)
FileViewMode.GRID_SMALL ->
GridLayoutManager(requireContext(), 4)
FileViewMode.LIST_TEXT, FileViewMode.LIST_THUMB -> LinearLayoutManager(requireContext())
FileViewMode.GRID_LARGE -> GridLayoutManager(requireContext(), 2)
FileViewMode.GRID_SMALL -> GridLayoutManager(requireContext(), 4)
}
}
}
@ -369,12 +646,15 @@ class CompletedFilesFragment : Fragment() {
class CompletedFilesAdapter(
private val onItemClick: (File) -> Unit,
private val onItemLongClick: (File) -> Unit,
private val getAccessCount: (String) -> Int // 💡 접근 빈도를 가져오기 위한 람다 함수 추가
private val getAccessCount: (String) -> Int
) : RecyclerView.Adapter<CompletedFilesAdapter.FileViewHolder>() {
private var fileList: List<File> = emptyList()
private var viewMode: FileViewMode = FileViewMode.LIST_THUMB
// 💡 어댑터 내부에 선택된 파일 정보 저장
private var selectedFiles: Set<File> = emptySet()
fun submitList(files: List<File>) {
fileList = files
notifyDataSetChanged()
@ -385,25 +665,28 @@ class CompletedFilesAdapter(
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int {
return viewMode.ordinal
// 💡 선택 상태가 바뀔 때 UI 갱신을 위해 호출
fun updateSelection(selection: Set<File>) {
selectedFiles = selection
notifyDataSetChanged()
}
override fun getItemViewType(position: Int) = 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)
return FileViewHolder(LayoutInflater.from(parent.context).inflate(layoutRes, parent, false))
}
override fun onBindViewHolder(holder: FileViewHolder, position: Int) {
holder.bind(fileList[position], viewMode)
}
override fun getItemCount(): Int = fileList.size
override fun getItemCount() = fileList.size
inner class FileViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvFileName: TextView? = itemView.findViewById(R.id.tvFileName)
@ -411,10 +694,16 @@ class CompletedFilesAdapter(
private val ivThumb: ImageView? = itemView.findViewById(R.id.ivThumb)
fun bind(file: File, mode: FileViewMode) {
// 💡 선택된 아이템이면 배경색을 푸른색 반투명 효과로 표시
if (selectedFiles.contains(file)) {
itemView.setBackgroundColor(Color.parseColor("#440099FF"))
} else {
itemView.setBackgroundColor(Color.TRANSPARENT)
}
if (file.name == "..") {
tvFileName?.text = "📁 .. (상위 폴더로)"
tvFileSize?.text = "이전 경로"
if (ivThumb != null) {
Glide.with(itemView.context).clear(ivThumb)
ivThumb.setImageResource(android.R.drawable.ic_menu_revert)
@ -423,7 +712,6 @@ class CompletedFilesAdapter(
tvFileName?.text = "📁 ${file.name}"
val count = file.listFiles()?.size ?: 0
tvFileSize?.text = "항목 $count"
if (ivThumb != null) {
Glide.with(itemView.context).clear(ivThumb)
ivThumb.setImageResource(android.R.drawable.ic_menu_gallery)
@ -431,18 +719,11 @@ class CompletedFilesAdapter(
} else {
tvFileName?.text = file.name
val sizeMb = file.length() / (1024.0 * 1024.0)
// 💡 접근 빈도를 가져와 텍스트에 포함시킵니다.
val accessCount = getAccessCount(file.name)
val countText = if (accessCount > 0) "조회: ${accessCount}" else "새 파일"
tvFileSize?.text = String.format("%.2f MB • %s • %s", sizeMb, file.extension.uppercase(), countText)
if (ivThumb != null) {
Glide.with(itemView.context)
.load(file)
.placeholder(android.R.drawable.ic_menu_report_image)
.into(ivThumb)
Glide.with(itemView.context).load(file).placeholder(android.R.drawable.ic_menu_report_image).into(ivThumb)
}
}

View File

@ -68,6 +68,7 @@ import org.mozilla.geckoview.WebExtension
import org.mozilla.geckoview.WebExtension.MessageDelegate
import org.mozilla.geckoview.WebExtension.PortDelegate
import org.mozilla.geckoview.WebExtensionController.AddonManagerDelegate
import org.mozilla.geckoview.WebRequestError
import org.mozilla.geckoview.WebResponse
import java.io.File
import java.io.FileOutputStream
@ -343,11 +344,31 @@ open class GeckoWeb @JvmOverloads constructor(
// scrollState = 0
}
override fun onLoadError(
session: GeckoSession,
uri: String?,
error: WebRequestError
): GeckoResult<String?>? {
Blog.LOGE("Gecko 에러 발생!! URI: $uri, Code: ${error.code}, Category: ${error.category}")
if (uri?.contains("booktoki") == true) {
when(lastArrow) {
1->{sendJsonMsg("CLICK_NEXT_CHAPTER")}
-1->{sendJsonMsg("CLICK_PREV_CHAPTER")}
else -> {}
}
} else {
session.reload()
}
return null
}
override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
this@GeckoWeb.canGoBack = canGoBack
}
}
var lastArrow = 0
fun goBack() {
if (true == canGoBack)
session?.goBack()
@ -708,7 +729,7 @@ open class GeckoWeb @JvmOverloads constructor(
private fun View.post(action: () -> Unit) = this.post(Runnable(action))
fun onPause() {
saveCurrentSessionState()
session?.stop()
// session?.stop()
session?.setActive(false)
Blog.LOGE("called onstop $lastedUrl")
}

View File

@ -310,7 +310,8 @@ open class NeoRssActivity : CommonActivity() {
}
override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
Blog.LOGE("keyEvent >>>>> dispatchGenericMotionEvent ${ev}")
if ("Virtual".equals(ev?.device?.name)) return true
Blog.LOGE("keyEvent >>>>> dispatchGenericMotionEvent ${ev?.device?.name ?: ""}: ${ev} ")
if (ev?.device?.name?.contains("BLE-M3") == true) {
ev?.action?.let { action ->
when(action) {
@ -354,6 +355,8 @@ open class NeoRssActivity : CommonActivity() {
Blog.LOGE("Arrow Up Click")
}
}
// if (onExit) return true
return false
}
@ -615,9 +618,9 @@ open class NeoRssActivity : CommonActivity() {
is TokiFragment -> {
currentFragment.back()
}
// is Novels -> {
// currentFragment.actionNextEvent(false)
// }
is CompletedFilesFragment -> {
currentFragment.backPress()
}
else -> {
// showContents(R.id.close)
}

View File

@ -414,6 +414,8 @@ object HistoryManager {
url = url.replace("//","/").trim()
}
}
Blog.LOGE("CURENTPATH $url")
var contentsPageInfo =
this.query(ContentsPageInfo::class).query("contentsType == $0", contentsType).query("pathUrl == $0", url).find()
if (contentsPageInfo.size > 0) {

View File

@ -307,7 +307,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
}
override fun onKeyEvent(event: KeyEvent): Boolean {
if (!isbooktoki()) return false // 조건부 처리
// if (!isbooktoki()) return false // 조건부 처리
val isUp = event.action == MotionEvent.ACTION_UP
return when (event.keyCode) {
@ -393,16 +393,16 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
override fun onLongClick() {
if (!enableGestures) return
if (contentsType.contains("book") &&
currentPage?.bookTitle?.isNotEmpty() == true &&
currentPage?.chapterTitle?.isNotEmpty() == true &&
binding.pagedLayer.text.isNotEmpty() == true && (binding.pagedLayer.text.length > 100)) {
currentPage?.bookTitle?.let { title ->
currentPage?.contents?.let { contents ->
saveTextToPrivateVault(title, "${this.currentChapter}_${this.currentTitle}",binding.pagedLayer.text)
}
}
}
// if (contentsType.contains("book") &&
// currentPage?.bookTitle?.isNotEmpty() == true &&
// currentPage?.chapterTitle?.isNotEmpty() == true &&
// binding.pagedLayer.text.isNotEmpty() == true && (binding.pagedLayer.text.length > 100)) {
// currentPage?.bookTitle?.let { title ->
// currentPage?.contents?.let { contents ->
// saveTextToPrivateVault(title, "${this.currentChapter}_${this.currentTitle}",binding.pagedLayer.text)
// }
// }
// }
Blog.LOGD(log = "onLongClick")
}
@ -1045,6 +1045,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
if (pathUrl != null && pathUrl.length > 6) {
HistoryManager.getNextPage(contentsType,pathUrl) {
if (it != null && (it.pathUrl?.length ?: 0) > 6) {
binding.lunaticBrowser.geckoWeb.lastArrow = 1
moveTo(it)
} else {
showToast("다음 편이 없다요.")
@ -1057,6 +1058,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
if (pathUrl != null && pathUrl.length > 6) {
HistoryManager.getPrevPage(contentsType,pathUrl) {
if (it != null && (it.pathUrl?.length ?: 0) > 6) {
binding.lunaticBrowser.geckoWeb.lastArrow = -1
moveTo(it)
} else {
showToast("이전 편이 없다요.")
@ -1099,12 +1101,46 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
fun actionNextEvent(fast: Boolean = false) {
if (binding.pagedLayer.isVisible && binding.pagedLayer.size() > 0 && (binding.pagedLayer.current() < binding.pagedLayer!!.size() - 1)) {
binding.pagedLayer.doNext(fast)
binding.lunaticBrowser.geckoWeb.pageDown()
updateLastInfo(binding.pagedLayer!!)
if (contentsType.contains("book")) {
if (binding.pagedLayer.isVisible && binding.pagedLayer.size() > 0 && (binding.pagedLayer.current() < binding.pagedLayer!!.size() - 1)) {
binding.pagedLayer.doNext(fast)
binding.lunaticBrowser.geckoWeb.pageDown()
updateLastInfo(binding.pagedLayer)
} else {
binding.pagedLayer.visibility = View.GONE
binding.lunaticBrowser.visibility = View.VISIBLE
moveToNext(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)
}
} else if (contentsType.contains("comics")) {
sendViewerTouch("right")
} else {
moveToNext(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)
if (binding.lunaticBrowser.geckoWeb.scrollState > 0) {
} else {
binding.lunaticBrowser.geckoWeb.pageDown()
}
}
}
fun actionPrevEvent(fast: Boolean = false) {
if(contentsType.contains("book")) {
if (binding.pagedLayer.isVisible && binding.pagedLayer.size() > 0 && binding.pagedLayer.current() > 0) {
binding.pagedLayer.doPrev(fast)
binding.lunaticBrowser.geckoWeb.pageUp()
updateLastInfo(binding.pagedLayer)
} else {
binding.pagedLayer.visibility = View.GONE
binding.lunaticBrowser.visibility = View.VISIBLE
moveToPrev(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)
}
} else if (contentsType.contains("comics")) {
sendViewerTouch("left")
} else {
if (binding.lunaticBrowser.geckoWeb.scrollState < 0) {
} else {
binding.lunaticBrowser.geckoWeb.pageUp()
}
}
}
@ -1135,16 +1171,6 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
}
}
fun actionPrevEvent(fast: Boolean = false) {
if (binding.pagedLayer.isVisible && binding.pagedLayer.size() > 0 && binding.pagedLayer.current() > 0) {
binding.pagedLayer.doPrev(fast)
binding.lunaticBrowser.geckoWeb.pageUp()
updateLastInfo(binding.pagedLayer)
} else {
moveToPrev(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)
}
}
fun showAlert(alert: String) {
Log.i(TAG, "showAlert >> " + alert)
}
@ -1239,6 +1265,7 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
view.visibility = if (!saveContinuation) VISIBLE else GONE
binding.lunaticBrowser.visibility = if (saveContinuation) VISIBLE else GONE
}
binding.lunaticBrowser.geckoWeb.lastArrow = 0
// view.forceUpdateUI()
lastedUrl?.let {
Uri.parse(it)?.let { uri ->

View File

@ -152,7 +152,7 @@
style="@style/SearchAccs"
app:autoSizeTextType="uniform"
android:id="@+id/search_music"
android:text="SPOTIFY"
android:text="MUSIC"
/>
<TextView
style="@style/SearchAccs"

View File

@ -75,6 +75,47 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewCompletedFiles"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="8dp"/>
<LinearLayout
android:id="@+id/layoutSelectionActions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="#EEEEEE"
android:padding="8dp"
android:visibility="gone">
<TextView
android:id="@+id/btnCancelSelection"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="취소"
android:textStyle="bold"
android:gravity="center"
android:padding="12dp"/>
<TextView
android:id="@+id/btnMoveSelected"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="폴더 이동"
android:textStyle="bold"
android:gravity="center"
android:padding="12dp"/>
<TextView
android:id="@+id/btnDeleteSelected"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="선택 삭제"
android:textColor="#FF0000"
android:textStyle="bold"
android:gravity="center"
android:padding="12dp"/>
</LinearLayout>
</LinearLayout>