From 3f0a7d0d5c0f43290d604c85aee13d5200ee0b5e Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Fri, 20 Mar 2026 17:55:48 +0900 Subject: [PATCH] .. --- .../extensions/my_extension/messaging.js | 30 + .../launcher/apps/AppDrawerBottomSheet.kt | 37 +- .../launcher/home/CompletedFilesFragment.kt | 561 +++++++++++++----- .../bums/lunatic/launcher/home/GeckoWeb.kt | 25 +- .../lunatic/launcher/home/NeoRssActivity.kt | 11 +- .../launcher/home/tokiz/HistoryManager.kt | 2 + .../launcher/home/tokiz/TokiFragment.kt | 79 ++- .../res/layout/bottom_sheet_app_drawer.xml | 2 +- .../res/layout/fragment_completed_files.xml | 43 +- 9 files changed, 598 insertions(+), 192 deletions(-) diff --git a/app/src/main/assets/extensions/my_extension/messaging.js b/app/src/main/assets/extensions/my_extension/messaging.js index 486bb016..2765df7d 100644 --- a/app/src/main/assets/extensions/my_extension/messaging.js +++ b/app/src/main/assets/extensions/my_extension/messaging.js @@ -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); diff --git a/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt b/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt index b23ec298..66b2ff09 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt @@ -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") } - // ... 나머지 버튼들도 동일하게 추가 ... + } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt index ceec1aa5..091e4037 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt @@ -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 { + 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() + 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(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() + + 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) { + // 이동 가능한 디렉토리 목록 수집 (현재 폴더는 뺄 필요가 있다면 필터링 로직 추가 가능) + var target = arrayListOf(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(R.id.spinnerFilter) val spinnerSort = view.findViewById(R.id.spinnerSort) val tvSortOrder = view.findViewById(R.id.tvSortOrder) val btnViewMode = view.findViewById(R.id.btnViewMode) - - // 💡 XML에 추가된 자동 정리 버튼 연동 (ID를 btnOrganize로 만들었다고 가정) val btnOrganize = view.findViewById(R.id.btnOrganize) - btnOrganize?.setOnClickListener { - organizeRootFiles() + + // 💡 선택 액션 바 버튼 이벤트 + view.findViewById(R.id.btnCancelSelection)?.setOnClickListener { toggleSelectionMode(false) } + view.findViewById(R.id.btnMoveSelected)?.setOnClickListener { showMoveDialog() } + view.findViewById(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(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() - - 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() { private var fileList: List = emptyList() private var viewMode: FileViewMode = FileViewMode.LIST_THUMB + // 💡 어댑터 내부에 선택된 파일 정보 저장 + private var selectedFiles: Set = emptySet() + fun submitList(files: List) { fileList = files notifyDataSetChanged() @@ -385,25 +665,28 @@ class CompletedFilesAdapter( notifyDataSetChanged() } - override fun getItemViewType(position: Int): Int { - return viewMode.ordinal + // 💡 선택 상태가 바뀔 때 UI 갱신을 위해 호출 + fun updateSelection(selection: Set) { + 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) } } 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 1e15a2cc..61d92f34 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt @@ -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? { + + 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") } 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 13ec88c7..2a26fcd9 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt @@ -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) } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/HistoryManager.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/HistoryManager.kt index 1dd26746..7245b560 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/HistoryManager.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/HistoryManager.kt @@ -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) { diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt index e1ce12d1..2819ebfa 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/tokiz/TokiFragment.kt @@ -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 -> diff --git a/app/src/main/res/layout/bottom_sheet_app_drawer.xml b/app/src/main/res/layout/bottom_sheet_app_drawer.xml index f8d81d2c..b547114e 100644 --- a/app/src/main/res/layout/bottom_sheet_app_drawer.xml +++ b/app/src/main/res/layout/bottom_sheet_app_drawer.xml @@ -152,7 +152,7 @@ style="@style/SearchAccs" app:autoSizeTextType="uniform" android:id="@+id/search_music" - android:text="SPOTIFY" + android:text="MUSIC" /> + + + + + + + + \ No newline at end of file